Internationalization (i18n) with Angular
Built-in Angular i18n from scratch: mark strings, extract XLF, translate to Greek and French, build per locale. No ngx-translate in this walkthrough.
Internationalization (i18n) gets your app ready for multiple languages. Localization (l10n) is the actual translation for a locale.
You can reach for ngx-translate or Angular's built-in i18n. This post sticks to built-in i18n so you see the compile-time path end to end.
If part of your audience reads Greek or French, shipping one English bundle is a choice you're making on purpose. Might be fine. Might not.
New Angular Project
# Install Angular CLI globally
npm install --global @angular/cli
# Create Angular project
ng new ng-internationalization
# Would you like to add Angular routing? N
# Which stylesheet format would you like to use? SCSS
# Go to project's directory
cd ng-internationalization
# Open this folder with your favourite editor
code .
# Serve the application
ng serve -o
Locales
Default is en-US. Extra languages mean extra build configs. Locale codes live in references like i18n-locales.
Starter template
Drop sample copy in app.component.html so we have something to mark and extract.
<main>
<section>
<h1>Why is Internationalization so important?</h1>
<p>
These days, with so many websites to choose from, making applications available and
user-friendly to a worldwide audience is important. It's the move to make a better user
experience.
<small
>Find the full article
<a href="https://github.com/ThPadelis/ng-internationalization" target="_blank"
>here</a
></small
>
</p>
</section>
</main>
We can add some styles to make this site a bit more user-friendly, inside of app.component.scss
@import url("https://fonts.googleapis.com/css?family=Roboto&display=swap&subset=greek");
main {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #6441a5;
background: -webkit-linear-gradient(to right, #6441a5, #2a0845);
background: linear-gradient(to right, #6441a5, #2a0845);
section {
font-family: "Roboto", sans-serif;
text-align: center;
color: #cec6d4;
border: solid thin #cec6d4;
padding: 40px;
width: 50%;
-webkit-box-shadow: 15px 15px 5px 0px rgba(0, 0, 0, 0.75);
-moz-box-shadow: 15px 15px 5px 0px rgba(0, 0, 0, 0.75);
box-shadow: 15px 15px 5px 0px rgba(0, 0, 0, 0.75);
a {
color: inherit;
}
small {
display: block;
margin-top: 10px;
}
}
}
And finally, inside of styles.scss let's set the body styles
body {
margin: 0px;
padding: 0px;
box-sizing: border-box;
}
You should see something like this:

Mark strings with i18n
Add the i18n attribute on every string you want translated. We'll target el-GR and fr-FR. French translations below are Google Translate quality (be kind).
Let's add the i18n attribute to all of the text that we want to translate
<main>
<section>
<h1 i18n>Why is Internationalization so important?</h1>
<p i18n>
These days, with so many websites to choose from, making applications available and
user-friendly to a worldwide audience is important. It's the move to make a better user
experience.
</p>
<small i18n
>Find the full article
<a href="https://dev.to/thpadelis/internationalization-i18n-with-angular-4ao7" target="_blank"
>here</a
></small
>
</section>
</main>
i18nis not a runtime directive. The compiler strips it after translation.
Add a script in package.json to extract messages:
"scripts": {
"i18n:extract": "ng xi18n --output-path src/locales"
}
Run npm run i18n:extract, then open src/locales/messages.xlf:
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="4bcef995bebcf205074f9fd756b822a488b452cc" datatype="html">
<source>Why is Internationalization so important?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="e8db4c58a5fc95a2a8d80183e4b527f4480fa06e" datatype="html">
<source>
These days, with so many websites to choose from, making applications
available and user-friendly to a worldwide audience is important. It's the
move to make a better user experience.
</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
</trans-unit>
<trans-unit id="0f16c7aaa76d2f52dbabcd329ebf11a39a26918d" datatype="html">
<source>Find the full article
<x id="START_LINK" ctype="x-a" equiv-text="<a>"/>here<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>
For each html element marked with the i18n directive, a trans-unit will be created.
<trans-unit id="4bcef995bebcf205074f9fd756b822a488b452cc" datatype="html">
<source>Why is Internationalization so important?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
Add description (and optional meaning) to help translators:
<main>
<section>
<h1 i18n="Title for the article">Why is Internationalization so important?</h1>
<p i18n="Description for the article">
These days, with so many websites to choose from, making applications available and
user-friendly to a worldwide audience is important. It's the move to make a better user
experience.
</p>
<small i18n="Read the whole article"
>Find the full article
<a href="https://github.com/ThPadelis/ng-internationalization" target="_blank">here</a></small
>
</section>
</main>
Additionally, we can help the translator with a description and a meaning by using this format
<!-- i18n="<meaning>|<description>" -->
<main>
<section>
<h1 i18n="Article Heading|Title for the article">Why is Internationalization so important?</h1>
<p i18n="Article Description|Description for the article">
These days, with so many websites to choose from, making applications available and
user-friendly to a worldwide audience is important. It's the move to make a better user
experience.
</p>
<small i18n="Full Article|Read the whole article"
>Find the full article
<a href="https://github.com/ThPadelis/ng-internationalization" target="_blank">here</a></small
>
</section>
</main>
Custom @@id keeps units stable when text moves:
<!-- i18n="<meanin>|<description>@@customId" -->
<h1 i18n="Article Heading|Title for the article@@articleHeading">
Why is Internationalization so important?
</h1>
We can finally generate our translations
npm run i18n:extract
Now that we have added meaning, description and id our messages.xml should look like this
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="articleHeading" datatype="html">
<source>
Why is Internationalization so important?
</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<note priority="1" from="description">Title for the article</note>
<note priority="1" from="meaning">Article Heading</note>
</trans-unit>
<trans-unit id="articleDescription" datatype="html">
<source>
These days, with so many websites to choose from, making applications
available and user-friendly to a worldwide audience is important. It's the
move to make a better user experience.
</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
<note priority="1" from="description">Description for the article</note>
<note priority="1" from="meaning">Article Description</note>
</trans-unit>
<trans-unit id="fullArticle" datatype="html">
<source>Find the full article
<x id="START_LINK" ctype="x-a" equiv-text="<a>"/>here<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
<note priority="1" from="description">Read the whole article</note>
<note priority="1" from="meaning">Full Article</note>
</trans-unit>
</body>
</file>
</xliff>
Translation files
Copy the base XLF per language:
cp src\locales\messages.xlf src\locales\messages.fr.xlf
cp src\locales\messages.xlf src\locales\messages.el.xlf
Greek
Let's start with messages.el.xlf. We will translate the messages by using the target and source. The target attribute is the the translation in that language.
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="articleHeading" datatype="html">
<source>Why is Internationalization so important?</source>
<target>Γιατί είναι τόσο σημαντική η διεθνοποίηση;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<note priority="1" from="description">Title for the article</note>
<note priority="1" from="meaning">Article Heading</note>
</trans-unit>
<trans-unit id="articleDescription" datatype="html">
<source>These days, with so many websites to choose from, making applications available and user-friendly to a worldwide audience is important. It's the move to make a better user experience.</source>
<target>Αυτές τις μέρες, ανάμεσα σε τόσους ιστότοπους για να διαλέξεις, κάνοντας εφαρμογές διαθέσιμες και φιλικές προς τον χρήστη είναι σημαντικό. Είναι μία κίνηση για καλύτερη εμπειρία χρήστη.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
<note priority="1" from="description">Description for the article</note>
<note priority="1" from="meaning">Article Description</note>
</trans-unit>
<trans-unit id="fullArticle" datatype="html">
<source>Find the full article <x id="START_LINK" ctype="x-a" equiv-text="<a>"/>here<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/></source>
<target>Βρείτε ολόκληρο το άρθρο <x id="START_LINK" ctype="x-a" equiv-text="<a>"/>εδώ<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
<note priority="1" from="description">Read the whole article</note>
<note priority="1" from="meaning">Full Article</note>
</trans-unit>
</body>
</file>
</xliff>
French
Now, let's do the same for messages.fr.xlf in French
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="articleHeading" datatype="html">
<source>Why is Internationalization so important?</source>
<target>Pourquoi l'internationalisation est-elle si importante?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<note priority="1" from="description">Title for the article</note>
<note priority="1" from="meaning">Article Heading</note>
</trans-unit>
<trans-unit id="articleDescription" datatype="html">
<source>These days, with so many websites to choose from, making applications available and user-friendly to a worldwide audience is important. It's the move to make a better user experience.</source>
<target>Ces jours-ci, avec autant de sites Web à choisir, il est important de rendre les applications disponibles et conviviales pour un public mondial. C'est le geste d'améliorer l'expérience utilisateur.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
<note priority="1" from="description">Description for the article</note>
<note priority="1" from="meaning">Article Description</note>
</trans-unit>
<trans-unit id="fullArticle" datatype="html">
<source>Find the full article <x id="START_LINK" ctype="x-a" equiv-text="<a>"/>here<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/></source>
<target>Retrouvez l'article complet <x id="START_LINK" ctype="x-a" equiv-text="<a>"/>ici<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
<note priority="1" from="description">Read the whole article</note>
<note priority="1" from="meaning">Full Article</note>
</trans-unit>
</body>
</file>
</xliff>
French below is machine-translated. Don't ship it to production without a human pass.
Build per locale
Wire angular.json so each locale gets its own output folder:
{
"projects": {
"ng-internationalization": {
"architect": {
"build": {
"configurations": {
"fr-FR": {
"aot": true,
"outputPath": "dist/fr-FR",
"i18nFile": "src/locales/messages.fr.xlf",
"i18nFormat": "xlf",
"i18nLocale": "fr",
"i18nMissingTranslation": "error"
},
"el-GR": {
"aot": true,
"outputPath": "dist/el-GR",
"i18nFile": "src/locales/messages.el.xlf",
"i18nFormat": "xlf",
"i18nLocale": "el-GR",
"i18nMissingTranslation": "error"
}
}
},
"serve": {
"configurations": {
"production": {
"browserTarget": "ng-internationalization:build:production"
},
"fr-FR": {
"browserTarget": "ng-internationalization:build:fr-FR"
},
"el-GR": {
"browserTarget": "ng-internationalization:build:el-GR"
}
}
}
}
}
}
}
NOTE: I've omitted most of the file to keep it brief.
Let's create some more scripts inside the package.json file to be able to build and serve our new locales.
"scripts": {
"start": "ng serve",
"start:fr": "ng serve --configuration=fr-FR",
"start:gr": "ng serve --configuration=el-GR",
"build": "ng build",
"build:fr": "ng build --configuration=fr-FR",
"build:gr": "ng build --configuration=el-GR",
}
NOTE: Once again, I kept the file brief.
Serve each locale on its own port:
npm run start
npm run start:fr -- --port 4201
npm run start:gr -- --port 4202

Wrap-up
Built-in i18n is more ceremony up front, but you get separate bundles per language without shipping every string to every user. ngx-translate trades compile-time safety for runtime flexibility. Pick based on how often copy changes and who owns translations.