Migration from v6 to v7
Breaking changes
Minimum requirements
The following 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 whenvalue(the 1st positional argument) isundefinedornull. (Inv6, the methods had relied onisEmpty()from@ember/utils, potentially allowing more values forvalueto return an empty string.)The
format*()methods don't make use ofoptions.allowEmpty. Remove the option at invocation sites.
{{! Display an empty string while @message is loaded, i.e. @message.timestamp is undefined or null }}
- {{format-date @message.timestamp allowEmpty=true}}
+ {{format-date @message.timestamp}}The
t()method no longer usesassert()to check thatkey(the 1st positional argument) isstring.The
t()method doesn't make use ofoptions.default. Remove the option at invocation sites. In the backing class, use early exit(s) and theintlservice'sexists()to make the logic clear.
- {{t
- (concat "components.message.status-" @message.status)
- default="components.message.status-unknown"
- }}
+ {{this.messageStatus}}import { type Registry as Services, service } from '@ember/service';
import Component from '@glimmer/component';
export default class MessageComponent extends Component {
@service declare intl: Services['intl'];
+ get messageStatus(): string {
+ const { status } = this.args.message;
+
+ if (this.intl.exists(`components.message.status-${status}`)) {
+ return this.intl.t(`components.message.status-${status}`);
+ }
+
+ return this.intl.t('components.message.status-unknown');
+ }
}- The
t()method doesn't make use ofoptions.resilient. Remove the option at invocation sites. In the backing class, use early exit(s) and theintlservice'sexists()to make the logic clear.
- {{t
- (concat "components.message.status-" @message.status)
- resilient=true
- }}
+ {{this.messageStatus}}import { type Registry as Services, service } from '@ember/service';
import Component from '@glimmer/component';
export default class MessageComponent extends Component {
@service declare intl: Services['intl'];
+ get messageStatus(): string | undefined {
+ const { status } = this.args.message;
+
+ if (this.intl.exists(`components.message.status-${status}`)) {
+ return this.intl.t(`components.message.status-${status}`);
+ }
+ }
}- The
localegetter and setter have been removed. UsesetLocale()to set the locale.
import 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
lookup()method, which iterated through thelocalesgetter when a locale hadn't been 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:
- const translation = this.intl.lookup('hello.message');
+ const translation = this.intl.getTranslation('hello.message', this.intl.primaryLocale);If you need to replicate how lookup() iterated through locales:
let translation?: string;
for (const locale of this.intl.locales) {
const candidate = this.intl.getTranslation('hello.message', locale);
if (candidate) {
translation = candidate;
break;
}
}The
onIntlError()method has been removed. SeesetOnFormatjsError()below.The
translationsFor()method has been removed. If you need to check the existence of translations, useexists()instead.
- if (this.intl.translationsFor('de-de')) {
+ if (this.intl.exists('some-key', 'de-de')) {
return;
}
// ... (e.g. load translations)The service provides 2 new methods to help you configure ember-intl. For more information, see the documentation for intl service.
setOnFormatjsError()lets you define what to do when@formatjs/intlerrors.setOnMissingTranslation()lets you define what to display when a translation is missing. The fileapp/utils/intl/missing-message.jshas no effect and can be removed.
Minimized API of helpers
The {{format-*}} and {{t}} helpers are simply a shortcut for calling the corresponding intl service's method in a template.
- The
intlservice'sformat*()methods return an empty string whenvalue(the 1st positional argument) isundefinedornull. This means, the{{format-*}}helpers also return an empty string.options.allowEmptyhas no effect and can be removed. - The
{{format-*}}helpers disallow passing options as the 2nd positional argument. #1633 had accidentally allowed this (a bug), because the helpers had inherited a base class at the time. The{{t}}helper continues to allow passing data as named or positional arguments.
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. We should consider the added code as private APIs, because they weren't mentioned in the documentation site and release notes as features.
By removing unnecessary code, we can reduce the package size and maintenance cost.
- The test helpers no longer use
assert()to check that you have calledsetupTest(),setupRenderingTest(), orsetupApplicationTest(). setupIntl()no longer setsthis.intl. So, by default,this.intlisundefinedin tests. You can useowner.lookup('service:intl')if you need theintlservice.setupIntl()doesn't allow passingoptions, a 4th argument that overrides implementation (missing message and formatters) and may cause tests to pass 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 a single translation file per locale (always *.yaml), not the more realistic case of multiple files per locale (possibly with nested folders). 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 not necessary in Octane, and prevent us from mainintaing and updating ember-intl more easily.
Check your codebase for import statements with ember-intl/macros, then rewrite code using getters. This may require you to glimmerize a classic component.
Before:
import Component from '@ember/component';
import { intl, raw, t } from 'ember-intl/macros';
export default class MyComponent 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 outputForIntl: string;
@t('hello.message', {
name: 'name',
})
declare outputForT: string;
@t('hello.message', {
name: raw('name'),
})
declare outputForTWithRaw: string;
}After:
import { type Registry as Services, service } from '@ember/service';
import Component from '@glimmer/component';
export default class MyComponent extends Component {
@service declare intl: Services['intl'];
get outputForIntl(): string {
return this.intl.formatList(this.args.fruits);
}
get outputForT(): string {
return this.intl.t('hello.message', {
name: this.args.name,
});
}
get outputForTWithRaw(): 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 might have seen in ember-intl@v6.
This change may be breaking if you have a computed property that lists intl.locale or intl.primaryLocale as a dependent key. You may try to glimmerize the classic component (recommended) or move the logic inside the computed property "outside."
Before:
import Component from '@ember/component';
import { computed } from '@ember/object';
import { type Registry as Services, service } from '@ember/service';
export default class MyComponent 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.');
}
}
}
}After:
import { type Registry as Services, service } from '@ember/service';
import Component from '@glimmer/component';
export default class MyComponent 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
Before v7, ember-intl allowed you to write setupIntl(hooks). Your tests would somehow pass, even though you didn't specify under which locale the tests make sense.
By favoring convenience and assuming that most apps target USA, we also created problems that became visible in v6:
'en-us'is always present in theintlservice'slocales, even when the app doesn't support theen-uslocale.setupIntl()needs to support 4 variations, increasing complexity and maintenance cost.
To solve these issues and encourage writing code that is explicit, 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, you can 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!')
});
});