I've been building frontends for Swiss banks and financial software companies for over ten years. First AngularJS, then Angular, with some React in between. Switzerland has four national languages, so almost every one of these projects had to ship in German, French and English, and often Italian too.
So I've done a lot of Angular i18n. And I've come to a strong opinion: translation keys are built for the translation process, not for the developers who have to live with them.
Every project starts the same way
Someone sets up ngx-translate, creates en.json, and the templates start to look like this:
<h2>{{ 'PORTFOLIO.OVERVIEW.PERFORMANCE_TITLE' | translate }}</h2> <button>{{ 'COMMON.ACTIONS.SAVE' | translate }}</button> It feels clean for about two sprints. Then:
- Nobody knows whether
PORTFOLIO.OVERVIEW.PERFORMANCE_TITLEis still used anywhere, so nobody dares to delete it. The JSON files only grow. - There are three keys with the text "Save", because searching the JSON before adding a key is nobody's favourite activity.
- In code review, the diff shows a key. To know what the user actually sees, you open a second file.
- When a key is missing in one language, the user sees
PORTFOLIO.OVERVIEW.PERFORMANCE_TITLEon the screen.
Angular's built-in i18n is not the escape
I like that the i18n attribute keeps the text in the template. That part is right. But then:
-
ng extract-i18n, XLF files, and a merge step every time the text changes. - Generated message IDs change when the text changes, so you start adding
@@custom-ids. Congratulations, you have translation keys again. - One build per locale. Switching the language means loading a different app.
For a public website that might be acceptable. For an internal dashboard where a portfolio manager switches between German and English, it's a lot of build pipeline for very little.
Shared component libraries make it worse
At one company I helped build a shared Angular component library used by several projects. The components had their own texts: "No results", "Show more", validation messages, table paging.
Who owns those translations? The library, so every app inherits texts it can't change? Or every app, so every consumer has to provide the same 40 keys? We went back and forth on this more than on any actual component.
The real cost is waiting
In a bank, a new text is rarely just a new text. The developer adds the key with the English value. Then someone has to provide French and Italian, ideally someone from the business side who knows the domain terms. Until that happens, the feature is done, tested, and can't be released.
The irony: most of these screens were internal tools used by a few dozen specialists.
What I do now
When I started my side project ChordPic, I didn't want any of this. So the text just stays in the template, gets translated by AI, and the result is cached. When users asked for their language, adding it was one line, and it was live the same day.
That turned into Magic Translate, and it has an Angular client:
// app.config.ts provideMagicTranslate({ apiKey: 'YOUR_API_KEY', language: 'de' }) <h2><t>Portfolio performance</t></h2> <p><t>Positions are revalued at <b>market close</b>.</t></p> <input [placeholder]="'Search positions' | t" /> That's it. No keys, no JSON files, and the diff in code review shows exactly what the user will read. And because this is Switzerland, formality: 'formal' is one option away. Nobody wants their bank to call them "du".
The tricky part: values in the text
One of the challenges with translating plain text is that real UI text is full of values. "Hi Anna, you have 3 new alerts" and "Hi Marco, you have 5 new alerts" are different strings. Translate them as they are, and every name and every count is a new translation: slower, more expensive, and nothing gets reused from the cache.
The fix is to tell Magic Translate which parts are values. In Angular you pass them in [v] and reference them with $name in the text:
<t [v]="{ name: user.name, count: alerts().length }"> Hi $name, you have $count new alerts </t> What gets translated is the template, Hi {0}, you have {1} new alerts. It's translated once and cached, whatever the values are, and the values themselves never leave the browser. The translation can also move the placeholders to wherever the other language needs them.
Where I would still use keys
To be fair, I wouldn't use this for everything:
- Legally binding text. Contracts, regulatory disclosures, anything compliance has to sign off string by string. You can edit translations by hand in the dashboard, but if every string needs a formal review, a classic workflow with translators is the right tool.
- Teams with professional translators. If translators are already part of your process, keys and translation files are what their tools expect.
But for the internal tools, dashboards and side projects that make up most of what I've built? Keys were never worth it.
So, what's your setup?
Are you on ngx-translate, Transloco, the built-in i18n, or something else? And has a raw translation key ever made it to production on your watch?
Disclosure: I built Magic Translate. It has a free tier at magictranslate.io.
