DEV: Migrate discourse core to Ember initializers (#22095)

https://meta.discourse.org/t/updating-our-initializer-naming-patterns/241919

For historical reasons, Discourse has different initializers conventions than standard Ember:

```
| Ember                 | Discourse          |                        |
| initializers          | pre-initializers   | runs once per app load |
| instance-initializers | (api-)initializers | runs once per app boot |
```

In addition, the arguments to the initialize function is different – Ember initializers get either the `Application` or `ApplicationInstance` as the only argument, but the "Discourse style" gets an extra container argument preceding that.

This is confusing, but it also causes problems with Ember addons, which expects the standard naming and argument conventions:

1. Typically, V1 addons will define their (app, instance) initializers in the `addon/(instance-)initializers/*`, which appears as `ember-some-addon-package-name/(instance-)initializers/*` in the require registry.

2. Just having those modules defined isn't supposed to do anything, so typically they also re-export them in `app/(instance-)initializers/*`, which gets merged into `discourse/(instance-)initializers/*` in the require registry.

3. The `ember-cli-load-initializers` package supplies a function called `loadInitializers`, which typically gets called in `app.js` to load the initializers according to the conventions above. Since we don't follow the same conventions, we can't use this function and instead have custom code in `app.js`, loosely based on official version but attempts to account for the different conventions.

The custom code that loads initializers is written with Discourse core and plug-ins/themes in mind, but does not take into account the fact that addons can also bring initializers, which causes the following problems:

* It does not check for the `discourse/` module prefix, so initializers in the `addon/` folders (point 1 above) get picked up as well. This means the initializer code is probably registered twice (once from the `addon/` folder, once from the `app/` re-export). This either causes a dev mode assertion (if they have the same name) or causes the code to run twice (if they have different names somehow).

* In modern Ember blueprints, it is customary to omit the `"name"` of the initializer since `ember-cli-load-initializers` can infer it from the module name. Our custom code does not do this and causes a dev mode assertion instead.

* It runs what then addon intends to be application initializers as instance initializers due to the naming difference. There is at least one known case of this where the `ember-export-application-global` application initialize is currently incorrectly registered as an instance initializer. (It happens to not use the `/addon` folder convention and explicitly names the initializer, so it does not trigger the previous error scenarios.)

* It runs the initializers with the wrong arguments. If all the addon initializer does is lookup stuff from the container, it happens to work, otherwise... ???

* It does not check for the `/instance-initializers/` module path so any instance initializers introduced by addons are silently ignored.

These issues were discovered when trying to install an addon that brings an application initializer in #22023.

To resolve these issues, this commit:

* Migrates Discourse core to use the standard Ember conventions – both in the naming and the arguments of the initialize function

* Updates the custom code for loading initializers:
  * For Discourse core, it essentially does the same thing as `ember-cli-load-initializers`
  * For plugins and themes, it preserves the existing Discourse conventions and semantics (to be revisited at a later time)

This ensures that going forward, Ember addons will function correctly.
This commit is contained in:
Godfrey Chan 2023-06-15 05:17:43 -07:00 committed by GitHub
parent 79a260a6bb
commit fa509224f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 449 additions and 460 deletions

View File

@ -1,5 +1,6 @@
import "./global-compat";
import require from "require";
import Application from "@ember/application";
import { buildResolver } from "discourse-common/resolver";
import { isTesting } from "discourse-common/config/environment";
@ -19,40 +20,6 @@ const Discourse = Application.extend({
Resolver: buildResolver("discourse"),
_prepareInitializer(moduleName) {
const themeId = moduleThemeId(moduleName);
let module = null;
try {
module = requirejs(moduleName, null, null, true);
if (!module) {
throw new Error(moduleName + " must export an initializer.");
}
} catch (error) {
if (!themeId || isTesting()) {
throw error;
}
fireThemeErrorEvent({ themeId, error });
return;
}
const init = module.default;
const oldInitialize = init.initialize;
init.initialize = (app) => {
try {
return oldInitialize.call(init, app.__container__, app);
} catch (error) {
if (!themeId || isTesting()) {
throw error;
}
fireThemeErrorEvent({ themeId, error });
}
};
return init;
},
// Start up the Discourse application by running all the initializers we've defined.
start() {
document.querySelector("noscript")?.remove();
@ -66,30 +33,7 @@ const Discourse = Application.extend({
Error.stackTraceLimit = Infinity;
}
Object.keys(requirejs._eak_seen).forEach((key) => {
if (/\/pre\-initializers\//.test(key)) {
const initializer = this._prepareInitializer(key);
if (initializer) {
this.initializer(initializer);
}
} else if (/\/(api\-)?initializers\//.test(key)) {
const initializer = this._prepareInitializer(key);
if (initializer) {
this.instanceInitializer(initializer);
}
}
});
// Plugins that are registered via `<script>` tags.
const withPluginApi = requirejs("discourse/lib/plugin-api").withPluginApi;
let initCount = 0;
_pluginCallbacks.forEach((cb) => {
this.instanceInitializer({
name: `_discourse_plugin_${++initCount}`,
after: "inject-objects",
initialize: () => withPluginApi(cb.version, cb.code),
});
});
loadInitializers(this);
},
_registerPluginCode(version, code) {
@ -129,4 +73,136 @@ export function getAndClearUnhandledThemeErrors() {
return copy;
}
/**
* Logic for loading initializers. Similar to ember-cli-load-initializers, but
* has some discourse-specific logic to handle loading initializers from
* plugins and themes.
*/
function loadInitializers(app) {
let initializers = [];
let instanceInitializers = [];
let discourseInitializers = [];
let discourseInstanceInitializers = [];
for (let moduleName of Object.keys(requirejs._eak_seen)) {
if (moduleName.startsWith("discourse/") && !moduleName.endsWith("-test")) {
// In discourse core, initializers follow standard Ember conventions
if (moduleName.startsWith("discourse/initializers/")) {
initializers.push(moduleName);
} else if (moduleName.startsWith("discourse/instance-initializers/")) {
instanceInitializers.push(moduleName);
} else {
// https://meta.discourse.org/t/updating-our-initializer-naming-patterns/241919
//
// For historical reasons, the naming conventions in plugins and themes
// differs from Ember:
//
// | Ember | Discourse | |
// | initializers | pre-initializers | runs once per app load |
// | instance-initializers | (api-)initializers | runs once per app boot |
//
// In addition, the arguments to the initialize function is different
// Ember initializers get either the `Application` or `ApplicationInstance`
// as the only argument, but the "discourse style" gets an extra container
// argument preceding that.
const themeId = moduleThemeId(moduleName);
if (
themeId !== undefined ||
moduleName.startsWith("discourse/plugins/")
) {
if (moduleName.includes("/pre-initializers/")) {
discourseInitializers.push([moduleName, themeId]);
} else if (
moduleName.includes("/initializers/") ||
moduleName.includes("/api-initializers/")
) {
discourseInstanceInitializers.push([moduleName, themeId]);
}
}
}
}
}
for (let moduleName of initializers) {
app.initializer(resolveInitializer(moduleName));
}
for (let moduleName of instanceInitializers) {
app.instanceInitializer(resolveInitializer(moduleName));
}
for (let [moduleName, themeId] of discourseInitializers) {
app.initializer(resolveDiscourseInitializer(moduleName, themeId));
}
for (let [moduleName, themeId] of discourseInstanceInitializers) {
app.instanceInitializer(resolveDiscourseInitializer(moduleName, themeId));
}
// Plugins that are registered via `<script>` tags.
const { withPluginApi } = require("discourse/lib/plugin-api");
for (let [i, callback] of _pluginCallbacks.entries()) {
app.instanceInitializer({
name: `_discourse_plugin_${i}`,
after: "inject-objects",
initialize: () => withPluginApi(callback.version, callback.code),
});
}
}
function resolveInitializer(moduleName) {
const module = require(moduleName, null, null, true);
if (!module) {
throw new Error(moduleName + " must export an initializer.");
}
const initializer = module["default"];
if (!initializer) {
throw new Error(moduleName + " must have a default export");
}
if (!initializer.name) {
initializer.name = moduleName.slice(moduleName.lastIndexOf("/") + 1);
}
return initializer;
}
function resolveDiscourseInitializer(moduleName, themeId) {
let initializer;
try {
initializer = resolveInitializer(moduleName);
} catch (error) {
if (!themeId || isTesting()) {
throw error;
} else {
fireThemeErrorEvent({ themeId, error });
return;
}
}
const oldInitialize = initializer.initialize;
initializer.initialize = (app) => {
try {
return oldInitialize.call(initializer, app.__container__, app);
} catch (error) {
if (!themeId || isTesting()) {
throw error;
} else {
fireThemeErrorEvent({ themeId, error });
}
}
};
return initializer;
}
export default Discourse;

View File

@ -16,16 +16,14 @@ import runloop from "@ember/runloop";
import { DEBUG } from "@glimmer/env";
export default {
name: "discourse-bootstrap",
// The very first initializer to run
initialize(container) {
initialize(app) {
if (DEBUG) {
runloop._backburner.ASYNC_STACKS = true;
}
setURLContainer(container);
setDefaultOwner(container);
setURLContainer(app.__container__);
setDefaultOwner(app.__container__);
// Our test environment has its own bootstrap code
if (isTesting()) {

View File

@ -8,9 +8,8 @@ import { dasherize } from "@ember/string";
export default {
after: "inject-discourse-objects",
name: "dynamic-route-builders",
initialize(_container, app) {
initialize(app) {
app.register(
"controller:discovery.category",
DiscoverySortableController.extend()

View File

@ -9,11 +9,10 @@ import User from "discourse/models/user";
import { registerDiscourseImplicitInjections } from "discourse/lib/implicit-injections";
export default {
name: "inject-discourse-objects",
after: "discourse-bootstrap",
initialize(container, app) {
const siteSettings = container.lookup("service:site-settings");
initialize(app) {
const siteSettings = app.__container__.lookup("service:site-settings");
const currentUser = User.current();
@ -22,7 +21,7 @@ export default {
app.register("service:current-user", currentUser, { instantiate: false });
this.topicTrackingState = TopicTrackingState.create({
messageBus: container.lookup("service:message-bus"),
messageBus: app.__container__.lookup("service:message-bus"),
siteSettings,
currentUser,
});

View File

@ -1,10 +1,9 @@
import { mapRoutes } from "discourse/mapping-router";
export default {
name: "map-routes",
after: "inject-discourse-objects",
initialize(_, app) {
initialize(app) {
this.routerClass = mapRoutes();
app.register("router:main", this.routerClass);
},

View File

@ -1,10 +0,0 @@
import { registerServiceWorker } from "discourse/lib/register-service-worker";
export default {
name: "register-service-worker",
initialize(container) {
let { serviceWorkerURL } = container.lookup("service:session");
registerServiceWorker(container, serviceWorkerURL);
},
};

View File

@ -31,8 +31,6 @@ function animatedImgs() {
}
export default {
name: "animated-images-pause-on-click",
initialize() {
withPluginApi("0.8.7", (api) => {
function _cleanUp() {

View File

@ -1,8 +1,8 @@
import { next } from "@ember/runloop";
export default {
name: "auth-complete",
after: "inject-objects",
initialize(container) {
initialize(owner) {
let lastAuthResult;
if (document.getElementById("data-authentication")) {
@ -12,14 +12,14 @@ export default {
}
if (lastAuthResult) {
const router = container.lookup("router:main");
const router = owner.lookup("router:main");
router.one("didTransition", () => {
const controllerName =
router.currentPath === "invites.show" ? "invites-show" : "login";
next(() => {
let controller = container.lookup(`controller:${controllerName}`);
let controller = owner.lookup(`controller:${controllerName}`);
controller.authenticationComplete(JSON.parse(lastAuthResult));
});
});

View File

@ -7,7 +7,7 @@ import RawHandlebars from "discourse-common/lib/raw-handlebars";
import { registerRawHelpers } from "discourse-common/lib/raw-handlebars-helpers";
import { setOwner } from "@ember/application";
export function autoLoadModules(container, registry) {
export function autoLoadModules(owner, registry) {
Object.keys(requirejs.entries).forEach((entry) => {
if (/\/helpers\//.test(entry) && !/-test/.test(entry)) {
requirejs(entry, null, null, true);
@ -18,16 +18,16 @@ export function autoLoadModules(container, registry) {
});
let context = {
siteSettings: container.lookup("service:site-settings"),
keyValueStore: container.lookup("service:key-value-store"),
capabilities: container.lookup("service:capabilities"),
currentUser: container.lookup("service:current-user"),
site: container.lookup("service:site"),
session: container.lookup("service:session"),
topicTrackingState: container.lookup("service:topic-tracking-state"),
siteSettings: owner.lookup("service:site-settings"),
keyValueStore: owner.lookup("service:key-value-store"),
capabilities: owner.lookup("service:capabilities"),
currentUser: owner.lookup("service:current-user"),
site: owner.lookup("service:site"),
session: owner.lookup("service:session"),
topicTrackingState: owner.lookup("service:topic-tracking-state"),
registry,
};
setOwner(context, container);
setOwner(context, owner);
createHelperContext(context);
registerHelpers(registry);
@ -35,7 +35,8 @@ export function autoLoadModules(container, registry) {
}
export default {
name: "auto-load-modules",
after: "inject-objects",
initialize: (container) => autoLoadModules(container, container.registry),
initialize: (owner) => {
autoLoadModules(owner, owner.__container__.registry);
},
};

View File

@ -1,19 +1,18 @@
// Updates the PWA badging if available
export default {
name: "badging",
after: "message-bus",
initialize(container) {
initialize(owner) {
if (!navigator.setAppBadge) {
return;
} // must have the Badging API
const user = container.lookup("service:current-user");
const user = owner.lookup("service:current-user");
if (!user) {
return;
} // must be logged in
const appEvents = container.lookup("service:app-events");
const appEvents = owner.lookup("service:app-events");
appEvents.on("notifications:changed", () => {
let notifications;
notifications = user.all_unread_notifications_count;

View File

@ -3,12 +3,11 @@ import { bind } from "discourse-common/utils/decorators";
import PreloadStore from "discourse/lib/preload-store";
export default {
name: "banner",
after: "message-bus",
initialize(container) {
this.site = container.lookup("service:site");
this.messageBus = container.lookup("service:message-bus");
initialize(owner) {
this.site = owner.lookup("service:site");
this.messageBus = owner.lookup("service:message-bus");
const banner = EmberObject.create(PreloadStore.get("banner") || {});
this.site.set("banner", banner);

View File

@ -1,5 +1,4 @@
export default {
name: "category-color-css-generator",
after: "register-hashtag-types",
/**
@ -9,8 +8,8 @@ export default {
* It is also used when styling hashtag icons, since they are colored
* based on the category color.
*/
initialize(container) {
this.site = container.lookup("service:site");
initialize(owner) {
this.site = owner.lookup("service:site");
// If the site is login_required and the user is anon there will be no categories preloaded.
if (!this.site.categories) {

View File

@ -33,18 +33,17 @@ function _clean(transition) {
}
export default {
name: "clean-dom-on-route-change",
after: "inject-objects",
initialize(container) {
const router = container.lookup("router:main");
initialize(owner) {
const router = owner.lookup("router:main");
router.on("routeDidChange", (transition) => {
if (transition.isAborted) {
return;
}
scheduleOnce("afterRender", container, _clean, transition);
scheduleOnce("afterRender", owner, _clean, transition);
});
},
};

View File

@ -2,9 +2,8 @@ import DiscourseURL from "discourse/lib/url";
import interceptClick from "discourse/lib/intercept-click";
export default {
name: "click-interceptor",
initialize(container, app) {
this.selector = app.rootElement;
initialize(owner) {
this.selector = owner.rootElement;
$(this.selector).on("click.discourse", "a", interceptClick);
window.addEventListener("hashchange", this.hashChanged);
},

View File

@ -5,10 +5,8 @@ import CodeblockButtons from "discourse/lib/codeblock-buttons";
let _codeblockButtons = [];
export default {
name: "codeblock-buttons",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
withPluginApi("0.8.7", (api) => {
function _cleanUp() {

View File

@ -14,11 +14,10 @@ GlimmerManager.getComponentTemplate = (component) => {
};
export default {
name: "colocated-template-overrides",
after: ["populate-template-map", "mobile"],
initialize(container) {
this.site = container.lookup("service:site");
initialize(owner) {
this.site = owner.lookup("service:site");
this.eachThemePluginTemplate((templateKey, moduleNames, mobile) => {
if (!mobile && DiscourseTemplateMap.coreTemplates.has(templateKey)) {
@ -32,9 +31,7 @@ export default {
}
componentName = componentName.slice("components/".length);
const component = container.owner.resolveRegistration(
`component:${componentName}`
);
const component = owner.resolveRegistration(`component:${componentName}`);
if (component && originalGetTemplate(component)) {
const finalOverrideModuleName = moduleNames[moduleNames.length - 1];

View File

@ -4,10 +4,9 @@ let installed = false;
let callbacks = $.Callbacks();
export default {
name: "csrf-token",
initialize(container) {
initialize(owner) {
// Add a CSRF token to all AJAX requests
let session = container.lookup("service:session");
let session = owner.lookup("service:session");
session.set(
"csrfToken",
document.head.querySelector("meta[name=csrf-token]")?.content

View File

@ -1,8 +1,6 @@
import { showPopover } from "discourse/lib/d-popover";
export default {
name: "d-popover",
initialize() {
["click", "mouseover"].forEach((eventType) => {
document.addEventListener(eventType, (e) => {

View File

@ -1,8 +1,6 @@
import { eagerLoadRawTemplateModules } from "discourse-common/lib/raw-templates";
export default {
name: "eager-load-raw-templates",
initialize() {
eagerLoadRawTemplateModules();
},

View File

@ -3,10 +3,8 @@ import { registerEmoji } from "pretty-text/emoji";
import { withPluginApi } from "discourse/lib/plugin-api";
export default {
name: "enable-emoji",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
if (!siteSettings.enable_emoji) {
return;
}

View File

@ -7,8 +7,6 @@ import { isTesting } from "discourse-common/config/environment";
const DELAY = isTesting() ? 0 : 5000;
export default {
name: "handle-cookies",
initialize() {
// No need to block boot for this housekeeping - we can defer it a few seconds
later(() => {

View File

@ -1,7 +1,6 @@
import { getHashtagTypeClasses } from "discourse/lib/hashtag-autocomplete";
export default {
name: "hashtag-css-generator",
after: "category-color-css-generator",
/**
@ -13,8 +12,8 @@ export default {
* with the hastag type via api.registerHashtagType. The default
* ones in core are CategoryHashtagType and TagHashtagType.
*/
initialize(container) {
this.site = container.lookup("service:site");
initialize(owner) {
this.site = owner.lookup("service:site");
// If the site is login_required and the user is anon there will be no categories
// preloaded, so there will be no category color CSS variables generated by

View File

@ -2,12 +2,11 @@ import { withPluginApi } from "discourse/lib/plugin-api";
import { replaceHashtagIconPlaceholder } from "discourse/lib/hashtag-autocomplete";
export default {
name: "hashtag-post-decorations",
after: "hashtag-css-generator",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
const site = container.lookup("service:site");
initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
const site = owner.lookup("service:site");
withPluginApi("0.8.7", (api) => {
if (siteSettings.enable_experimental_hashtag_autocomplete) {

View File

@ -11,8 +11,6 @@ import { withPluginApi } from "discourse/lib/plugin-api";
// the lifetime of all `<img` elements.
export default {
name: "image-aspect-ratio",
initWithApi(api) {
const supportsAspectRatio = CSS.supports("aspect-ratio: 1");

View File

@ -4,14 +4,13 @@ import Site from "discourse/models/site";
import deprecated from "discourse-common/lib/deprecated";
export default {
name: "inject-objects",
after: "sniff-capabilities",
initialize(container, app) {
initialize(owner) {
// This is required for Ember CLI tests to work
setDefaultOwner(app.__container__);
setDefaultOwner(owner.__container__);
Object.defineProperty(app, "SiteSettings", {
Object.defineProperty(owner, "SiteSettings", {
get() {
deprecated(
`use injected siteSettings instead of Discourse.SiteSettings`,
@ -21,10 +20,10 @@ export default {
id: "discourse.global.site-settings",
}
);
return container.lookup("service:site-settings");
return owner.lookup("service:site-settings");
},
});
Object.defineProperty(app, "User", {
Object.defineProperty(owner, "User", {
get() {
deprecated(
`import discourse/models/user instead of using Discourse.User`,
@ -37,7 +36,7 @@ export default {
return User;
},
});
Object.defineProperty(app, "Site", {
Object.defineProperty(owner, "Site", {
get() {
deprecated(
`import discourse/models/site instead of using Discourse.Site`,

View File

@ -6,7 +6,6 @@ import deprecated from "discourse-common/lib/deprecated";
let jqueryPluginsConfigured = false;
export default {
name: "jquery-plugins",
initialize() {
if (jqueryPluginsConfigured) {
return;

View File

@ -2,10 +2,8 @@ import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import ItsATrap from "@discourse/itsatrap";
export default {
name: "keyboard-shortcuts",
initialize(container) {
KeyboardShortcuts.init(ItsATrap, container);
initialize(owner) {
KeyboardShortcuts.init(ItsATrap, owner);
KeyboardShortcuts.bindEvents();
},

View File

@ -5,11 +5,9 @@ import { bind } from "discourse-common/utils/decorators";
// Use the message bus for live reloading of components for faster development.
export default {
name: "live-development",
initialize(container) {
this.messageBus = container.lookup("service:message-bus");
const session = container.lookup("service:session");
initialize(owner) {
this.messageBus = owner.lookup("service:message-bus");
const session = owner.lookup("service:session");
// Preserve preview_theme_id=## and pp=async-flamegraph parameters across pages
const params = new URLSearchParams(window.location.search);

View File

@ -2,11 +2,10 @@ import I18n from "I18n";
import bootbox from "bootbox";
export default {
name: "localization",
after: "inject-objects",
isVerboseLocalizationEnabled(container) {
const siteSettings = container.lookup("service:site-settings");
isVerboseLocalizationEnabled(owner) {
const siteSettings = owner.lookup("service:site-settings");
if (siteSettings.verbose_localization) {
return true;
}
@ -18,8 +17,8 @@ export default {
}
},
initialize(container) {
if (this.isVerboseLocalizationEnabled(container)) {
initialize(owner) {
if (this.isVerboseLocalizationEnabled(owner)) {
I18n.enableVerboseLocalization();
}

View File

@ -6,13 +6,12 @@ let _showingLogout = false;
// Subscribe to "logout" change events via the Message Bus
export default {
name: "logout",
after: "message-bus",
initialize(container) {
this.messageBus = container.lookup("service:message-bus");
this.dialog = container.lookup("service:dialog");
this.currentUser = container.lookup("service:current-user");
initialize(owner) {
this.messageBus = owner.lookup("service:message-bus");
this.dialog = owner.lookup("service:dialog");
this.currentUser = owner.lookup("service:current-user");
if (this.currentUser) {
this.messageBus.subscribe(

View File

@ -3,18 +3,17 @@ import Singleton from "discourse/mixins/singleton";
let initializedOnce = false;
export default {
name: "logs-notice",
after: "message-bus",
initialize(container) {
initialize(owner) {
if (initializedOnce) {
return;
}
const siteSettings = container.lookup("service:site-settings");
const messageBus = container.lookup("service:message-bus");
const keyValueStore = container.lookup("service:key-value-store");
const currentUser = container.lookup("service:current-user");
const siteSettings = owner.lookup("service:site-settings");
const messageBus = owner.lookup("service:message-bus");
const keyValueStore = owner.lookup("service:key-value-store");
const currentUser = owner.lookup("service:current-user");
LogsNotice.reopenClass(Singleton, {
createCurrent() {
return this.create({

View File

@ -25,22 +25,19 @@ function ajax(opts, messageBusConnectivity, appState) {
}
export default {
name: "message-bus",
after: "inject-objects",
initialize(container) {
initialize(owner) {
// We don't use the message bus in testing
if (isTesting()) {
return;
}
const messageBus = container.lookup("service:message-bus"),
user = container.lookup("service:current-user"),
siteSettings = container.lookup("service:site-settings"),
appState = container.lookup("service:app-state"),
messageBusConnectivity = container.lookup(
"service:message-bus-connectivity"
);
const messageBus = owner.lookup("service:message-bus"),
user = owner.lookup("service:current-user"),
siteSettings = owner.lookup("service:site-settings"),
appState = owner.lookup("service:app-state"),
messageBusConnectivity = owner.lookup("service:message-bus-connectivity");
messageBus.alwaysLongPoll = !isProduction();
messageBus.shouldLongPollCallback = () =>

View File

@ -1,12 +1,11 @@
import { bind } from "discourse-common/utils/decorators";
export default {
name: "mobile-keyboard",
after: "mobile",
initialize(container) {
const site = container.lookup("service:site");
this.capabilities = container.lookup("service:capabilities");
initialize(owner) {
const site = owner.lookup("service:site");
this.capabilities = owner.lookup("service:capabilities");
if (!this.capabilities.isIpadOS && !site.mobileView) {
return;

View File

@ -3,12 +3,11 @@ import { setResolverOption } from "discourse-common/resolver";
// Initializes the `Mobile` helper object.
export default {
name: "mobile",
after: "inject-objects",
initialize(container) {
initialize(owner) {
Mobile.init();
const site = container.lookup("service:site");
const site = owner.lookup("service:site");
site.set("mobileView", Mobile.mobileView);
site.set("desktopView", !Mobile.mobileView);

View File

@ -1,5 +1,4 @@
export default {
name: "moment",
after: "message-bus",
initialize() {

View File

@ -1,19 +1,17 @@
import NarrowDesktop from "discourse/lib/narrow-desktop";
export default {
name: "narrow-desktop",
initialize(container) {
initialize(owner) {
NarrowDesktop.init();
let site;
if (!container.isDestroyed) {
site = container.lookup("service:site");
if (!owner.isDestroyed) {
site = owner.lookup("service:site");
site.set("narrowDesktopView", NarrowDesktop.narrowDesktopView);
}
if ("ResizeObserver" in window) {
this._resizeObserver = new ResizeObserver((entries) => {
if (container.isDestroyed) {
if (owner.isDestroyed) {
return;
}
for (let entry of entries) {
@ -22,7 +20,7 @@ export default {
entry.contentRect.width
);
if (oldNarrowDesktopView !== newNarrowDesktopView) {
const applicationController = container.lookup(
const applicationController = owner.lookup(
"controller:application"
);
site.set("narrowDesktopView", newNarrowDesktopView);

View File

@ -38,8 +38,6 @@ function _cleanUp() {
}
export default {
name: "onebox-decorators",
initialize() {
withPluginApi("0.8.42", (api) => {
api.decorateCookedElement(

View File

@ -1,9 +1,7 @@
import { getAbsoluteURL } from "discourse-common/lib/get-url";
export default {
name: "opengraph-tag-updater",
initialize(container) {
initialize(owner) {
// workaround for Safari on iOS 14.3
// seems it has started using opengraph tags when sharing
const ogTitle = document.querySelector("meta[property='og:title']");
@ -17,7 +15,7 @@ export default {
return;
}
const appEvents = container.lookup("service:app-events");
const appEvents = owner.lookup("service:app-events");
appEvents.on("page:changed", ({ title, url }) => {
ogTitle.setAttribute("content", title);
ogUrl.setAttribute("content", getAbsoluteURL(url));

View File

@ -6,16 +6,15 @@ import {
import { viewTrackingRequired } from "discourse/lib/ajax";
export default {
name: "page-tracking",
after: "inject-objects",
initialize(container) {
initialize(owner) {
// Tell our AJAX system to track a page transition
const router = container.lookup("router:main");
const router = owner.lookup("router:main");
router.on("routeWillChange", viewTrackingRequired);
let appEvents = container.lookup("service:app-events");
let documentTitle = container.lookup("service:document-title");
let appEvents = owner.lookup("service:app-events");
let documentTitle = owner.lookup("service:document-title");
startPageTracking(router, appEvents, documentTitle);

View File

@ -1,7 +1,6 @@
import discourseTemplateMap from "discourse-common/lib/discourse-template-map";
export default {
name: "populate-template-map",
initialize() {
discourseTemplateMap.setModuleNames(Object.keys(requirejs.entries));
},

View File

@ -12,12 +12,11 @@ import { create } from "virtual-dom";
import showModal from "discourse/lib/show-modal";
export default {
name: "post-decorations",
initialize(container) {
initialize(owner) {
withPluginApi("0.1", (api) => {
const siteSettings = container.lookup("service:site-settings");
const session = container.lookup("service:session");
const site = container.lookup("service:site");
const siteSettings = owner.lookup("service:site-settings");
const session = owner.lookup("service:session");
const site = owner.lookup("service:site");
api.decorateCookedElement(
(elem) => {
return highlightSyntax(elem, siteSettings, session);
@ -78,7 +77,7 @@ export default {
{ id: "discourse-audio" }
);
const caps = container.lookup("service:capabilities");
const caps = owner.lookup("service:capabilities");
if (caps.isSafari || caps.isIOS) {
api.decorateCookedElement(
(elem) => {

View File

@ -2,12 +2,11 @@ import { bind } from "discourse-common/utils/decorators";
// Subscribe to "read-only" status change events via the Message Bus
export default {
name: "read-only",
after: "message-bus",
initialize(container) {
this.messageBus = container.lookup("service:message-bus");
this.site = container.lookup("service:site");
initialize(owner) {
this.messageBus = owner.lookup("service:message-bus");
this.site = owner.lookup("service:site");
this.messageBus.subscribe("/site/read-only", this.onMessage);
},

View File

@ -3,13 +3,12 @@ import CategoryHashtagType from "discourse/lib/hashtag-types/category";
import TagHashtagType from "discourse/lib/hashtag-types/tag";
export default {
name: "register-hashtag-types",
before: "hashtag-css-generator",
initialize(container) {
initialize(owner) {
withPluginApi("0.8.7", (api) => {
api.registerHashtagType("category", new CategoryHashtagType(container));
api.registerHashtagType("tag", new TagHashtagType(container));
api.registerHashtagType("category", new CategoryHashtagType(owner));
api.registerHashtagType("tag", new TagHashtagType(owner));
});
},
};

View File

@ -3,11 +3,9 @@ import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin"
import { Promise } from "rsvp";
export default {
name: "register-media-optimization-upload-processor",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
const capabilities = container.lookup("service:capabilities");
initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
const capabilities = owner.lookup("service:capabilities");
if (siteSettings.composer_media_optimization_image_enabled) {
// NOTE: There are various performance issues with the Canvas
@ -32,11 +30,11 @@ export default {
({ isMobileDevice }) => {
return {
optimizeFn: (data, opts) => {
if (container.isDestroyed || container.isDestroying) {
if (owner.isDestroyed || owner.isDestroying) {
return Promise.resolve();
}
return container
return owner
.lookup("service:media-optimization-worker")
.optimizeImage(data, opts);
},

View File

@ -0,0 +1,8 @@
import { registerServiceWorker } from "discourse/lib/register-service-worker";
export default {
initialize(owner) {
let { serviceWorkerURL } = owner.lookup("service:session");
registerServiceWorker(serviceWorkerURL);
},
};

View File

@ -2,8 +2,6 @@ import { updateRelativeAge } from "discourse/lib/formatter";
// Updates the relative ages of dates on the screen.
export default {
name: "relative-ages",
initialize() {
this._interval = setInterval(function () {
updateRelativeAge(document.querySelectorAll(".relative-date"));

View File

@ -2,10 +2,8 @@ import I18n from "I18n";
import Sharing from "discourse/lib/sharing";
export default {
name: "sharing-sources",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
Sharing.addSource({
id: "twitter",

View File

@ -1,9 +1,7 @@
export default {
name: "show-footer",
initialize(container) {
const router = container.lookup("router:main");
const application = container.lookup("controller:application");
initialize(owner) {
const router = owner.lookup("router:main");
const application = owner.lookup("controller:application");
// only take care of hiding the footer here
// controllers MUST take care of displaying it

View File

@ -6,15 +6,13 @@ const ONE_DAY = 24 * 60 * 60 * 1000;
const PROMPT_HIDE_DURATION = ONE_DAY;
export default {
name: "signup-cta",
initialize(container) {
const screenTrack = container.lookup("service:screen-track");
initialize(owner) {
const screenTrack = owner.lookup("service:screen-track");
const session = Session.current();
const siteSettings = container.lookup("service:site-settings");
const keyValueStore = container.lookup("service:key-value-store");
const user = container.lookup("service:current-user");
const appEvents = container.lookup("service:app-events");
const siteSettings = owner.lookup("service:site-settings");
const keyValueStore = owner.lookup("service:key-value-store");
const user = owner.lookup("service:current-user");
const appEvents = owner.lookup("service:app-events");
// Preconditions
if (user) {

View File

@ -1,9 +1,6 @@
export default {
name: "sniff-capabilities",
after: "export-application-global",
initialize(container) {
const caps = container.lookup("service:capabilities");
initialize(owner) {
const caps = owner.lookup("service:capabilities");
const html = document.documentElement;
if (caps.touch) {

View File

@ -1,11 +1,10 @@
import StickyAvatars from "discourse/lib/sticky-avatars";
export default {
name: "sticky-avatars",
after: "inject-objects",
initialize(container) {
this._stickyAvatars = StickyAvatars.init(container);
initialize(owner) {
this._stickyAvatars = StickyAvatars.init(owner);
},
teardown() {

View File

@ -1,6 +1,4 @@
export default {
name: "strip-mobile-app-url-params",
initialize() {
let queryStrings = window.location.search;

View File

@ -15,23 +15,22 @@ import Notification from "discourse/models/notification";
import { bind } from "discourse-common/utils/decorators";
export default {
name: "subscribe-user-notifications",
after: "message-bus",
initialize(container) {
this.currentUser = container.lookup("service:current-user");
initialize(owner) {
this.currentUser = owner.lookup("service:current-user");
if (!this.currentUser) {
return;
}
this.messageBus = container.lookup("service:message-bus");
this.store = container.lookup("service:store");
this.messageBus = container.lookup("service:message-bus");
this.appEvents = container.lookup("service:app-events");
this.siteSettings = container.lookup("service:site-settings");
this.site = container.lookup("service:site");
this.router = container.lookup("router:main");
this.messageBus = owner.lookup("service:message-bus");
this.store = owner.lookup("service:store");
this.messageBus = owner.lookup("service:message-bus");
this.appEvents = owner.lookup("service:app-events");
this.siteSettings = owner.lookup("service:site-settings");
this.site = owner.lookup("service:site");
this.router = owner.lookup("router:main");
this.reviewableCountsChannel = `/reviewable_counts/${this.currentUser.id}`;

View File

@ -1,11 +1,8 @@
import { loadSprites } from "discourse/lib/svg-sprite-loader";
export default {
name: "svg-sprite-fontawesome",
after: "export-application-global",
initialize(container) {
const session = container.lookup("service:session");
initialize(owner) {
const session = owner.lookup("service:session");
if (session.svgSpritePath) {
loadSprites(session.svgSpritePath, "fontawesome");

View File

@ -13,15 +13,12 @@ import Ember from "ember";
const showingErrors = new Set();
export default {
name: "theme-errors-handler",
after: "export-application-global",
initialize(container) {
initialize(owner) {
if (isTesting()) {
return;
}
this.currentUser = container.lookup("service:current-user");
this.currentUser = owner.lookup("service:current-user");
getAndClearUnhandledThemeErrors().forEach((e) => this.reportThemeError(e));

View File

@ -13,8 +13,6 @@ const FLAG_PRIORITY = 700;
const DEFER_PRIORITY = 500;
export default {
name: "topic-footer-buttons",
initialize() {
registerTopicFooterButton({
id: "share-and-invite",

View File

@ -3,11 +3,10 @@ import { initializeDefaultHomepage } from "discourse/lib/utilities";
import escapeRegExp from "discourse-common/utils/escape-regexp";
export default {
name: "url-redirects",
after: "inject-objects",
initialize(container) {
const currentUser = container.lookup("service:current-user");
initialize(owner) {
const currentUser = owner.lookup("service:current-user");
if (currentUser) {
const username = currentUser.get("username");
const escapedUsername = escapeRegExp(username);
@ -23,11 +22,11 @@ export default {
DiscourseURL.rewrite(/^\/groups\//, "/g/");
// Initialize default homepage
let siteSettings = container.lookup("service:site-settings");
let siteSettings = owner.lookup("service:site-settings");
initializeDefaultHomepage(siteSettings);
let defaultUserRoute = siteSettings.view_user_route || "summary";
if (!container.lookup(`route:user.${defaultUserRoute}`)) {
if (!owner.lookup(`route:user.${defaultUserRoute}`)) {
defaultUserRoute = "summary";
}

View File

@ -1,17 +1,16 @@
import { bind } from "discourse-common/utils/decorators";
export default {
name: "user-tips",
after: "message-bus",
initialize(container) {
this.currentUser = container.lookup("service:current-user");
initialize(owner) {
this.currentUser = owner.lookup("service:current-user");
if (!this.currentUser) {
return;
}
this.messageBus = container.lookup("service:message-bus");
this.site = container.lookup("service:site");
this.messageBus = owner.lookup("service:message-bus");
this.site = owner.lookup("service:site");
this.messageBus.subscribe(
`/user-tips/${this.currentUser.id}`,

View File

@ -3,11 +3,10 @@ import discourseLater from "discourse-common/lib/later";
// Send bg color to webview so iOS status bar matches site theme
export default {
name: "webview-background",
after: "inject-objects",
initialize(container) {
const caps = container.lookup("service:capabilities");
initialize(owner) {
const caps = owner.lookup("service:capabilities");
if (caps.isAppWebview) {
window
.matchMedia("(prefers-color-scheme: dark)")

View File

@ -1,8 +1,8 @@
import { setOwner } from "@ember/application";
export default class HashtagTypeBase {
constructor(container) {
setOwner(this, container);
constructor(owner) {
setOwner(this, owner);
}
get type() {

View File

@ -1,5 +1,6 @@
import { bind } from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
import { getOwner, setOwner } from "@ember/application";
import { run, throttle } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later";
import {
@ -121,7 +122,9 @@ function preventKeyboardEvent(event) {
}
export default {
init(keyTrapper, container) {
init(keyTrapper, owner) {
setOwner(this, owner);
// Sometimes the keyboard shortcut initializer is not torn down. This makes sure
// we clear any previous test state.
if (this.keyTrapper) {
@ -130,14 +133,13 @@ export default {
}
this.keyTrapper = new keyTrapper();
this.container = container;
this._stopCallback();
this.searchService = this.container.lookup("service:search");
this.appEvents = this.container.lookup("service:app-events");
this.currentUser = this.container.lookup("service:current-user");
this.siteSettings = this.container.lookup("service:site-settings");
this.site = this.container.lookup("service:site");
this.searchService = owner.lookup("service:search");
this.appEvents = owner.lookup("service:app-events");
this.currentUser = owner.lookup("service:current-user");
this.siteSettings = owner.lookup("service:site-settings");
this.site = owner.lookup("service:site");
// Disable the shortcut if private messages are disabled
if (!this.currentUser?.can_send_private_messages) {
@ -158,11 +160,10 @@ export default {
this.keyTrapper?.destroy();
this.keyTrapper = null;
this.container = null;
},
isTornDown() {
return this.keyTrapper == null || this.container == null;
return this.keyTrapper == null;
},
bindKey(key, binding = null) {
@ -298,12 +299,12 @@ export default {
const topic = this.currentTopic();
if (topic && document.querySelectorAll(".posts-wrapper").length) {
preventKeyboardEvent(event);
this.container.lookup("controller:topic").send("toggleBookmark");
getOwner(this).lookup("controller:topic").send("toggleBookmark");
}
},
logout() {
this.container.lookup("route:application").send("logout");
getOwner(this).lookup("route:application").send("logout");
},
quoteReply() {
@ -354,7 +355,7 @@ export default {
if (el) {
el.click();
} else {
const controller = this.container.lookup("controller:topic");
const controller = getOwner(this).lookup("controller:topic");
// Only the last page contains list of suggested topics.
const url = `/t/${controller.get("model.id")}/last.json`;
ajax(url).then((result) => {
@ -383,7 +384,7 @@ export default {
_jumpTo(direction) {
if (document.querySelector(".container.posts")) {
this.container.lookup("controller:topic").send(direction);
getOwner(this).lookup("controller:topic").send(direction);
}
},
@ -426,7 +427,7 @@ export default {
run(() => {
if (document.querySelector(".container.posts")) {
event.preventDefault(); // We need to stop printing the current page in Firefox
this.container.lookup("controller:topic").print();
getOwner(this).lookup("controller:topic").print();
}
});
},
@ -445,14 +446,14 @@ export default {
return;
}
this.container.lookup("service:composer").open({
getOwner(this).lookup("service:composer").open({
action: Composer.CREATE_TOPIC,
draftKey: Composer.NEW_TOPIC_KEY,
});
},
focusComposer(event) {
const composer = this.container.lookup("service:composer");
const composer = getOwner(this).lookup("service:composer");
if (event) {
event.preventDefault();
event.stopPropagation();
@ -461,14 +462,14 @@ export default {
},
fullscreenComposer() {
const composer = this.container.lookup("service:composer");
const composer = getOwner(this).lookup("service:composer");
if (composer.get("model")) {
composer.toggleFullscreen();
}
},
pinUnpinTopic() {
this.container.lookup("controller:topic").togglePinnedState();
getOwner(this).lookup("controller:topic").togglePinnedState();
},
goToPost(event) {
@ -497,7 +498,7 @@ export default {
},
showHelpModal() {
this.container
getOwner(this)
.lookup("controller:application")
.send("showKeyboardShortcutsHelp");
},
@ -531,7 +532,7 @@ export default {
sendToTopicListItemView(action, elem) {
elem = elem || document.querySelector("tr.selected.topic-list-item");
if (elem) {
const registry = this.container.lookup("-view-registry:main");
const registry = getOwner(this).lookup("-view-registry:main");
if (registry) {
const view = registry[elem.id];
view.send(action);
@ -540,7 +541,7 @@ export default {
},
currentTopic() {
const topicController = this.container.lookup("controller:topic");
const topicController = getOwner(this).lookup("controller:topic");
if (topicController) {
const topic = topicController.get("model");
if (topic) {
@ -550,7 +551,7 @@ export default {
},
isPostTextSelected() {
const topicController = this.container.lookup("controller:topic");
const topicController = getOwner(this).lookup("controller:topic");
return !!topicController?.get("quoteState")?.postId;
},
@ -565,7 +566,7 @@ export default {
}
if (selectedPostId) {
const topicController = this.container.lookup("controller:topic");
const topicController = getOwner(this).lookup("controller:topic");
const post = topicController
.get("model.postStream.posts")
.findBy("id", selectedPostId);
@ -574,7 +575,7 @@ export default {
let actionMethod = topicController.actions[action];
if (!actionMethod) {
const topicRoute = this.container.lookup("route:topic");
const topicRoute = getOwner(this).lookup("route:topic");
actionMethod = topicRoute.actions[action];
}
@ -849,7 +850,7 @@ export default {
},
_replyToPost() {
this.container.lookup("controller:topic").send("replyToPost");
getOwner(this).lookup("controller:topic").send("replyToPost");
},
_getSelectedPost() {
@ -861,7 +862,7 @@ export default {
},
deferTopic() {
this.container.lookup("controller:topic").send("deferTopic");
getOwner(this).lookup("controller:topic").send("deferTopic");
},
toggleAdminActions() {

View File

@ -1,10 +1,6 @@
import getAbsoluteURL, { isAbsoluteURL } from "discourse-common/lib/get-url";
export function registerServiceWorker(
container,
serviceWorkerURL,
registerOptions = {}
) {
export function registerServiceWorker(serviceWorkerURL, registerOptions = {}) {
if (window.isSecureContext && "serviceWorker" in navigator) {
if (serviceWorkerURL) {
navigator.serviceWorker.getRegistrations().then((registrations) => {

View File

@ -2,11 +2,12 @@ import { addWidgetCleanCallback } from "discourse/components/mount-widget";
import Site from "discourse/models/site";
import { bind } from "discourse-common/utils/decorators";
import { headerOffset } from "discourse/lib/offset-calculator";
import { getOwner, setOwner } from "@ember/application";
import { schedule } from "@ember/runloop";
export default class StickyAvatars {
static init(container) {
return new this(container).init();
static init(owner) {
return new this(owner).init();
}
stickyClass = "sticky-avatar";
@ -15,8 +16,8 @@ export default class StickyAvatars {
direction = "⬇️";
prevOffset = -1;
constructor(container) {
this.container = container;
constructor(owner) {
setOwner(this, owner);
}
init() {
@ -24,7 +25,7 @@ export default class StickyAvatars {
return;
}
const appEvents = this.container.lookup("service:app-events");
const appEvents = getOwner(this).lookup("service:app-events");
appEvents.on("topic:current-post-scrolled", this._handlePostNodes);
appEvents.on("topic:scrolled", this._handleScroll);
appEvents.on("page:topic-loaded", this._initIntersectionObserver);
@ -34,9 +35,7 @@ export default class StickyAvatars {
return this;
}
destroy() {
this.container = null;
}
destroy() {}
@bind
_handleScroll(offset) {

View File

@ -3,7 +3,7 @@ import Session from "discourse/models/session";
import Site from "discourse/models/site";
import TopicTrackingState from "discourse/models/topic-tracking-state";
import User from "discourse/models/user";
import { autoLoadModules } from "discourse/initializers/auto-load-modules";
import { autoLoadModules } from "discourse/instance-initializers/auto-load-modules";
import QUnit, { test } from "qunit";
import { setupRenderingTest as emberSetupRenderingTest } from "ember-qunit";
import { currentSettings } from "discourse/tests/helpers/site-settings";

View File

@ -1,17 +1,19 @@
import { module, test } from "qunit";
import I18n from "I18n";
import LocalizationInitializer from "discourse/initializers/localization";
import { getApplication } from "@ember/test-helpers";
import LocalizationInitializer from "discourse/instance-initializers/localization";
import { setupTest } from "ember-qunit";
module("initializer:localization", {
_locale: I18n.locale,
_translations: I18n.translations,
_extras: I18n.extras,
_compiledMFs: I18n._compiledMFs,
_overrides: I18n._overrides,
_mfOverrides: I18n._mfOverrides,
module("initializer:localization", function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
this._locale = I18n.locale;
this._translations = I18n.translations;
this._extras = I18n.extras;
this._compiledMFs = I18n._compiledMFs;
this._overrides = I18n._overrides;
this._mfOverrides = I18n._mfOverrides;
beforeEach() {
I18n.locale = "fr";
I18n.translations = {
@ -59,19 +61,18 @@ module("initializer:localization", {
},
},
};
},
});
afterEach() {
hooks.afterEach(function () {
I18n.locale = this._locale;
I18n.translations = this._translations;
I18n.extras = this._extras;
I18n._compiledMFs = this._compiledMFs;
I18n._overrides = this._overrides;
I18n._mfOverrides = this._mfOverrides;
},
});
});
test("translation overrides", function (assert) {
test("translation overrides", function (assert) {
I18n._overrides = {
fr: {
"js.composer.both_languages1": "composer.both_languages1 (FR override)",
@ -82,7 +83,7 @@ test("translation overrides", function (assert) {
"js.composer.only_english1": "composer.only_english1 (EN override)",
},
};
LocalizationInitializer.initialize(getApplication());
LocalizationInitializer.initialize(this.owner);
assert.strictEqual(
I18n.t("composer.both_languages1"),
@ -107,9 +108,9 @@ test("translation overrides", function (assert) {
"composer.both_languages2 (FR)",
"prefers translation in current locale over override in fallback locale"
);
});
});
test("translation overrides (admin_js)", function (assert) {
test("translation overrides (admin_js)", function (assert) {
I18n._overrides = {
fr: {
"admin_js.admin.api.both_languages1":
@ -125,7 +126,7 @@ test("translation overrides (admin_js)", function (assert) {
"admin.api.only_english1 (EN override)",
},
};
LocalizationInitializer.initialize(getApplication());
LocalizationInitializer.initialize(this.owner);
assert.strictEqual(
I18n.t("admin.api.both_languages1"),
@ -156,34 +157,35 @@ test("translation overrides (admin_js)", function (assert) {
"type_to_filter (FR override)",
"correctly changes the translation key by removing `admin_js`"
);
});
});
test("translation overrides for MessageFormat strings", function (assert) {
test("translation overrides for MessageFormat strings", function (assert) {
I18n._mfOverrides = {
"js.user.messages.some_key_MF": () =>
"user.messages.some_key_MF (FR override)",
};
LocalizationInitializer.initialize(getApplication());
LocalizationInitializer.initialize(this.owner);
assert.strictEqual(
I18n.messageFormat("user.messages.some_key_MF", {}),
"user.messages.some_key_MF (FR override)",
"overrides existing MessageFormat string"
);
});
});
test("skip translation override if parent node is not an object", function (assert) {
test("skip translation override if parent node is not an object", function (assert) {
I18n._overrides = {
fr: {
"js.composer.both_languages1.foo":
"composer.both_languages1.foo (FR override)",
},
};
LocalizationInitializer.initialize(getApplication());
LocalizationInitializer.initialize(this.owner);
assert.strictEqual(
I18n.t("composer.both_languages1.foo"),
"[fr.composer.both_languages1.foo]"
);
});
});

View File

@ -1,4 +1,4 @@
import { decorateGithubOneboxBody } from "discourse/initializers/onebox-decorators";
import { decorateGithubOneboxBody } from "discourse/instance-initializers/onebox-decorators";
import { replaceHashtagIconPlaceholder } from "discourse/lib/hashtag-autocomplete";
import { withPluginApi } from "discourse/lib/plugin-api";
import highlightSyntax from "discourse/lib/highlight-syntax";