Quickstart (Apps)
1. Install ember-intl
v1 apps
Use your package manager to install ember-intl and @ember-intl/v1-compat.
pnpm add -D ember-intl @ember-intl/v1-compatNext, create the folder translations as a sibling to app.
my-app
├── app
└── translationsv2 apps
Use your package manager to install ember-intl and @ember-intl/vite.
pnpm add -D ember-intl @ember-intl/viteTo prepare for loading translations, you will need to update 2 configuration files. The first is vite.config.{mjs,mts}, where you add loadTranslations to the list of plugins for Vite. The second is if you use TypeScript: Add the path @ember-intl/vite/virtual to compilerOptions.types in tsconfig.json.
import { loadTranslations } from '@ember-intl/vite';
import { classicEmberSupport, ember, extensions } from '@embroider/vite';
import { babel } from '@rollup/plugin-babel';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
classicEmberSupport(),
ember(),
babel({
babelHelpers: 'runtime',
extensions,
}),
loadTranslations(),
],
});{
"compilerOptions": {
"types": [
"ember-source/types",
"@embroider/core/virtual",
"@glint/ember-tsc/types",
"vite/client",
"@ember-intl/vite/virtual"
]
}
}Finally, create the folder translations as a sibling to app.
my-app
├── app
└── translations2. Define translations
Create a translation in translations/en-us.yaml.
hello.message: "Hello, {name}!"To render the translation in a component's or route's template, import the t helper and call it inside a <template> tag.
import { t } from 'ember-intl';
<template>
{{t "hello.message" name="Zoey"}}
</template>IMPORTANT
This guide will show examples of templates only in "strict mode." Strict means, the template lives inside a <template> tag in a *.{gjs,gts} file, and you use import to get what you need from ember-intl.
In older projects, you can still use ember-intl's helpers in "loose mode," i.e. in an *.hbs file or an <hbs> tag in rendering tests. To do so, you skip the import and dasherize a helper's name (e.g. formatDate in strict mode becomes format-date in loose, while t is the same by chance).
{{t "hello.message" name="Zoey"}}For more information, see Helpers - <template> tag.
3. Define languages
Create the file translations/de-de.yaml to support the de-de locale. (Throughout the guide, we use the terms "language" and "locale" interchangeably.)
hello.message: "Hallo, {name}!"NOTE
You can also use .yml or .json. Here is the JSON equivalent of the YAML code above.
{
"hello.message": "Hallo, {name}!"
}4. Set up ember-intl
ember-intl provides the intl service, which acts as the source of truth when it comes to internationalization.
Before your app renders, you need to tell the intl service which translations are available and which locale(s) to use. Since timing is a factor, we will set up ember-intl in the application route's beforeModel hook.
v1 apps
When the app starts, @ember-intl/v1-compat automatically loads your translations, then passes them to the intl service. So your only job is to call setLocale to specify which language(s) the app should use initially.
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.setLocale(['en-us']);
}
}v2 apps
When the app starts, @ember-intl/vite automatically loads your translations, but it does not pass them to the intl service. You are responsible for importing the translations that you need, then calling addTranslations to pass them to the intl service.
Like in v1 apps, you call setLocale to specify the initial locale(s).
import Route from '@ember/routing/route';
import { type Registry as Services, service } from '@ember/service';
import translationsForDeDe from 'virtual:ember-intl/translations/de-de';
import translationsForEnUs from 'virtual:ember-intl/translations/en-us';
export default class ApplicationRoute extends Route {
@service declare intl: Services['intl'];
beforeModel(): void {
this.setupIntl();
}
private setupIntl(): void {
this.intl.addTranslations('de-de', translationsForDeDe);
this.intl.addTranslations('en-us', translationsForEnUs);
this.intl.setLocale(['en-us']);
}
}NOTE
File paths prefixed with virtual: are called a "virtual module" in Vite. They don't physically exist on disk.
5. Configure linters
@ember-intl/lint
@ember-intl/lint is the official linter for ember-intl.
ember-template-lint
ember-template-lint provides no-bare-strings. This finds hard-coded texts in templates.
'use strict';
module.exports = {
extends: ['recommended'],
rules: {
'no-bare-strings': true,
},
};glint
At the end of September 2025, glint released a Volar-based v2. Put it simply, you have v1 if your app depends on @glint/core, and v2 if on @glint/ember-tsc.
If you use v1 and "loose mode" templates (you have
*.hbsfiles orhbstags), then extendember-intl's template registry.tsimport '@glint/environment-ember-loose'; import type EmberIntlRegistry from 'ember-intl/template-registry'; declare module '@glint/environment-ember-loose/registry' { export default interface Registry extends EmberIntlRegistry, /* other addon registries */ { // local entries } }If you use v1 and "strict mode" templates (you don't have
*.hbsfiles orhbstags, or don't wish to type-check any remaining ones), then you are good to go.If you use v2, then you are good to go.
Miscellaneous
prettier can format your translation files. To reduce noise in pull requests, consider sorting the translation keys and standardizing their names.