DEV Community

devremoto
devremoto

Posted on

Automating Angular i18n: from hard-coded strings to ngx-translate in one right-click

Adding i18n to an Angular app that never had it is one of those jobs everybody postpones. The library part is easy — ngx-translate is a five-minute install. The painful part is the other 95%: hunting down every hard-coded string across hundreds of templates and TypeScript files, inventing a key for each one, moving the text into JSON, and then doing it again for every language.

I hit that wall on a side project and got tired of doing it by hand, so I built a VS Code extension that does the whole loop in one right-click.

Usage demo

What one right-click does

  • Scans your TS/HTML — including inline @Component templates — for user-facing strings
  • Replaces them in place: {{ 'APP.HERO.TITLE' | translate }} in templates, this.translateService.instant('APP.HERO.TITLE') in TypeScript
  • Generates one JSON per language under assets/i18n, with nested keys derived from the file path
  • Auto-translates the other languages (free Google endpoint — no API key, no account)
  • Wires up ngx-translate: an HttpClient loader, main.ts providers, and a ready-made language-selector component

Why zero configuration matters

The first version of this extension had settings for the source folder, the output folder, the languages file, and main.ts. Every one of them was a chance to be wrong — and they were: point the extension at a workspace opened one level above the app, and paths silently doubled up into frontend/frontend/src/assets/i18n.

So I deleted all of them. Now the extension reads your project:

  1. Find the nearest angular.json — searching up from the file you triggered the command on, then down from the opened folder
  2. Read the sourceRoot of the project that owns that path
  3. Derive everything else from it — locales at <sourceRoot>/assets/i18n, the languages list, and main.ts from the build target

A monorepo with projects/app/src works untouched. Open the workspace above your Angular app, below it, or right on it — same result. If there's no angular.json anywhere, the command stops with a clear message instead of guessing.

What it deliberately does NOT touch

An extractor that rewrites your source is only useful if you can trust it. These are all guarded, each because it broke something first:

  • Angular control flow — the text node } @else { is structure, not a string. Extracting it deletes your block delimiters and you get NG5002 errors hundreds of lines away.
  • HTML comments — a multi-line comment containing <style> or an apostrophe used to get shredded, taking its --> with it.
  • CSS selectors in DOM callsquerySelectorAll('img, .photo') is two words, so a naive "is this a sentence?" heuristic happily translated it. Now every string argument of querySelector, closest, addEventListener, classList.* and friends is off-limits.
  • Strings outside a classthis.translateService can't compile in a module-level const, so those are skipped with a warning rather than replaced.
  • Your existing injection — if the class already has a TranslateService (any name, inject() or constructor), it's reused. And the injected member never collides with a member you already declare: a service with its own translate(id, lang) method gets translateService, not a duplicate identifier.

That last one is decided by a real Babel parse of the class, not a regex — object-literal keys like translate: this.translate used to look like class members and trigger false renames.

Try it

I'm the author. It's free and open source — if it mangles something in your templates, open an issue with the snippet and I'll add a guard for it. Edge cases from real codebases are exactly what makes this kind of tool trustworthy.

Top comments (0)