Migration from v6 to v7
Breaking changes
Minimum requirements
Projects with these versions are supported when issues arise.
- Ember 3.28 and above1
- Node 18 and above
- TypeScript 5 and above
1. Ember 4.4 and 4.8 are no longer checked in CI.
Minimized API of the intl service
The intl service provides just the essentials so that we can reduce the package size and maintain the project easily. The service has been rewritten to provide native types. We highly recommend that you use TypeScript and Glint.
The
format*methods return an empty string when the input value isundefinedornull. (Inv6, the methods had relied onisEmptyfrom@ember/utilsand allowed more values to return an empty string.)The
format*methods don't use the optionallowEmpty. Remove it from the invocation sites.diff{{! Display an empty string while @message is loaded }} - {{format-date @message.timestamp allowEmpty=true}} + {{format-date @message.timestamp}}diffimport templateOnlyComponent from '@ember/component/template-only'; interface MessageSignature { Args: { message?: { status: 'read' | 'received' | 'sent'; timestamp: Date; }; }; } const Message = templateOnlyComponent<MessageSignature>(); export default Message;The
tmethod no longer usesassertfrom@ember/debugto check thatkey(the 1st positional argument) is astring.The
tmethod doesn't use the optiondefault. Remove it from the invocation sites. In the backing class, use early exit(s) and theintlservice'sexistsinstead.diff- {{t - (concat "message.status-" @message.status) - default="message.status-unknown" - }} + {{this.messageStatus}}diffimport { type Registry as Services, service } from '@ember/service'; import Component from '@glimmer/component'; interface MessageSignature { Args: { message: { status: 'read' | 'received' | 'sent'; timestamp: Date; }; }; } export default class Message extends Component<MessageSignature> { + @service declare intl: Services['intl']; + + get messageStatus(): string { + const { message } = this.args; + + if (this.intl.exists(`message.status-${message.status}`)) { + return this.intl.t(`message.status-${message.status}`); + } + + return this.intl.t('message.status-unknown'); + } }The
tmethod doesn't use the optionresilient. Remove it from the invocation sites. In the backing class, use early exit(s) and theintlservice'sexistsinstead.diff- {{t - (concat "message.status-" @message.status) - resilient=true - }} + {{this.messageStatus}}diffimport { type Registry as Services, service } from '@ember/service'; import Component from '@glimmer/component'; interface MessageSignature { Args: { message: { status: 'read' | 'received' | 'sent'; timestamp: Date; }; }; } export default class Message extends Component<MessageSignature> { + @service declare intl: Services['intl']; + + get messageStatus(): string | undefined { + const { message } = this.args; + + if (this.intl.exists(`message.status-${message.status}`)) { + return this.intl.t(`message.status-${message.status}`); + } + } }The
localegetter and setter have been removed. UsesetLocaleto set the locale.diffimport Route from '@ember/routing/route'; import { type Registry as Services, service } from '@ember/service'; export default class ApplicationRoute extends Route { @service declare intl: Services['intl']; beforeModel(): void { this.setupIntl(); } private setupIntl(): void { - this.intl.locale = ['en-us']; + this.intl.setLocale(['en-us']); } }The
lookupmethod, which iterates through thelocalesgetter when a locale wasn't specified, has been removed.v7provides a simpler method calledgetTranslation, one that requires you to specify the locale.If you need the translation message for the primary locale:
diff- const translation = this.intl.lookup('hello.message'); + const translation = this.intl.getTranslation('hello.message', this.intl.primaryLocale);If you need to replicate how
lookupiterated throughlocales:tslet translation?: string; for (const locale of this.intl.locales) { const candidate = this.intl.getTranslation('hello.message', locale); if (candidate) { translation = candidate; break; } }The
onIntlErrormethod has been removed. UsesetOnFormatjsErrorinstead.The
translationsFormethod has been removed. If you need to check that a translation exists, useexistsinstead.diff- if (!this.intl.translationsFor('de-de')) { + if (!this.intl.exists('some-special-key', 'de-de')) { // Load translations }
The service provides 2 new methods to help you configure ember-intl.
setOnFormatjsErrorlets you define what to do when@formatjs/intlerrors.setOnMissingTranslationlets you define what to display when a translation is missing. The fileapp/utils/intl/missing-message.jshas no effect and should be removed.
Minimized API of helpers
The {{format-*}} and {{t}} helpers are simply a shortcut for calling an intl service's method in a template.
Just like the service, the
{{format-*}}helpers return an empty string when the input value isundefinedornull. The optionallowEmptycan be removed.The
{{format-*}}helpers no longer accept options via the 2nd positional argument. #1633 had accidentally allowed this (a bug), because the helpers had inherited a base class at the time. You can continue to pass data to the{{t}}helper using a named argument(s) or the 2nd positional argument.
Minimized API of test helpers
#1432, which had been released in v5.5.0-beta.7, increased the API in addon-test-support to help write tests for the ember-intl repo. This API can be considered private, because it wasn't mentioned in the documentation site and release notes as a feature.
By removing unnecessary code, we can reduce the package size and maintenance cost.
The test helpers no longer use
assertfrom@ember/debugto check that you have calledsetupTest,setupRenderingTest, orsetupApplicationTest.setupIntlno longer stores theintlservice onthis. In other words,this.intlisundefinedby default. You can usethis.owner.lookup('service:intl')if you need theintlservice in a test.setupIntlno longer allowsoptions, a 4th positional argument that overridesember-intl's implementation (missing message and formatters) and causes tests to pass or fail under wrong assumptions.ember-intlno longer provides the custom typesTestContextandIntlTestContext. Always importTestContextfrom@ember/test-helpers.
Removed ember generate translation
This command has been removed, because the blueprints only addressed the simple case of 1 translation file per locale (no support for JSON). In reality, a production app may have multiple files per locale. The blueprints also required installing a dependency.
How translation files are created will be left up to the end-developers.
Removed @intl and @t macros
The macros are a remnant of classic components and ember-i18n. They are unnecessary in Octane and prevent us from mainintaing ember-intl easily.
Check for files with the import path ember-intl/macros, then rewrite them to use native getters. This may require you to glimmerize a classic component.
import Component from '@ember/component';
import { intl, raw, t } from 'ember-intl/macros';
export default class Example extends Component {
@intl('fruits', function (_intl: Services['intl']) {
// @ts-expect-error: 'this' implicitly has type 'any' because it does not have a type annotation.
return _intl.formatList(this.fruits);
})
declare exampleForIntl: string;
@t('hello.message', {
name: 'name',
})
declare exampleForT: string;
@t('hello.message', {
name: raw('name'),
})
declare exampleForTWithRaw: string;
}import { type Registry as Services, service } from '@ember/service';
import Component from '@glimmer/component';
export default class Example extends Component {
@service declare intl: Services['intl'];
get exampleForIntl(): string {
return this.intl.formatList(this.args.fruits);
}
get exampleForT(): string {
return this.intl.t('hello.message', {
name: this.args.name,
});
}
get exampleForTWithRaw(): string {
return this.intl.t('hello.message', {
name: 'name',
});
}
}Removed the use of @dependentKeyCompat decorator
The @dependentKeyCompat decorator was used to support the @intl and @t macros. Now that these macros are gone, so is @dependentKeyCompat.
This change should fix the error You attempted to update _locale [...] in the same computation. that you would have seen in v6.
This change may be breaking if you have a computed property that lists intl.locale or intl.primaryLocale as a dependent key. Consider removing the computed property (recommended) or moving the business logic in the computed property to a utility.
import Component from '@ember/component';
import { computed } from '@ember/object';
import { type Registry as Services, service } from '@ember/service';
export default class Example extends Component {
@service declare intl: Services['intl'];
@computed('intl.{locale,primaryLocale}')
get fruits(): string[] {
switch (this.intl.primaryLocale) {
case 'de-de': {
return ['Äpfel', 'Bananen', 'Orangen'];
}
case 'en-us': {
return ['Apples', 'Bananas', 'Oranges'];
}
default: {
throw new Error('Locale must be de-de or en-us.');
}
}
}
}import { type Registry as Services, service } from '@ember/service';
import Component from '@glimmer/component';
export default class Example extends Component {
@service declare intl: Services['intl'];
get fruits(): string[] {
switch (this.intl.primaryLocale) {
case 'de-de': {
return ['Äpfel', 'Bananen', 'Orangen'];
}
case 'en-us': {
return ['Apples', 'Bananas', 'Oranges'];
}
default: {
throw new Error('Locale must be de-de or en-us.');
}
}
}
}Required locale in test helpers
In v6, ember-intl let you write setupIntl(hooks). Your tests could somehow pass, though you didn't specify under which locale the tests make sense.
By favoring convenience and assuming that most apps target USA, we had also created problems that became visible in v6:
'en-us'is always present in theintlservice'slocales, even when the app doesn't have translations foren-us.setupIntlneeds to support 4 variations, increasing complexity and maintenance cost.
To solve these issues and encourage writing explicit code, setupIntl now requires you to specify the locale.
To migrate code, use find-and-replace-all in your text editor.
module('Integration | Component | hello', function (hooks) {
setupRenderingTest(hooks);
- setupIntl(hooks);
+ setupIntl(hooks, 'en-us');
test('it renders', async function (assert) {
await render(hbs`
<Hello @name="Zoey" />
`);
assert.dom('[data-test-message]').hasText('Hello, Zoey!');
});
});TIP
If you want to test multiple locales, use nested modules.
module('Integration | Component | hello', function (hooks) {
setupRenderingTest(hooks);
module('de-de', function (nestedHooks) {
setupIntl(nestedHooks, 'de-de');
test('it renders', async function (assert) {
await render(hbs`
<Hello @name="Zoey" />
`);
assert.dom('[data-test-message]').hasText('Hallo, Zoey!');
});
});
module('en-us', function (nestedHooks) {
setupIntl(nestedHooks, 'en-us');
test('it renders', async function (assert) {
await render(hbs`
<Hello @name="Zoey" />
`);
assert.dom('[data-test-message]').hasText('Hello, Zoey!');
});
});
});Similarly, addTranslations now requires the locale.
module('Integration | Component | lazy-hello', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');
test('Lazily loaded translations', async function (assert) {
await render(hbs`
<LazyHello @name="Zoey" />
`);
assert
.dom('[data-test-message]')
.hasText('t:lazy-hello.message:("name":"Zoey")');
- await addTranslations({
+ await addTranslations('en-us', {
'lazy-hello': {
message: 'Hello, {name}!',
},
});
assert.dom('[data-test-message]').hasText('Hello, Zoey!')
});
});