Internationalization for Ember projects
[!IMPORTANT] If your
ember-intl
is currently between versions6.3.0
and6.5.2
, please update it to6.5.3
or higher.
Thanks to @johanrd for investigating the issue and providing the fix quickly.
Updating ember-cli-typescript
will help remove warnings that ember-intl
(along with other addons) might have produced.
WARNING: ember-cli-typescript requires ember-cli-babel ^7.17.0, but you have version 8.0.0 installed; your TypeScript files may not be transpiled correctly
[!IMPORTANT] The blueprint files, which make
ember g translation <locale>
currently possible, will be removed inv7.0.0
. You can manually create the translation file (more accurately).
In the docs
folder, I created 3 additional projects so that there's a living documentation (tested in CI) of how apps, v1 addons, and v2 addons can provide translations.
I also updated the documentation site. You will find new content in:
Getting Started > Overview
Getting Started > Quickstart (Apps)
Getting Started > Quickstart (Addons)
[!WARNING] To lower the maintenance cost, I removed code that should be unused, and refactored code that should be private.
While existing tests continued to pass, it's possible that one of the refactors is a soft breaking change for your project (e.g. because you overwrote a method from the
intl
service). To be safe, I went with a minor release.If you find a regression, please open an issue and provide me context, so that we can see if a revert is needed.
Thanks to @bertdeblock for fixing a URL typo in the blueprints.
The dependencies of ember-intl
(in particular, @types/ember__runloop
and @types/ember__template
) have been updated to the latest version. This may fix the error messages shown below:
[lint:types] node_modules/ember-intl/-private/formatters/-base.d.ts:1:33 - error TS2307: Cannot find module '@ember/template/-private/handlebars' or its corresponding type declarations.
[lint:types]
[lint:types] 1 import type { SafeString } from '@ember/template/-private/handlebars';
[lint:types] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[lint:types]
[lint:types] node_modules/ember-intl/-private/formatters/format-message.d.ts:1:33 - error TS2307: Cannot find module '@ember/template/-private/handlebars' or its corresponding type declarations.
[lint:types]
[lint:types] 1 import type { SafeString } from '@ember/template/-private/handlebars';
[lint:types] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[lint:types]
[lint:types] node_modules/ember-intl/services/intl.d.ts:1:36 - error TS2307: Cannot find module '@ember/runloop/types' or its corresponding type declarations.
[lint:types]
[lint:types] 1 import type { EmberRunTimer } from '@ember/runloop/types';
[lint:types] ~~~~~~~~~~~~~~~~~~~~~~
[lint:types]
[lint:types] node_modules/ember-intl/services/intl.d.ts:3:33 - error TS2307: Cannot find module '@ember/template/-private/handlebars' or its corresponding type declarations.
[lint:types]
[lint:types] 3 import type { SafeString } from '@ember/template/-private/handlebars';
[lint:types] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Thanks to @jelhan.
setLocale()
and addTranslation()
to call settled()
v7.0.0
)<template>
-tag support (allowed importing helpers from index
)Before v6.4.0
, calling setLocale()
and addTranslations()
wouldn't have an effect on the application in tests. You might have had to call await settled()
or use something from @ember/runloop
to trigger an update.
Now, the test helpers handle the update and mean the following in tests:
setLocale()
- update the locale as if the user had somehow changed their preferred languageaddTranslations()
- update the translations as if you had somehow added them (e.g. via lazy loading)You will want to search your test files for setLocale(
, addTranslations(
, and ember-intl/test-support
, then migrate code as follows:
setLocale()
- import { render, settled } from '@ember/test-helpers';
+ import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setLocale, setupIntl } from 'ember-intl/test-support';
import { setupRenderingTest } from 'ember-qunit';
import { module, test } from 'qunit';
module('Integration | Component | hello', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
test('it renders', async function (assert) {
await render(hbs`
<Hello @name="Zoey" />
`);
assert.dom('[data-test-message]').hasText('Hello, Zoey!');
- setLocale('de-de');
- await settled();
+ await setLocale('de-de');
assert.dom('[data-test-message]').hasText('Hallo, Zoey!');
});
});
addTranslations()
- import { render, settled } from '@ember/test-helpers';
+ import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { addTranslations, setupIntl, t } from 'ember-intl/test-support';
import { setupRenderingTest } from 'ember-qunit';
import { module, test } from 'qunit';
module('Integration | Component | lazy-hello', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
test('it renders', async function (assert) {
await render(hbs`
<LazyHello @name="Zoey" />
`);
assert
.dom('[data-test-message]')
.doesNotIncludeText(
'Hello, Zoey!',
'Before translations are loaded, we cannot write assertions against a string.',
)
.hasText(
't:lazy-hello.message:("name":"Zoey")',
'Before translations are loaded, we should see the missing-message string.',
)
.hasText(
t('lazy-hello.message', { name: 'Zoey' }),
'Before translations are loaded, we can write assertions against the test helper t().',
);
- addTranslations({
- 'lazy-hello': {
- message: 'Hello, {name}!',
- },
- });
- await settled();
+ await addTranslations({
+ 'lazy-hello': {
+ message: 'Hello, {name}!',
+ },
+ });
assert
.dom('[data-test-message]')
.hasText(
'Hello, Zoey!',
'After translations are loaded, we can write assertions against a string.',
)
.doesNotIncludeText(
't:lazy-hello.message:("name":"Zoey")',
'After translations are loaded, we should not see the missing-message string.',
)
.hasText(
t('lazy-hello.message', { name: 'Zoey' }),
'After translations are loaded, we can write assertions against the test helper t().',
);
});
});
[!IMPORTANT] The macros, which are a remnant of classic components and
ember-i18n
, will be removed inv7.0.0
. I updated the documentation for macros to warn developers.
[!IMPORTANT] You can no longer import the macros from the
index
file, because thet()
macro would conflict with thet()
helper.Until the macros are removed in
v7.0.0
, you can name-import them from'ember-intl/macros'
or refer to the full path (e.g.import t from 'ember-intl/macros/t';
).
In classic and Glimmer components, inject the intl
service to access its methods. If you want to create a value that depends on other things, you can use the @computed
decorator (i.e. create a computed property) in classic and the native getter in Glimmer components. Alternatively, you can use ember-intl
's helpers in the template.
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging, ember/no-classic-classes, ember/no-classic-components, ember/no-computed-properties-in-native-classes, import/export */
import Component from '@ember/component';
import type { Registry as Services } from '@ember/service';
import { intl, raw, t } from 'ember-intl/macros';
export default class MyComponent extends Component {
tagName = '';
@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;
}
import { inject as service, type Registry as Services } 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',
});
}
}
<template>
-tag componentsBefore v6.4.0
, you had to remember and write the full path to use one of ember-intl
's helpers in a <template>
-tag component. Now, you can import all helpers from the index
file.
t()
import type { TOC } from '@ember/component/template-only';
- import t from 'ember-intl/helpers/t';
+ import { t } from 'ember-intl';
interface HelloSignature {
Args: {
name: string;
};
}
const HelloComponent: TOC<HelloSignature> =
<template>
<div data-test-message>
{{t "hello.message" name=@name}}
</div>
</template>
export default HelloComponent;
allowEmpty
If you happened to overwrite an ember-intl
's helper so that, in your app, the helper allows "empty" values by default, then you may continue to use the following syntax:
/* my-addon/addon/helpers/t.ts */
import THelper from 'ember-intl/helpers/t';
export default class extends THelper {
allowEmpty = true;
}
However, this (overwriting the addon, especially through inheritance) is not recommended, as implementation details can change in the future.
[!NOTE] Going forward, I'd like to see if all helpers can return an empty string when value is either
undefined
ornull
. You can visit https://github.com/ember-intl/ember-intl/issues/1813 and Discord thread to provide your feedback.
In addition to separating concerns better, the pull request fixes an error that you might have seen in your tests after installing [email protected]
or 6.3.1
.
actual: >
null
stack: >
Error: Can not call `.lookup` after the owner has been destroyed
at Container.lookup
at Class.lookup
at THelper.getInjection
at untrack
at ComputedProperty.get
at THelper.getter [as intl]
I fixed the type issues introduced in 6.3.0
. All helpers have a return type of string
once again.
[!IMPORTANT] To resolve long-existing type issues, I had to change an implementation detail: In certain error cases, the helpers now return an empty string
''
instead ofundefined
.Since both values are considered falsy in
*.hbs
as well as*.{js,ts}
, I'm hoping that changing the return value will be non-breaking in most cases.Going forward, we'll try to limit the API and be more strict about types.
I removed the base helper -format-base.js
so that the code for each helper is easier to understand and maintain.
[!IMPORTANT] If you happened to create a helper that extends
-format-base.js
(private implementation), please extend theHelper
class from@ember/component/helper
and inject theintl
service instead. For reference, please check the{{t}}
helper.
/* addon/helpers/legacy-format-money.ts */
import BaseHelper from 'ember-intl/helpers/-format-base';
import { legacyFormatMoney } from '../utils/index';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Type 'typeof AbstractHelper' is not a constructor function type.
export default class LegacyFormatMoneyHelper extends BaseHelper {
format(money, options): string {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Property 'intl' does not exist on type 'LegacyFormatMoneyHelper'
return legacyFormatMoney(this.intl, money, options);
}
}
/* addon/helpers/legacy-format-money.ts */
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import type { IntlService } from 'ember-intl';
import { legacyFormatMoney } from '../utils/index';
export default class LegacyFormatMoneyHelper extends Helper {
@service declare intl: IntlService;
constructor() {
// eslint-disable-next-line prefer-rest-params
super(...arguments);
// @ts-expect-error: Property 'onLocaleChanged' is private and only accessible within class 'IntlService'.
this.intl.onLocaleChanged(this.recompute, this);
}
compute([money], options): string {
return legacyFormatMoney(this.intl, money, options);
}
}
Compared to v6.2.2
, the unpacked size may be slightly larger (~2 kB). On the plus side, a few type errors and hidden bugs (runtime errors) should be fixed now. Since the code change is large, I went with a minor release to be on the safe side.
[!WARNING] There is a known issue for Glint users, caused by the
{{t}}
helper returningSafeString
andundefined
as possible types. This will be addressed soon.app/templates/form.hbs:1:14 - error TS2345: Argument of type 'string | SafeString | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. 1 {{page-title (t "routes.form.title")}} ~~~~~~~~~~~~~~~~~~~~~~~ app/templates/form.hbs:6:6 - error TS2322: Type 'string | SafeString | undefined' is not assignable to type 'string | undefined'. Type 'SafeString' is not assignable to type 'string'. 6 @instructions={{t ~~~~~~~~~~~~
If you encountered TypeScript errors of the following form after installing 6.2.0
or 6.2.1
,
tests/integration/components/my-component.ts:59:23 - error TS2345: Argument of type 'string | SafeString' is not assignable to parameter of type 'string'.
Type 'SafeString' is not assignable to type 'string'.
59 .hasText(t('some.key'));
please try installing 6.2.2
instead. As a temporary fix, I cast the return type of the test helper t()
to be string
.