<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: devremoto</title>
    <description>The latest articles on DEV Community by devremoto (@devremoto).</description>
    <link>https://dev.to/devremoto</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3563364%2F6966bebd-64ff-4dd3-9db9-1d2d168ca55d.jpeg</url>
      <title>DEV Community: devremoto</title>
      <link>https://dev.to/devremoto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devremoto"/>
    <language>en</language>
    <item>
      <title>Automating Angular i18n: from hard-coded strings to ngx-translate in one right-click</title>
      <dc:creator>devremoto</dc:creator>
      <pubDate>Tue, 11 Aug 2026 01:51:30 +0000</pubDate>
      <link>https://dev.to/devremoto/automating-angular-i18n-from-hard-coded-strings-to-ngx-translate-in-one-right-click-1e6g</link>
      <guid>https://dev.to/devremoto/automating-angular-i18n-from-hard-coded-strings-to-ngx-translate-in-one-right-click-1e6g</guid>
      <description>&lt;p&gt;Adding i18n to an Angular app that never had it is one of those jobs everybody postpones. The library part is easy — &lt;code&gt;ngx-translate&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zyotx2r79ta2trvs53t.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zyotx2r79ta2trvs53t.gif" alt="Usage demo" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What one right-click does
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  Why zero configuration matters
&lt;/h2&gt;

&lt;p&gt;The first version of this extension had settings for the source folder, the output folder, the languages file, and &lt;code&gt;main.ts&lt;/code&gt;. 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 &lt;code&gt;frontend/frontend/src/assets/i18n&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I deleted all of them. Now the extension reads your project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the nearest &lt;code&gt;angular.json&lt;/code&gt; — searching up from the file you triggered the command on, then down from the opened folder&lt;/li&gt;
&lt;li&gt;Read the &lt;code&gt;sourceRoot&lt;/code&gt; of the project that owns that path&lt;/li&gt;
&lt;li&gt;Derive everything else from it — locales at &lt;code&gt;&amp;lt;sourceRoot&amp;gt;/assets/i18n&lt;/code&gt;, the languages list, and &lt;code&gt;main.ts&lt;/code&gt; from the build target&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h2&gt;
  
  
  What it deliberately does NOT touch
&lt;/h2&gt;

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

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

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

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Marketplace: &lt;a href="https://marketplace.visualstudio.com/items?itemName=AdilsondeAlmeidaPedro.angular-translation-extractor" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=AdilsondeAlmeidaPedro.angular-translation-extractor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/devremoto/angular-translation-extractor" rel="noopener noreferrer"&gt;https://github.com/devremoto/angular-translation-extractor&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>i18n</category>
      <category>vscode</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
