Skip to content

Migration from v8 to v9

TIP

Run pnpx @ember-intl/update to address breaking changes. The badge Run codemod appears next to those that the codemod can handle.

Breaking changes

Minimum requirements

Projects with these versions are supported when issues arise.

  • Ember 4.12 and above
  • Node 22 and above
  • @ember/test-helpers 5.x and above

Removed handling nested translation JSON

From 5.x to 7.x, the intl service's addTranslations flattened a translation JSON (data representation of translation files), because the Node part of the addon, which was responsible for reading translation files, didn't handle flattening.

In 8.x, addTranslations continued to flatten a JSON even though @ember-intl/vite had already flattened it, because @ember-intl/v1-compat didn't handle flattening. That is, Vite apps encountered an unnecessary cost, while maintaining the ember-intl project became more difficult due to different implementations and unclear boundaries.

Going forward, all packages that load translations (i.e. lint, v1-compat, and vite) will be responsible for flattening a translation JSON. When you call intl.addTranslations(), you will need to ensure that translations (the 2nd parameter) is flat:

diff
type TranslationKey = string;
type TranslationMessage = string;

- type TranslationJson = Record<TranslationJson | TranslationKey, TranslationMessage>;
+ type TranslationJson = Record<TranslationKey, TranslationMessage>;
diff
this.intl.addTranslations('en-us', {
-   hello: {
-     message: 'Hello, {name}!',
-   },
+   'hello.message': 'Hello, {name}!',
});

You will need to update code if your tests rely on the test helper addTranslations or on setupIntl with translations (the 3rd parameter used to stub translations).

diff
import { render } from '@ember/test-helpers';
import { setupIntl } from 'ember-intl/test-support';
import Hello from 'my-app/components/hello';
import { setupRenderingTest } from 'my-app/tests/helpers';
import { module, test } from 'qunit';

module('Integration | Component | hello', function (hooks) {
  setupRenderingTest(hooks);
  setupIntl(hooks, 'en-us', {
-   hello: {
-     message: 'Hi, {name}!',
-   },
+   'hello.message': 'Hi, {name}!',
  });

  // ...
});

Removed handling options for formatMessage and t

From 5.x to 8.x, the intl service's formatMessage processed the object values that you passed to options (the 2nd parameter) when options.htmlSafe is set to true. It sanitized string values and converted SafeString to string by copying implementation details from ember-source (removed in 6.6.0). The same processing occurs in t, since it relies on formatMessage.

The additional code made maintenance more difficult. It didn't necessarily improve your developer experience either, because the output may differ from what you expect. To address both problems, formatMessage and t will no longer process the object values in options when options.htmlSafe is set to true.

The code diff below shows that, from 5.x to 8.x, you would have had to call trustHTML (formerly known as htmlSafe) from @ember/template to see the text Hello, Zoey! and the CSS classes message and emphasize. Now, you shouldn't call trustHTML if you want to see the same HTML output.

diff
import { trustHTML } from '@ember/template';
import { render } from '@ember/test-helpers';
import { formatMessage } from 'ember-intl';
import { setupIntl } from 'ember-intl/test-support';
import { setupRenderingTest } from 'my-app/tests/helpers';
import { module, test } from 'qunit';

module('Integration | Helper | format-message', function (hooks) {
  setupRenderingTest(hooks);
  setupIntl(hooks, 'en-us');

  const message = '<div class="message">Hello, {name}!</div>';

  test('it renders', async function (assert) {
    const name = '<span class="emphasize">Zoey</span>';

    await render(
      <template>
        {{formatMessage message name=name}}
      </template>,
    );

    assert
      .dom()
      .hasText(
        '<div class="message">Hello, <span class="emphasize">Zoey</span>!</div>',
      );
  });

  test('it renders (with options.htmlSafe)', async function (assert) {
    const name = '<span class="emphasize">Zoey</span>';

    await render(
      <template>
        {{formatMessage message htmlSafe=true name=name}}
      </template>,
    );

-     assert.dom().hasText('Hello, <span class="emphasize">Zoey</span>!');
+     assert.dom().hasText('Hello, Zoey!');
  });

  test('it renders (with options.htmlSafe and trustHTML)', async function (assert) {
    const name = trustHTML(
      '<span class="emphasize">Zoey</span>',
    ) as unknown as string;

    await render(
      <template>
        {{formatMessage message htmlSafe=true name=name}}
      </template>,
    );

-     assert.dom().hasText('Hello, Zoey!');
+     assert.dom().hasText('Hello, ,Zoey,!');
  });
});

Removed handling spaces in translation folder names

Apps can use wrapTranslationsWithNamespace (now called namespaceKeysByDir) to namespace translation keys by folder names. From 5.x to 7.x, ember-intl used to convert spaces in folder names to underscores. This implementation detail was likely seldom needed and was unknown to end-developers.

@ember-intl/lint, @ember-intl/v1-compat, and @ember-intl/vite will no longer normalize the folder names. If a subfolder in /translations has a space in its name, replace the space with the underscore _ to keep your source code the same.

Removed test helper t

For a while, the documentation site recommended not using the test helper t because it creates a tautology: t(...) is equal to t(...) (here, the t refers to that from the intl service).

An hasText or includesText assertion, when combined with the test helper t, becomes weak: It only guarantees that the translation key is correct, not the rendered message.

NOTE

Even when a translation is missing (by accident), the hasText or includesText assertion would pass, since the test helper t can't help you verify the rendered message.

If possible, load translations in tests and always pass the string that you expect to see to hasText and includesText. This way, you can easily know what the app displayed at a given time (static code analysis). You can also change translations with more confidence when assertions begin to fail.

diff
- import { setupIntl, t } from 'ember-intl/test-support';
+ import { setupIntl } from 'ember-intl/test-support';

module('Integration | Component | hello', function (hooks) {
  setupRenderingTest(hooks);
  setupIntl(hooks, 'de-de');

  test('it renders', async function (assert) {
    await render(<template><Hello @name="Zoey" /></template>);

    assert
      .dom('[data-test-message]')
-       .hasText(t('hello.message', { name: 'Zoey' }));
+       .hasText('Hallo, Zoey!');
  });
});

Renamed build options

To clarify intent and remove references to implementation details in classic Ember, three of the four buildOptions keys that @ember-intl/lint@v1, @ember-intl/v1-compat@v1, and @ember-intl/vite@v1 (i.e. ember-intl@v8) relied on have been renamed.

BeforeAfter
inputPathtranslationsDir
publicOnlybundleSeparately
wrapTranslationsWithNamespacenamespaceKeysByDir

These packages will throw an error if you continue to use the old key name. Note, the key name fallbackLocale remains the same.