<?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>Forem: Anton Antonov</title>
    <description>The latest articles on Forem by Anton Antonov (@anton_antonov).</description>
    <link>https://forem.com/anton_antonov</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3644119%2F74c5b0fa-e9d5-41d0-9638-ba0c062ec2a0.jpg</url>
      <title>Forem: Anton Antonov</title>
      <link>https://forem.com/anton_antonov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/anton_antonov"/>
    <language>en</language>
    <item>
      <title>Flutter Localization Done Right: ARB Files, Pluralization, and Automated Translations</title>
      <dc:creator>Anton Antonov</dc:creator>
      <pubDate>Tue, 10 Mar 2026 12:43:32 +0000</pubDate>
      <link>https://forem.com/anton_antonov/flutter-localization-done-right-arb-files-pluralization-and-automated-translations-4g9h</link>
      <guid>https://forem.com/anton_antonov/flutter-localization-done-right-arb-files-pluralization-and-automated-translations-4g9h</guid>
      <description>&lt;p&gt;Flutter’s built-in localization system uses Application Resource Bundle (ARB) files and the intl package to deliver fully native experiences in any language. This guide covers everything from project setup and ARB format details to language switching, pluralization, and automating translations with l10n.dev.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Flutter Localization?
&lt;/h2&gt;

&lt;p&gt;Flutter localization is the official approach to adapting your app for multiple languages and regions. It relies on ARB files to store translated strings and the flutter_gen code generator to produce type-safe Dart accessors. At runtime, Flutter selects the correct ARB file based on the device locale — no restart required when switching locales programmatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Setup
&lt;/h2&gt;

&lt;p&gt;Flutter's localization pipeline is driven by two configuration files: pubspec.yaml and l10n.yaml. Enable code generation in pubspec.yaml and point Flutter to your ARB directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Update pubspec.yaml
&lt;/h3&gt;

&lt;p&gt;Add flutter_localizations and intl as dependencies, then enable the generate flag so Flutter auto-generates the Dart localization classes from your ARB files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flutter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sdk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flutter&lt;/span&gt;
  &lt;span class="na"&gt;flutter_localizations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sdk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flutter&lt;/span&gt;
  &lt;span class="na"&gt;intl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;any&lt;/span&gt;

&lt;span class="na"&gt;flutter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Create l10n.yaml
&lt;/h3&gt;

&lt;p&gt;Place an l10n.yaml file at the root of your project to configure the ARB directory, the template file, and the generated output file name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;arb-dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lib/l10n&lt;/span&gt;
&lt;span class="na"&gt;template-arb-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_en.arb&lt;/span&gt;
&lt;span class="na"&gt;output-localization-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_localizations.dart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The ARB File Format
&lt;/h2&gt;

&lt;p&gt;ARB (Application Resource Bundle) is a JSON-based format designed specifically for Flutter internationalization. Each key maps to a translatable string, and optional metadata keys (prefixed with @) carry descriptions, placeholder definitions, and context for translators.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;@@locale&lt;/strong&gt;: Declares the locale for the file (e.g., "en", "fr", "zh_CN"). l10n.dev automatically updates this to the target language code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@@last_modified&lt;/strong&gt;: Timestamp of the last modification. l10n.dev automatically sets this to the current UTC timestamp when generating translated files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@key&lt;/code&gt; metadata&lt;/strong&gt;: Metadata entries such as description, example, and placeholders are NOT translated by default - they remain unchanged to preserve context for translators. Enable the translateMetadata setting if you want these translated.&lt;/li&gt;
&lt;li&gt;Placeholders and ICU messages: Strings can contain named placeholders ({name}) and ICU message syntax for pluralization and selection - all of which are preserved correctly during translation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Source ARB file (English)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@@locale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@@last_modified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-15T10:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"appTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My App"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@appTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The title of the application"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"welcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome, {name}!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@welcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome message shown on the home screen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"placeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"example"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"unreadMessages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{count, plural, =0{No unread messages} =1{1 unread message} other{{count} unread messages}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@unreadMessages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Number of unread messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"placeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Translated ARB file (French)
&lt;/h3&gt;

&lt;p&gt;After translation, UI strings are localized while metadata structure remains intact. l10n.dev updates @@locale and @@last_modified automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@@locale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@@last_modified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-15T10:30:01Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"appTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mon Application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@appTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The title of the application"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"welcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bienvenue, {name} !"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@welcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome message shown on the home screen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"placeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"example"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"unreadMessages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{count, plural, =0{Aucun message non lu} =1{1 message non lu} other{{count} messages non lus}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@unreadMessages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Number of unread messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"placeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  File Naming Conventions
&lt;/h2&gt;

&lt;p&gt;ARB files follow a straightforward naming pattern: a prefix (by default app_) followed by the locale code and the .arb extension. Flutter and l10n.dev use underscores to separate language and region codes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ARB files use underscores, not hyphens - write app_en_US.arb and app_zh_CN.arb, not app_en-US.arb or app_zh-CN.arb.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Default pattern (recommended)
app_en.arb
app_fr.arb
app_en_US.arb          # Locale with region (underscore format)
app_zh_CN.arb          # Chinese Simplified

# Custom prefix patterns also supported
my_app_en.arb
my_app_fr.arb

# Note: ARB files use underscores, not hyphens
# ✓  app_en_US.arb
# ✗  app_en-US.arb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;Organize all your ARB files inside a dedicated l10n folder within lib/. Flutter's code generator picks them up automatically based on the arb-dir setting in l10n.yaml, and outputs ready-to-use Dart classes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lib/
├── l10n/
│   ├── app_en.arb        ← Source (English)
│   ├── app_fr.arb        ← French
│   ├── app_de.arb        ← German
│   ├── app_ja.arb        ← Japanese
│   ├── app_zh_CN.arb     ← Chinese Simplified
│   └── app_es.arb        ← Spanish
├── main.dart
└── ...

# Generated output (do not edit manually)
.dart_tool/
└── flutter_gen/
    └── gen_l10n/
        └── app_localizations.dart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Initialization
&lt;/h2&gt;

&lt;p&gt;Wire up the generated Dart localization class with MaterialApp by providing the localizationsDelegates and supportedLocales. Flutter will then resolve the correct ARB data for the device locale.&lt;/p&gt;

&lt;h3&gt;
  
  
  MaterialApp Setup
&lt;/h3&gt;

&lt;p&gt;Pass the four required delegates - your generated AppLocalizations.delegate plus the three Flutter SDK delegates - and list every locale your app supports. Use AppLocalizations.of(context)! anywhere in the widget tree to access translated strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_localizations/flutter_localizations.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_gen/gen_l10n/app_localizations.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="s"&gt;'My App'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

      &lt;span class="c1"&gt;// Required delegates&lt;/span&gt;
      &lt;span class="nl"&gt;localizationsDelegates:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;AppLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;GlobalMaterialLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;GlobalWidgetsLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;GlobalCupertinoLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;

      &lt;span class="c1"&gt;// Supported locales&lt;/span&gt;
      &lt;span class="nl"&gt;supportedLocales:&lt;/span&gt; &lt;span class="n"&gt;AppLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supportedLocales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

      &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Use in a widget&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomePage&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;l10n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AppLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l10n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appTitle&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l10n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;welcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Language Switching at Runtime
&lt;/h2&gt;

&lt;p&gt;Flutter allows you to switch the app locale at runtime by updating the locale property on MaterialApp. A common pattern is to hold a Locale in a StatefulWidget or a state management solution and expose a callback to change it. The widget tree automatically rebuilds with the new locale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Manage locale state at the app level&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_MyAppState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_MyAppState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Locale&lt;/span&gt; &lt;span class="n"&gt;_locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'en'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_changeLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Locale&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;locale:&lt;/span&gt; &lt;span class="n"&gt;_locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;localizationsDelegates:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;AppLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;GlobalMaterialLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;GlobalWidgetsLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;GlobalCupertinoLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="nl"&gt;supportedLocales:&lt;/span&gt; &lt;span class="n"&gt;AppLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supportedLocales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="n"&gt;SettingsPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;onLocaleChange:&lt;/span&gt; &lt;span class="n"&gt;_changeLocale&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Language selector widget&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LanguageSelector&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;onLocaleChange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LanguageSelector&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onLocaleChange&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DropdownButton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;value:&lt;/span&gt; &lt;span class="n"&gt;Localizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;localeOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;items:&lt;/span&gt; &lt;span class="n"&gt;AppLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supportedLocales&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DropdownMenuItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;value:&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLanguageTag&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
              &lt;span class="p"&gt;))&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nl"&gt;onChanged:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;onLocaleChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pluralization
&lt;/h2&gt;

&lt;p&gt;Flutter uses ICU message syntax for pluralization inside ARB files. The intl package handles all CLDR plural categories, automatically applying the correct form based on the target language rules.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use {count, plural, =0{...} =1{...} other{...}} syntax inside ARB string values.&lt;/li&gt;
&lt;li&gt;Declare the count placeholder with type int in the &lt;code&gt;@key&lt;/code&gt; metadata.&lt;/li&gt;
&lt;li&gt;l10n.dev generates all required plural forms for each target language - including complex languages such as Arabic, Russian, and Polish - without manual intervention.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cartItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{count, plural, =0{Your cart is empty} =1{1 item in your cart} other{{count} items in your cart}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@cartItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cart item count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"placeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"daysLeft"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{days, plural, =1{1 day left} other{{days} days left}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@daysLeft"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Days remaining"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"placeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Full ARB Support in l10n.dev
&lt;/h2&gt;

&lt;p&gt;l10n.dev is purpose-built to work seamlessly with Flutter's ARB workflow. Upload your source ARB file and receive accurate, correctly-structured translations back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Metadata Updates&lt;/strong&gt;: @@locale is updated to the target language code and @@last_modified is set to the current UTC timestamp automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata Translation Control&lt;/strong&gt;: By default, &lt;code&gt;@key&lt;/code&gt; metadata entries (description, example, context) are NOT translated and remain unchanged, keeping translator notes intact. Enable the translateMetadata setting to translate them alongside UI strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom File Prefixes&lt;/strong&gt;: Any naming pattern is supported - app_en.arb, my_app_en_US.arb, strings_fr.arb, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Underscore Locale Format&lt;/strong&gt;: ARB locale codes use underscores (en_US, zh_CN) instead of hyphens - l10n.dev handles this correctly so generated files drop straight into your Flutter project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pluralization Aware&lt;/strong&gt;: ICU plural forms are generated for every target language according to CLDR rules, so no manual plural editing is needed after translation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automate Translations with npm
&lt;/h2&gt;

&lt;p&gt;Use the ai-l10n npm package to translate your source ARB file from the command line or as part of a CI/CD pipeline. Install once, then translate to any number of languages in a single command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the CLI&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;ai-l10n

&lt;span class="c"&gt;# Translate your source ARB to multiple languages&lt;/span&gt;
npx ai-l10n translate lib/l10n/app_en.arb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--languages&lt;/span&gt; fr,de,ja,zh_CN,es,ko
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integration into a Flutter Build
&lt;/h3&gt;

&lt;p&gt;You can wire ai-l10n directly into your Flutter build process so translations are always up to date before the app is compiled. Two common approaches are a package.json script or a Makefile.&lt;/p&gt;

&lt;h4&gt;
  
  
  Via package.json scripts
&lt;/h4&gt;

&lt;p&gt;Add a translate script and use npm's prebuild lifecycle hook to run it automatically before every build. Run npm run build:android (or build:ios / build:web) and ai-l10n will translate first, then hand off to Flutter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"translate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ai-l10n translate lib/l10n/app_en.arb --languages fr,de,ja,zh_CN,es,ko --update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prebuild"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run translate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:android"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flutter build apk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:ios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flutter build ios"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:web"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flutter build web"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Via Makefile
&lt;/h4&gt;

&lt;p&gt;If your team uses Make, declare a translate target and make each build target depend on it. Running make build-android (or build-ios / build-all) will translate all target languages before invoking flutter build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;LANGUAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; fr,de,ja,zh_CN,es,ko

&lt;span class="nl"&gt;translate&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    npx ai-l10n translate lib/l10n/app_en.arb &lt;span class="nt"&gt;--languages&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;LANGUAGES&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--update&lt;/span&gt;

&lt;span class="nl"&gt;build-android&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;translate&lt;/span&gt;
    flutter build apk

&lt;span class="nl"&gt;build-ios&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;translate&lt;/span&gt;
    flutter build ios

&lt;span class="nl"&gt;build-web&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;translate&lt;/span&gt;
    flutter build web

&lt;span class="nl"&gt;build-all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;translate&lt;/span&gt;
    flutter build apk
    flutter build ios
    flutter build web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For CI/CD integration, incremental updates, batch translation across multiple files, and GitHub Actions workflow examples, see the &lt;a href="https://l10n.dev/help/localization-automation" rel="noopener noreferrer"&gt;Localization Automation Guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  VS Code Extension
&lt;/h2&gt;

&lt;p&gt;The l10n.dev VS Code extension brings Flutter ARB translation directly into your editor. Right-click any ARB file and translate it to your target languages without leaving VS Code. Get the &lt;a href="https://marketplace.visualstudio.com/items?itemName=l10n-dev.translate-i18n-json" rel="noopener noreferrer"&gt;VS Code Extension for ARB files translation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works in VS Code:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open your source ARB file (e.g., app_en.arb) in the editor.&lt;/li&gt;
&lt;li&gt;Right-click in the editor and select "Translate JSON".&lt;/li&gt;
&lt;li&gt;Choose one or more target languages (e.g., French, Japanese, German).&lt;/li&gt;
&lt;li&gt;The extension creates localized ARB files alongside your source, with @@locale and @@last_modified updated automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  See It in Action
&lt;/h3&gt;

&lt;p&gt;Here's a live example of translating a Flutter ARB file to Uzbek inside VS Code:&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2F8o1sokue70la2wxvd4bf.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.amazonaws.com%2Fuploads%2Farticles%2F8o1sokue70la2wxvd4bf.gif" alt="Translating a Flutter ARB file inside VS Code"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ready to reach global users? You can &lt;a href="https://l10n.dev/ws/translate-i18n-files" rel="noopener noreferrer"&gt;translate ARB files directly in the l10n.dev workspace&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/ai-l10n" rel="noopener noreferrer"&gt;automate translation with the npm CLI&lt;/a&gt;, or translate right inside VS Code. Thanks for reading!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>mobile</category>
      <category>automation</category>
    </item>
    <item>
      <title>Pseudo-localization for Automated i18n Testing</title>
      <dc:creator>Anton Antonov</dc:creator>
      <pubDate>Tue, 23 Dec 2025 10:02:31 +0000</pubDate>
      <link>https://forem.com/anton_antonov/pseudo-localization-for-automated-i18n-testing-31</link>
      <guid>https://forem.com/anton_antonov/pseudo-localization-for-automated-i18n-testing-31</guid>
      <description>&lt;p&gt;Pseudo-localization is a powerful testing technique that transforms your source text into a fake language to identify internationalization (i18n) issues before actual translation begins. This guide shows you how to automate pseudo-localization testing using the &lt;a href="https://www.npmjs.com/package/pseudo-l10n" rel="noopener noreferrer"&gt;pseudo-l10n&lt;/a&gt; npm package.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Pseudo-localization?
&lt;/h2&gt;

&lt;p&gt;Pseudo-localization is the process of transforming your application’s source text into an altered, fake language that mimics how UI behaves after translation. It helps QA engineers and developers identify i18n issues early in the development cycle.&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.amazonaws.com%2Fuploads%2Farticles%2Fi3mfgxso81vjt39qh97g.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2Fi3mfgxso81vjt39qh97g.jpg" alt="Example of using pseudo-localization" width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example of using pseudo-localization to identify potential internationalization issues. The font and size are identical on both sides, but supporting other scripts often requires more space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Pseudo-localization is Useful
&lt;/h2&gt;

&lt;p&gt;Pseudo-localization helps you catch i18n issues early:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exposes layout breakages caused by text expansion (German and Finnish are typically 30–40% longer than English).&lt;/li&gt;
&lt;li&gt;Surfaces encoding issues by adding accented characters to test UTF-8 support.&lt;/li&gt;
&lt;li&gt;Ensures all strings are translatable — hard-coded text remains unchanged and is easy to spot.&lt;/li&gt;
&lt;li&gt;Reveals placeholder mishandling — dynamic content like {{name}} must be preserved.&lt;/li&gt;
&lt;li&gt;Helps define translation length limits for UI components with space constraints.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automation Strategies for QA
&lt;/h2&gt;

&lt;p&gt;Here are proven strategies to automate i18n testing with pseudo-localization:&lt;/p&gt;

&lt;h3&gt;
  
  
  Accented &amp;amp; Non-Latin Characters
&lt;/h3&gt;

&lt;p&gt;Replace Latin letters with accented forms or different scripts to test character encoding and font support.&lt;br&gt;
&lt;strong&gt;Example:&lt;/strong&gt; “Save” → “Šàvē”&lt;br&gt;
&lt;strong&gt;QA check:&lt;/strong&gt; Ensure all characters display correctly and nothing breaks due to encoding issues.&lt;/p&gt;
&lt;h3&gt;
  
  
  Text Expansion Simulation
&lt;/h3&gt;

&lt;p&gt;Automatically expand each string by ~30–40% to mimic long languages like German or Finnish. Wrap with visual markers for easy clipping detection.&lt;br&gt;
&lt;strong&gt;Example:&lt;/strong&gt; “Save” → ⟦Šàvēēēēē⟧&lt;br&gt;
&lt;strong&gt;QA check:&lt;/strong&gt; Use automated screenshot comparison to spot UI overflow, clipping, or misalignment.&lt;/p&gt;
&lt;h3&gt;
  
  
  Placeholder Stress Testing
&lt;/h3&gt;

&lt;p&gt;Replace interpolation variables (placeholders) with visible markers to verify they’re preserved during translation.&lt;br&gt;
&lt;strong&gt;Example:&lt;/strong&gt; “You have {{count}} items” → “You have  items”&lt;br&gt;
&lt;strong&gt;QA check:&lt;/strong&gt; Run regression tests; fail if a marker is missing or incorrectly escaped (&amp;lt;COUNT&amp;gt;).&lt;/p&gt;
&lt;h3&gt;
  
  
  RTL Simulation
&lt;/h3&gt;

&lt;p&gt;Wrap text in right-to-left (RTL) markers using Unicode control characters to simulate Arabic or Hebrew.&lt;br&gt;
&lt;strong&gt;QA check:&lt;/strong&gt; Verify alignment, text direction, and mirroring are correct for RTL languages.&lt;/p&gt;
&lt;h3&gt;
  
  
  CI/CD Integration
&lt;/h3&gt;

&lt;p&gt;Add pseudo-localization to your automated test pipeline to catch i18n issues before they reach production.&lt;br&gt;
&lt;strong&gt;QA check:&lt;/strong&gt; Block deployment if tests detect missing translations, broken placeholders, or layout issues.&lt;/p&gt;
&lt;h2&gt;
  
  
  Automating with pseudo-l10n Package
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.npmjs.com/package/pseudo-l10n" rel="noopener noreferrer"&gt;pseudo-l10n&lt;/a&gt; npm package automates pseudo-localization for your JSON translation files, making it easy to integrate i18n testing into your development workflow.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Text Expansion: Simulates how translated text is often 30–40% longer than English.&lt;/li&gt;
&lt;li&gt;Accented Characters: Tests UTF-8 encoding and font support with accented equivalents.&lt;/li&gt;
&lt;li&gt;Visual Markers: Wraps strings with ⟦…⟧ markers to easily spot untranslated or truncated text.&lt;/li&gt;
&lt;li&gt;Placeholder Handling: Preserves placeholders like {{name}}, {count}, %key%, etc.&lt;/li&gt;
&lt;li&gt;RTL Simulation: Simulates Right-to-Left languages using Unicode control characters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;Install pseudo-l10n globally for command-line usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g pseudo-l10n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add it as a development dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install --save-dev pseudo-l10n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic Usage
&lt;/h3&gt;

&lt;p&gt;Transform your source translation file into a pseudo-localized version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pseudo-l10n input.json output.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example Transformation&lt;/strong&gt;&lt;br&gt;
Input (en.json):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "welcome": "Welcome to our application",
  "greeting": "Hello, {{name}}!",
  "itemCount": "You have {{count}} items"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output (pseudo-en.json):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "welcome": "⟦Ŵëļçõɱë ţõ õür àƥƥļïçàţïõñēēēēēēēēēēēēēēēēēē⟧",
  "greeting": "⟦Ĥëļļõēēēēēē, {{name}}!ēēēēē⟧",
  "itemCount": "⟦Ŷõü ĥàṽë {{count}} ïţëɱšēēēēēēēēēēēēēēēē⟧"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Advanced Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Custom Expansion&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pseudo-l10n en.json pseudo-en.json --expansion=30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RTL Simulation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Simulate right-to-left languages like Arabic or Hebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pseudo-l10n en.json pseudo-en.json --replace-placeholders

# Input:  { "greeting": "Hello, {{name}}!" }
# Output: { "greeting": "⟦Ĥëļļõēēēēēē, &amp;lt;NAME&amp;gt;!ēēēēē⟧" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Support for Different Placeholder Formats&lt;/strong&gt;&lt;br&gt;
The package supports various placeholder formats used by different i18n libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# For i18next (default)
pseudo-l10n en.json pseudo-en.json --placeholder-format="{{key}}"

# For Angular/React Intl
pseudo-l10n en.json pseudo-en.json --placeholder-format="{key}"

# For sprintf style
pseudo-l10n en.json pseudo-en.json --placeholder-format="%key%"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Programmatic Usage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use pseudo-l10n programmatically in your Node.js scripts or build process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { generatePseudoLocaleSync, pseudoLocalize } = require('pseudo-l10n');

// Generate a pseudo-localized JSON file
generatePseudoLocaleSync('en.json', 'pseudo-en.json', {
  expansion: 40,
  rtl: false
});
// Pseudo-localize a single string
const result = pseudoLocalize('Hello, {{name}}!');
console.log(result);
// Output: ⟦Ĥëļļõēēēēēēēēēēēēēē, {{name}}!ēēēēē⟧
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integration into Your Workflow
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;npm Scripts&lt;/strong&gt;&lt;br&gt;
Add pseudo-localization generation to your package.json scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "scripts": {
    "pseudo": "pseudo-l10n src/locales/en.json src/locales/pseudo-en.json",
    "pseudo:rtl": "pseudo-l10n src/locales/en.json src/locales/pseudo-ar.json --rtl"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Build Process Integration&lt;/strong&gt;&lt;br&gt;
Generate pseudo-locales as part of your build process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// build.js
const { generatePseudoLocaleSync } = require('pseudo-l10n');

// Generate pseudo-locales as part of build
generatePseudoLocaleSync(
  './src/locales/en.json',
  './src/locales/pseudo-en.json',
  { expansion: 40 }
);
generatePseudoLocaleSync(
  './src/locales/en.json',
  './src/locales/pseudo-ar.json',
  { rtl: true }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CI/CD Pipeline Integration&lt;/strong&gt;&lt;br&gt;
Integrate pseudo-localization into your continuous integration pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .github/workflows/test.yml
- name: Generate pseudo-locales
  run: |
    npm install -g pseudo-l10n
    pseudo-l10n src/locales/en.json src/locales/pseudo-en.json

- name: Run i18n tests
  run: npm run test:i18n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing Strategy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Generate pseudo-locale during your build process.&lt;/li&gt;
&lt;li&gt;Add pseudo-locale to your application (e.g., in the language selector).&lt;/li&gt;
&lt;li&gt;Test your application with pseudo-locale enabled.&lt;/li&gt;
&lt;li&gt;Review the application for common i18n issues.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What to Look For&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing ⟦⟧ markers = untranslated strings (hard-coded text).&lt;/li&gt;
&lt;li&gt;Cut-off markers = text truncation or UI overflow.&lt;/li&gt;
&lt;li&gt;Broken layout = insufficient space for text expansion.&lt;/li&gt;
&lt;li&gt;Garbled text = encoding issues or missing font support.&lt;/li&gt;
&lt;li&gt;Wrong text direction = RTL problems (for RTL pseudo-locales).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pseudo-localization is an essential testing technique that helps you catch internationalization issues before they reach production. By automating pseudo-localization testing with the pseudo-l10n package, you can ensure your application is truly ready for global audiences.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Testing to Real Translation
&lt;/h2&gt;

&lt;p&gt;Once you’ve validated your i18n implementation with pseudo-localization, it’s time to translate your application for real users. This is where AI-powered translation services like &lt;a href="https://l10n.dev" rel="noopener noreferrer"&gt;l10n&lt;/a&gt;.dev come in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why l10n.dev for Production Translation
&lt;/h3&gt;

&lt;p&gt;After ensuring your app handles internationalization correctly with pseudo-localization, use l10n.dev for professional AI-powered translation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preserves placeholders, format, and structure — just like pseudo-l10n protects during testing.&lt;/li&gt;
&lt;li&gt;Supports 165 languages with context-aware AI translation.&lt;/li&gt;
&lt;li&gt;Handles plural forms, interpolation, and special characters automatically.&lt;/li&gt;
&lt;li&gt;Provides CI/CD integration via API, npm package &lt;a href="https://www.npmjs.com/package/ai-l10n" rel="noopener noreferrer"&gt;ai-l10n&lt;/a&gt;, or &lt;a href="https://github.com/marketplace/actions/l10n-dev-ai-localization-automation" rel="noopener noreferrer"&gt;GitHub Action for automated localization&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Offers &lt;a href="https://marketplace.visualstudio.com/items?itemName=l10n-dev.translate-i18n-json" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt; and web UI for manual &lt;a href="https://l10n.dev/ws/translate-i18n-files" rel="noopener noreferrer"&gt;i18n translation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Complete i18n Workflow
&lt;/h3&gt;

&lt;p&gt;Test your i18n implementation with pseudo-l10n to catch issues early.&lt;br&gt;
Fix any layout, encoding, or placeholder issues discovered during testing.&lt;br&gt;
Translate i18n files using l10n.dev for production-ready translations.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
      <category>frontend</category>
      <category>automation</category>
    </item>
    <item>
      <title>A Cleaner Way to Do i18n in Angular: i18next + Type Safety + Dynamic JSON Imports</title>
      <dc:creator>Anton Antonov</dc:creator>
      <pubDate>Tue, 16 Dec 2025 10:01:53 +0000</pubDate>
      <link>https://forem.com/anton_antonov/a-cleaner-way-to-do-i18n-in-angular-i18next-type-safety-dynamic-json-imports-3p67</link>
      <guid>https://forem.com/anton_antonov/a-cleaner-way-to-do-i18n-in-angular-i18next-type-safety-dynamic-json-imports-3p67</guid>
      <description>&lt;p&gt;Internationalization (i18n) is essential for building global Angular applications. This guide shows you how to implement i18next in Angular with proper resource loading, initialization, and usage patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why i18next for Angular?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.i18next.com/" rel="noopener noreferrer"&gt;i18next&lt;/a&gt; is one of the most popular i18n frameworks, offering powerful features and flexibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Framework agnostic — works seamlessly with Angular, React, Vue, and others.&lt;/li&gt;
&lt;li&gt;Powerful interpolation, pluralization, and context support.&lt;/li&gt;
&lt;li&gt;Lazy loading and code splitting support.&lt;/li&gt;
&lt;li&gt;TypeScript support with type safety.&lt;/li&gt;
&lt;li&gt;Large ecosystem with plugins and extensions.&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2F335at40ifngv1wndaoyb.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2F335at40ifngv1wndaoyb.jpg" alt=" " width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Install i18next using npm or yarn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install i18next
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it! i18next is a standalone library with no framework-specific dependencies. You don’t need angular-i18next or any other wrapper — just use i18next directly in your Angular application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading Translation Resources
&lt;/h2&gt;

&lt;p&gt;There are two main approaches to loading translation resources in Angular with i18next:&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Imports (Recommended)
&lt;/h3&gt;

&lt;p&gt;Using dynamic imports leverages Webpack’s code splitting to load translations on-demand. This approach is ideal for applications with many languages or large translation files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bundle size optimization — only load needed translations.&lt;/li&gt;
&lt;li&gt;Better performance — faster initial load time.&lt;/li&gt;
&lt;li&gt;Works perfectly with SSR and prerendering.&lt;/li&gt;
&lt;li&gt;No additional HTTP requests required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: Loading with Dynamic Imports&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private async loadLanguageResources(
  lang: string,
): Promise&amp;lt;Record&amp;lt;string, Record&amp;lt;string, unknown&amp;gt;&amp;gt;&amp;gt; {
  const languageResources: Record&amp;lt;string, Record&amp;lt;string, unknown&amp;gt;&amp;gt; = {};

  // Load all namespaces for the given language in parallel
  await Promise.all(
    NAMESPACES.map(async (namespace) =&amp;gt; {
      const translationModule = await this.loadJson(lang, namespace);
      languageResources[namespace] = translationModule.default;
    }),
  );
  return languageResources;
}

private async loadJson(
  language: string,
  namespace: string,
): Promise&amp;lt;{ default: Record&amp;lt;string, unknown&amp;gt; }&amp;gt; {
  // Webpack automatically code-splits this into separate chunks
  return import(`../../i18n/${language}/${namespace}.json`);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach imports translation files as ES modules, allowing Webpack to automatically code-split them into separate chunks that are loaded on demand.&lt;/p&gt;

&lt;h3&gt;
  
  
  i18next-http-backend
&lt;/h3&gt;

&lt;p&gt;The HTTP backend plugin loads translations via HTTP requests from a server or CDN. This is useful when you want to update translations without rebuilding your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update translations without redeployment.&lt;/li&gt;
&lt;li&gt;Load translations from external CDN.&lt;/li&gt;
&lt;li&gt;Useful for runtime translation management.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Additional HTTP requests slow down initial load.&lt;/li&gt;
&lt;li&gt;Requires network connectivity.&lt;/li&gt;
&lt;li&gt;More complex SSR setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: Loading with HTTP Backend&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import HttpBackend from 'i18next-http-backend';

await init({
  lng: initialLanguage,
  fallbackLng: DEFAULT_LANGUAGE,
  backend: {
    loadPath: '/assets/i18n/{{lng}}/{{ns}}.json',
  },
  use: [HttpBackend],
  ns: NAMESPACES,
  defaultNS: DEFAULT_NAMESPACE,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach fetches translation files from a server at runtime. Note that this requires additional configuration for SSR.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Approach to Choose?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use Dynamic Imports when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want optimal bundle size and performance.&lt;/li&gt;
&lt;li&gt;Translations are part of your build process.&lt;/li&gt;
&lt;li&gt;You need SSR/prerendering support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use HTTP Backend when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to update translations without redeployment.&lt;/li&gt;
&lt;li&gt;Translations are managed externally.&lt;/li&gt;
&lt;li&gt;You have a CDN for serving translation files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Proper i18next Initialization
&lt;/h2&gt;

&lt;p&gt;Initializing i18next correctly is crucial for avoiding runtime errors and ensuring translations are available when your app renders.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using provideAppInitializer
&lt;/h3&gt;

&lt;p&gt;Angular’s provideAppInitializer function (introduced in Angular 18) ensures i18next is fully initialized before the application starts rendering. The provided function is executed during app bootstrap, and initialization does not complete until the Promise resolves. This prevents translation keys from appearing instead of translated text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: App Initializer Configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const appConfig: ApplicationConfig = {
  providers: [
    // ... other providers
    provideAppInitializer(initializeI18n),
    // ... more providers
  ],
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why This Matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The function runs in the injection context during application startup.&lt;/li&gt;
&lt;li&gt;Prevents flickering or showing translation keys on initial load.&lt;/li&gt;
&lt;li&gt;Ensures translations are available in all components from the start.&lt;/li&gt;
&lt;li&gt;Works correctly with SSR, prerendering, and client-side rendering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  i18next Initialization Function
&lt;/h3&gt;

&lt;p&gt;The initialization function handles loading resources and configuring i18next settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: I18next Initialization&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public async initializeI18n(): Promise&amp;lt;void&amp;gt; {
  const initialLanguage = this.getInitialLanguage();
  const resources: Record&amp;lt;string, Record&amp;lt;string, Record&amp;lt;string, unknown&amp;gt;&amp;gt;&amp;gt; = {};

  if (isPlatformBrowser(this.platformId)) {
    // Browser: Only load the specific language needed
    const languageResources = await this.loadLanguageResources(initialLanguage);
    resources[initialLanguage] = languageResources;
  } else {
    // SSR: Load all languages for prerendering
    for (const lang of SUPPORTED_LANGUAGES) {
      const languageResources = await this.loadLanguageResources(lang);
      resources[lang] = languageResources;
    }
  }

  await init({
    lng: initialLanguage,
    fallbackLng: DEFAULT_LANGUAGE,
    resources,
    ns: NAMESPACES,
    defaultNS: DEFAULT_NAMESPACE,
    interpolation: {
      escapeValue: false,
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load only the current language in browser for optimal performance.&lt;/li&gt;
&lt;li&gt;Load all languages during SSR for prerendering support.&lt;/li&gt;
&lt;li&gt;Use fallbackLng to handle missing translations gracefully.&lt;/li&gt;
&lt;li&gt;Configure namespaces for better organization.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Translation Pipe
&lt;/h2&gt;

&lt;p&gt;The Angular translation pipe provides a clean way to use translations in templates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pipe Implementation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Example: Translation Pipe&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Pipe, PipeTransform } from '@angular/core';
import { t } from 'i18next';

@Pipe({
  name: 't',
  standalone: true,
  pure: false, // Need to update when language changes
})
export class TranslatePipe implements PipeTransform {
  transform(key: string, options?: Record&amp;lt;string, unknown&amp;gt;): string {
    return t(key, options);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipe is marked as pure: false to ensure it updates when the language changes. This is important because language changes don’t modify the translation key itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the Translation Pipe
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;In Templates:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- With namespace --&amp;gt;
&amp;lt;p&amp;gt;{{ "common:welcomeMessage" | t }}&amp;lt;/p&amp;gt;

&amp;lt;!-- With nested keys --&amp;gt;
&amp;lt;p&amp;gt;{{ "dashboard.title" | t }}&amp;lt;/p&amp;gt;

&amp;lt;!-- Simple interpolation --&amp;gt;
&amp;lt;p&amp;gt;{{ "greeting" | t: { name: userName } }}&amp;lt;/p&amp;gt;
&amp;lt;!-- Result: "Hello, John!" from greeting: "Hello, {{name}}!" --&amp;gt;

&amp;lt;!-- With Angular pipes --&amp;gt;
&amp;lt;p&amp;gt;{{ "price" | t: { amount: 29.99 | currency } }}&amp;lt;/p&amp;gt;
&amp;lt;!-- Result: "Price: $29.99" from: "Price: {{amount}}" --&amp;gt;

&amp;lt;!-- Multiple variables --&amp;gt;
&amp;lt;p&amp;gt;{{ "updated" | t: { date: lastModified | date, user: currentUser } }}&amp;lt;/p&amp;gt;
&amp;lt;!-- Result: "Updated on Jan 15, 2024 by Alice" from: "Updated on {{date}} by {{user}}." --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interpolation allows you to insert dynamic values into your translations. You can pass variables, use Angular pipes for formatting, and combine multiple values in a single translation string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Component Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Component } from '@angular/core';
import { t } from 'i18next';

@Component({
  selector: 'app-dashboard',
  template: `
    &amp;lt;h1&amp;gt;{{ title }}&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;{{ welcomeMsg }}&amp;lt;/p&amp;gt;
  `,
})
export class DashboardComponent {
  title = t('dashboard.title');
  welcomeMsg = t('greeting', { name: 'John' });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices for Angular i18next
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use Meaningful Keys
&lt;/h3&gt;

&lt;p&gt;Use dot notation that reflects your app structure: ‘auth:login.title’ instead of ‘loginTitle’ or raw text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Organize with Namespaces
&lt;/h3&gt;

&lt;p&gt;Split translations into logical namespaces (e.g., common.json, auth.json, dashboard.json) for better maintainability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handle Pluralization
&lt;/h3&gt;

&lt;p&gt;Use i18next’s built-in pluralization support for handling singular/plural forms correctly in all languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wait for Initialization
&lt;/h3&gt;

&lt;p&gt;Always ensure i18next is initialized before rendering your app using provideAppInitializer() to run initialization during app bootstrap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Type Safety for Translation Keys
&lt;/h3&gt;

&lt;p&gt;Use TypeScript to extract translation keys directly from your JSON files for complete type safety. Instead of manually maintaining type definitions, leverage TypeScript’s typeof and dynamic imports to automatically generate types from your actual translation files.&lt;/p&gt;

&lt;p&gt;Define a type that extracts all possible keys from your JSON translation files using a recursive NestedKeys helper type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import common from './en/common.json'; // defaultNS: DEFAULT_NAMESPACE,
import auth from './en/auth.json'; // namespaces

type NestedKeys&amp;lt;T, Prefix extends string = ''&amp;gt; = T extends object
  ? {
      [K in keyof T &amp;amp; (string | number)]: T[K] extends object
        ?
            | NestedKeys&amp;lt;T[K], `${Prefix}${K &amp;amp; (string | number)}.`&amp;gt;
            | `${Prefix}${K &amp;amp; (string | number)}`
        : `${Prefix}${K &amp;amp; (string | number)}`;
    }[keyof T &amp;amp; (string | number)]
  : never;

export type TranslationKey =
  | `common:${NestedKeys&amp;lt;typeof common&amp;gt;}`
  | `auth:${NestedKeys&amp;lt;typeof auth&amp;gt;}`;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the pipe’s transform method to accept TranslationKey instead of string. This provides type safety in templates and ensures only valid translation keys can be used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Pipe, PipeTransform } from '@angular/core';
import { t } from 'i18next';

import { TranslationKey } from '../../i18n/translation-key';

@Pipe({
  name: 't',
  standalone: true,
  pure: false, // Need to update when language changes
})
export class TranslatePipe implements PipeTransform {
  transform(key: TranslationKey, options?: Record&amp;lt;string, unknown&amp;gt;): string {
    return t(key, options);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you get full IntelliSense autocomplete and compile-time errors for invalid keys in both TypeScript code and templates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t('common:greeting');       // ✅ Autocomplete
t('auth:login.title');      // ✅ Valid
t('invalid.key');           // ❌ IDE shows error

{{ "common:greeting" | t }} // ✅ Valid in templates
{{ "invalid.key" | t }}     // ❌ IDE shows error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits of Type-Safe Translation Keys:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Autocomplete in your IDE for all available translation keys across all namespaces.&lt;/li&gt;
&lt;li&gt;Compile-time errors if you use a non-existent key, catching typos before runtime.&lt;/li&gt;
&lt;li&gt;Refactoring support — rename keys safely across your entire codebase.&lt;/li&gt;
&lt;li&gt;Zero runtime overhead — types are completely removed during compilation.&lt;/li&gt;
&lt;li&gt;Auto-generated from your actual JSON files — no manual type maintenance required.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Language Detection and Switching
&lt;/h3&gt;

&lt;p&gt;Implement intelligent language detection and allow users to switch languages at runtime with automatic resource loading.&lt;/p&gt;

&lt;p&gt;Determine the user’s language to use when your app starts by checking multiple sources in priority order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private getInitialLanguage(): string {
  if (isPlatformBrowser(this.platformId)) {
    // Try to get language from localStorage (user preference)
    const savedLang = localStorage.getItem('language');
    if (savedLang) {
      return savedLang;
    }

    // Fall back to browser language
    return navigator.language ?? DEFAULT_LANGUAGE;
  }

  // Default language for SSR
  return DEFAULT_LANGUAGE;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach checks localStorage first (user preference), then falls back to the browser’s language (navigator.language), and finally defaults to the app’s default language.&lt;/p&gt;

&lt;p&gt;Switch languages at runtime while automatically loading translation resources on-demand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public async switchLanguage(
  language: string
): Promise&amp;lt;void&amp;gt; {
  // In browser, check if we need to load the translation first
  if (isPlatformBrowser(this.platformId)) {
    const i18next = await import('i18next');

    await Promise.all(
      NAMESPACES.map(async (namespace) =&amp;gt; {
        const currentResources = i18next.default.getResourceBundle(
          lang,
          namespace,
        );
        // Check if any namespace is missing for this language
        if (!currentResources) {
          // Translation not loaded yet, load it dynamically
          const translationModule = await this.loadJson(lang, namespace);
          i18next.default.addResourceBundle(
            lang,
            namespace,
            translationModule.default,
          );
        }
      }),
    );
  }

  // Switch to the new language
  await changeLanguage(lang);

  // Save preference to localStorage
  if (isPlatformBrowser(this.platformId)) {
    localStorage.setItem('language', lang);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using dynamic imports, translations are loaded on-demand. The changeLanguage() method checks if resources are already loaded using getResourceBundle(). If missing, it dynamically imports and adds them using addResourceBundle(). This prevents redundant network requests while ensuring all needed translations are available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Implementation Details:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check localStorage first to restore user’s preferred language.&lt;/li&gt;
&lt;li&gt;Use navigator.language as fallback for browser’s language preference.&lt;/li&gt;
&lt;li&gt;Load missing resources dynamically with addResourceBundle().&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automate Translation with AI
&lt;/h2&gt;

&lt;p&gt;Managing translations manually across multiple languages is time-consuming and error-prone. That’s where AI-powered localization comes in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why l10n.dev?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://l10n.dev" rel="noopener noreferrer"&gt;l10n&lt;/a&gt;.dev is an AI-powered translation service specifically designed for i18next JSON files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preserves JSON structure, keys, and nested objects.&lt;/li&gt;
&lt;li&gt;Maintains placeholders like {{name}}, {{count}} correctly.&lt;/li&gt;
&lt;li&gt;Automatically generates plural forms for all languages.&lt;/li&gt;
&lt;li&gt;Handles context and interpolation intelligently.&lt;/li&gt;
&lt;li&gt;Supports all i18next features including namespaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Simple Workflow
&lt;/h3&gt;

&lt;p&gt;Use &lt;a href="https://l10n.dev/#integrations" rel="noopener noreferrer"&gt;l10n integrations&lt;/a&gt;. The &lt;a href="https://www.npmjs.com/package/ai-l10n" rel="noopener noreferrer"&gt;ai-l10n&lt;/a&gt; npm package brings AI-powered translation directly into your development workflow with intelligent automation features (SDK, CLI, Github Actions).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Save hours of manual translation work and avoid common errors like broken placeholders or incorrect plural forms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>angular</category>
      <category>frontend</category>
      <category>typescript</category>
    </item>
    <item>
      <title>i18n Testing — A Practical Guide for QA Engineers</title>
      <dc:creator>Anton Antonov</dc:creator>
      <pubDate>Tue, 09 Dec 2025 11:24:29 +0000</pubDate>
      <link>https://forem.com/anton_antonov/i18n-testing-a-practical-guide-for-qa-engineers-f7h</link>
      <guid>https://forem.com/anton_antonov/i18n-testing-a-practical-guide-for-qa-engineers-f7h</guid>
      <description>&lt;p&gt;Building a global-ready app starts with internationalization (i18n) — but testing ensures it stays that way across languages. Let’s walk through why thoughtful i18n testing matters and how to make it effective.&lt;/p&gt;

&lt;p&gt;Developers often focus on building features and adding i18n. But QA engineers play a critical role in ensuring a product works flawlessly for users across the globe.&lt;/p&gt;

&lt;p&gt;In my previous article on &lt;a href="https://dev.to/anton_antonov/i18n-vs-l10n-why-developers-should-care-and-how-ai-can-help-18i8"&gt;i18n vs l10n&lt;/a&gt;, I explained why proper internationalization matters and how AI can help with localization. In this one, we’ll go deeper into what QA should focus on. In the examples below, I use i18next because it’s one of the most popular and feature-rich localization frameworks for JavaScript, making it easy to demonstrate concepts like pluralization and interpolation. Even if your project uses a different format (not JSON), you should still ensure it supports at least plural forms and interpolation — these are essential for providing robust internationalization capabilities. Here’s your checklist to guarantee quality:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. All User-Facing Strings Are Extracted for Translation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Switch the app to a test locale (e.g., pseudo-language) and verify that all user-visible text appears translated, with no hard-coded or untranslated strings left.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Plural Form Coverage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Check that all required plural keys (_one, _few, _many, _other) are present — particularly for complex-plural languages like Polish, Russian, or Arabic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my previous article, I noted that developers often define only _one and _other for English, and localization teams may not add the rest. With &lt;strong&gt;l10n.dev&lt;/strong&gt;, extra forms are auto-added during &lt;a href="https://l10n.dev/ws/translate-i18n-file" rel="noopener noreferrer"&gt;i18next file translation&lt;/a&gt;, ensuring full support for target languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Placeholder Protection (interpolation)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Confirm that dynamic placeholders ({{name}}, {{count}}, etc.) remain untouched during translation to avoid runtime issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;l10n.dev preserves placeholders and understands their meaning for safer translations.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Date, Number &amp;amp; Currency Formats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Ensure locale-aware formatting: e.g., 1,234.56 (US) vs 1.234,56 (Germany), 24-hour vs 12-hour clock, different first days of the week.&lt;/li&gt;
&lt;li&gt;Check currencies are correct for the target market — they should not be changed during translation.&lt;/li&gt;
&lt;li&gt;Verify date/time accuracy across all relevant time zones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As highlighted earlier, &lt;strong&gt;l10n.dev&lt;/strong&gt; intelligently converts formats — for example, turning “Founded on July 4, 1776” into “1776年7月4日創設” for Japanese locale.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Text Length &amp;amp; UI Layout
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Some languages (like German or Russian) tend to be 30–40% longer than English. Your UI should adapt without truncation. Simulate long strings and ensure buttons, labels, and menus resize gracefully.&lt;/li&gt;
&lt;li&gt;Languages like Arabic and Hebrew are read right-to-left (RTL). Verify that text alignment, icons, and navigation reverse correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a translation is longer than the source, AI &lt;a href="//l10n.dev/ws/translate-i18n-file"&gt;i18n translator&lt;/a&gt; can intelligently shorten it without losing context. Example: “administrator” → “admin” in space-limited UI components.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Missing Keys &amp;amp; Fallbacks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Run localized build and click through screens to confirm no fallback keys (like auth.login.title) appear accidentally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;l10n.dev&lt;/strong&gt; preserves translation files structure and key ordering for cleaner diffs and easier review.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Contextual Variants
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Ensure that gender (e.g., _male, _female), tone (formal/informal), or role-based translations are properly used. Keys like greeting_formal vs greeting_informal should render correctly based on context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. Encoding and Character Support
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;App should handle all Unicode characters — including emojis, right-to-left scripts, and accented letters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automating i18n Testing with Pseudo-Localization
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/pseudo-l10n" rel="noopener noreferrer"&gt;&lt;strong&gt;Pseudo-localization&lt;/strong&gt;&lt;/a&gt; is the process of transforming your app’s source text into an altered, fake “language” to mimic how UI behaves after translation. Automation QA teams can use this to detect i18n issues before the real translation arrives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why It’s Useful&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exposes layout breakages caused by text expansion.&lt;/li&gt;
&lt;li&gt;Surfaces encoding issues by adding accented characters.&lt;/li&gt;
&lt;li&gt;Ensures all strings are translatable (no hard-coded text).&lt;/li&gt;
&lt;li&gt;Reveals placeholder mishandling.&lt;/li&gt;
&lt;li&gt;Helps define translation length limits (e.g., maximum allowed characters).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automation Strategies for QA
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Accented &amp;amp; Non-Latin Characters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What to do&lt;/strong&gt;: Replace Latin letters with accented forms or different scripts.
Example: “Save” → “Šàvē”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA check&lt;/strong&gt;: Ensure all characters display correctly and nothing breaks due to encoding.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Text Expansion Simulation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What to do&lt;/strong&gt;: Automatically expand each string by ~30–40% to mimic long languages like German or Finnish. Wrap with markers for easy clipping detection.
Example: “Save” → [★ Šàvēēēēē ★]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA check&lt;/strong&gt;: Use automated screenshot comparison to spot UI overflow, clipping, or misalignment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Placeholder Stress Testing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What to do&lt;/strong&gt;: Replace interpolation variables (placeholders) with visible markers (e.g., , ).
Example: “You have {{count}} items” → “You have  items”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA check&lt;/strong&gt;: Run regression tests; fail if a marker is missing, or escaped (&amp;lt;COUNT&amp;gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. RTL Simulation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What to do&lt;/strong&gt;: Wrap text in right-to-left (RTL) markers \u202E${text}\u202C to simulate Arabic or Hebrew.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA check&lt;/strong&gt;: Verify alignment, text direction, and mirroring are correct.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. CI/CD Integration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What to do&lt;/strong&gt;: Add pseudo-localization to your automated test pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA check&lt;/strong&gt;: Block deployment if tests detect missing translations, broken placeholders, or layout issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below the screenshot is a &lt;strong&gt;pseudo-localization example&lt;/strong&gt; used to identify potential internationalization (i18n) issues. The font and size are identical on both sides, but supporting other scripts often requires more space:&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.amazonaws.com%2Fuploads%2Farticles%2Fgzc9rsp38bof2tbzpqnt.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fgzc9rsp38bof2tbzpqnt.webp" alt="Example of using pseudo-localization" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To automate pseudo-localization, check out &lt;a href="https://www.npmjs.com/package/pseudo-l10n" rel="noopener noreferrer"&gt;pseudo-l10n&lt;/a&gt; — an npm package that generates pseudo-localized JSON files with configurable text expansion, accented characters, visual markers, and RTL support. Install with &lt;code&gt;npm install -g pseudo-l10n&lt;/code&gt; and &lt;code&gt;run pseudo-l10n en.json pseudo.json&lt;/code&gt; to instantly create test locales.&lt;/p&gt;

&lt;p&gt;By automating i18n testing, you ensure that localization becomes smooth, fast, and cost-effective. With AI localization, &lt;a href="https://l10n.dev" rel="noopener noreferrer"&gt;l10n&lt;/a&gt;.dev can help you get there.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>testing</category>
      <category>frontend</category>
      <category>tutorial</category>
      <category>qa</category>
    </item>
    <item>
      <title>i18n vs l10n: Why Developers Should Care — and How AI Can Help</title>
      <dc:creator>Anton Antonov</dc:creator>
      <pubDate>Thu, 04 Dec 2025 10:57:44 +0000</pubDate>
      <link>https://forem.com/anton_antonov/i18n-vs-l10n-why-developers-should-care-and-how-ai-can-help-18i8</link>
      <guid>https://forem.com/anton_antonov/i18n-vs-l10n-why-developers-should-care-and-how-ai-can-help-18i8</guid>
      <description>&lt;p&gt;&lt;strong&gt;Internationalization&lt;/strong&gt; (i18n) and &lt;strong&gt;localization&lt;/strong&gt; (l10n) often feel like “later” problems — right up until they’re not.&lt;/p&gt;

&lt;p&gt;As developers, we’ve all done something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;button&amp;gt;Order now&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or buried inside a template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p&amp;gt;Welcome back, {{ user.name }}!&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works fine — until your product team says,&lt;br&gt;
&lt;strong&gt;“We’re launching in Japan, Germany, and the Middle East next quarter.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Suddenly, every hardcoded string turns into technical debt. Engineers scramble to extract text, designers fight broken layouts, QA chases missing translations, and releases slip.&lt;/p&gt;

&lt;p&gt;That’s the moment you stop and ask: &lt;strong&gt;So, what’s i18n vs l10n — and why is it critical?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In short:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internationalization (i18n)&lt;/strong&gt; is how you prepare your app to support multiple languages.&lt;br&gt;
&lt;strong&gt;Localization (l10n)&lt;/strong&gt; is how you adapt your app to each language and culture.&lt;/p&gt;

&lt;p&gt;Understanding the difference is key to building global-ready products — so let’s break it down with real examples and developer-friendly guidance.&lt;/p&gt;

&lt;p&gt;Hi! I’m a developer with deep experience in building scalable applications, including time in the game development industry at companies like &lt;strong&gt;Plarium&lt;/strong&gt;, known for MMO RPG titles where localization is critical. Every project I worked on — from games to enterprise apps — had to support internationalization (i18n) and localization (l10n) from the ground up.&lt;/p&gt;

&lt;p&gt;Now, I’m using that experience to improve how apps are localized. I’m building an AI-powered service that enhances &lt;a href="https://l10n.dev" rel="noopener noreferrer"&gt;localization&lt;/a&gt; pipelines, and I’ve seen firsthand how getting i18n and l10n right can accelerate global growth — or become a blocker.&lt;/p&gt;

&lt;p&gt;In this post, we’ll explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The difference between i18n and l10n (with real examples)&lt;/li&gt;
&lt;li&gt;Best practices to future-proof your codebase&lt;/li&gt;
&lt;li&gt;How AI can automate and streamline localization at scale&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What is i18n? (Internationalization)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Internationalization&lt;/strong&gt; is the process of designing and building your software so it can support multiple languages, regions, and cultural formats &lt;em&gt;without needing code changes later&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Think of it as laying the groundwork that makes your app ready for any language — the logic, structure, and flexibility needed to support global users.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Extract All User-Facing Strings
&lt;/h2&gt;

&lt;p&gt;Hardcoded strings are your biggest enemy when preparing an app for localization. They hide in your templates and components, making translation painful and error-prone. That’s why the first step in proper internationalization is extracting all user-facing text into structured files — most commonly JSON.&lt;/p&gt;

&lt;p&gt;JSON has become the de facto format for localization in web development, especially with JavaScript-based frameworks like React, Angular, and Vue. It’s simple, lightweight, easy to version in Git, and works well with popular i18n libraries.&lt;/p&gt;

&lt;p&gt;But more importantly, JSON supports nested keys, allowing you to organize translations in a way that mirrors your application’s structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "auth": {
    "login": {
      "title": "Sign in to your account",
      "submit": "Log in",
      "forgot": "Forgot your password?",
      "errors": {
        "required": "This field is required",
        "invalid": "Invalid email or password"
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧑‍💻 In Code (React + i18next):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useTranslation } from 'react-i18next';

const LoginForm = () =&amp;gt; {
  const { t } = useTranslation();

  return (
    &amp;lt;form&amp;gt;
      &amp;lt;h1&amp;gt;{t('auth.login.title')}&amp;lt;/h1&amp;gt;
      &amp;lt;input placeholder={t('auth.login.forgot')} /&amp;gt;
      &amp;lt;button&amp;gt;{t('auth.login.submit')}&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, your components remain language-agnostic — and localization becomes a plug-and-play process.&lt;/p&gt;

&lt;p&gt;You can use tools like &lt;a href="https://github.com/i18next/i18next-parser" rel="noopener noreferrer"&gt;i18next-parser&lt;/a&gt; (for React/Vue) or &lt;a href="https://www.npmjs.com/package/@vendure/ngx-translate-extract" rel="noopener noreferrer"&gt;ngx-translate-extract&lt;/a&gt; (for Angular) to automatically extract strings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why i18next?
&lt;/h3&gt;

&lt;p&gt;Among many i18n libraries, &lt;a href="https://i18next.com/" rel="noopener noreferrer"&gt;i18next&lt;/a&gt; stands out as the most widely adopted in the JavaScript ecosystem — for good reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works seamlessly with &lt;strong&gt;React, Vue, Angular, Next.js&lt;/strong&gt;, and others&lt;/li&gt;
&lt;li&gt;Supports namespaces, lazy loading, pluralization, context, and interpolation&lt;/li&gt;
&lt;li&gt;Built-in support for JSON format with nested keys&lt;/li&gt;
&lt;li&gt;Actively maintained with a large community and ecosystem (e.g., react-i18next, i18next-parser, i18next-browser-languagedetector)&lt;/li&gt;
&lt;li&gt;Compatible with AI-powered translation workflows and external services (like &lt;a href="https://l10n.dev" rel="noopener noreferrer"&gt;l10n.dev&lt;/a&gt; 😉)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In contrast, formats like .po, .xliff, or .properties are more common in older or enterprise environments and may require additional tooling or conversion layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Use Meaningful Keys — Not Sentences or Raw IDs
&lt;/h2&gt;

&lt;p&gt;When defining your translation keys, avoid using full text strings or generic IDs. Instead, use descriptive, meaningful keys that reflect the context and structure of your application.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Good&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"form.error.required": "This field is required"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🚫 &lt;strong&gt;Bad&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"This field is required": "Ce champ est requis"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🚫 &lt;strong&gt;Also Bad&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"msg_001": "This field is required"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why it matters:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reusable&lt;/strong&gt;: A key like form.error.required can be used across multiple forms without duplication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainable&lt;/strong&gt;: If the English text changes, the key stays the same — so translations don’t need to be redone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear&lt;/strong&gt;: Developers and translators can quickly understand the purpose of the key from its name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable&lt;/strong&gt;: Avoids unnecessary changes in translation files and version control noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use dot notation to reflect app structure (form.login.button) and avoid long, flat key files.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Split into Namespaces — Avoid One Giant JSON File
&lt;/h2&gt;

&lt;p&gt;As your app grows, so do your translations. Splitting them into &lt;strong&gt;namespaces&lt;/strong&gt; (e.g., auth.json, dashboard.json, common.json) improves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintainability&lt;/li&gt;
&lt;li&gt;Load time (you can lazy-load only needed namespaces)&lt;/li&gt;
&lt;li&gt;Collaboration (teams can own parts of the translation structure)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// locales/en/auth.json
{
  "login.title": "Sign in to your account"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;i18next.use(initReactI18next).init({
  ns: ['auth', 'common'],
  defaultNS: 'common',
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Is Localization (l10n)?
&lt;/h2&gt;

&lt;p&gt;Localization isn’t just about translating words — it’s about adapting your entire user interface to feel native in another language and culture. That includes grammar, plural rules, date formats, name conventions, and even tone.&lt;/p&gt;

&lt;p&gt;So why do the next practicies matter?&lt;/p&gt;

&lt;p&gt;Because if developers don’t structure things properly — like using plural forms, contexts, or interpolation — translators can’t do their job well. The result? Awkward translations, broken layouts, or content that simply doesn’t make sense to your users.&lt;/p&gt;

&lt;p&gt;Even the best AI translator or localization team can’t fix a poor foundation. You need to give them the right keys, metadata, and flexibility — and that’s your job during i18n.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Interpolation — Insert Dynamic Values into Translations
&lt;/h2&gt;

&lt;p&gt;Interpolation lets you inject dynamic data (like user names, numbers, and dates) into your translated strings without hardcoding them. This keeps translations reusable, secure, and easier to maintain.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Good Example&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "welcome": "Welcome, {{name}}!",
  "order_summary": "Total: {{amount}} {{currency}}"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your React code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t('welcome', { name: 'Anton' })        // → Welcome, Anton!
t('order_summary', { amount: 25, currency: 'USD'})     // → Total: 25 USD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔐 i18next escapes all values by default to protect against XSS. You can disable it if needed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ interpolation: { escapeValue: false } }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🚫 What Should Not Be Translated
&lt;/h3&gt;

&lt;p&gt;When preparing your app for localization, it’s just as important to know what not to include in your translatable strings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;Dates and times&lt;/strong&gt; should not be translated manually — instead, format them to user’s locale at runtime.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Dynamic data&lt;/strong&gt; like numbers, prices, and currencies should use interpolation. Translators should never rewrite actual values like 25 USD.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Proper names&lt;/strong&gt; (e.g. names, brands, company names) should stay intact.&lt;/li&gt;
&lt;li&gt;✅ Only historical or well-known proper names (e.g., “Christopher Columbus”) may be translated. Converting the name from one script to another, for example: Steve Jobs -&amp;gt; Cтив Джобс&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How l10n.dev Handles This
&lt;/h3&gt;

&lt;p&gt;l10n.dev &lt;a href="https://l10n.dev/ws/translate-i18n-files" rel="noopener noreferrer"&gt;translates i18n&lt;/a&gt; JSON files with AI that understands context — it intelligently preserves placeholders like {{name}}, {{count}}, and {{amount}}, and avoids awkward or literal translations that are common with basic machine translation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Understands placeholder meaning&lt;/strong&gt; and keeps dynamic data intact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatically converts date and number formats&lt;/strong&gt; to match the target locale — especially useful when translating historical or factual content like “Founded on July 4, 1776” → “Основан 4 июля 1776 г.”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knows what not to translate&lt;/strong&gt;, like user names, currencies, and runtime values, reducing risk and saving time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ensures translations feel natural&lt;/strong&gt; and localized, not robotic or confusing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes l10n.dev a smarter choice for developers — giving you production-ready translations that respect both your code and your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Handle Plural Forms Properly
&lt;/h2&gt;

&lt;p&gt;Different languages treat plurals differently. English is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "days_count_one": "{{count}} day",
  "days_count_other": "{{count}} days"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But Slavic languages like Russian, Polish, and Ukrainian require 3 forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;one&lt;/strong&gt; → 1 день&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;few&lt;/strong&gt; → 2 дня, 3 дня, 4 дня&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;other&lt;/strong&gt; → 0 дней, 5 дней, 11 дней, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;No need to worry — i18next handles these rules for you.&lt;/strong&gt; You just define the forms in your translation files, and i18next picks the right one based on the current language and count value.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ 
  "days_count_one": "{{count}} день",
  "days_count_few": "{{count}} дня",
  "days_count_other": "{{count}} дней"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t('days_count', { count: 3 })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it — i18next figures out the rest&lt;/p&gt;

&lt;p&gt;Of course, &lt;strong&gt;developers aren’t translators&lt;/strong&gt; — so it’s perfectly normal to have only _one and _other for English. But when your app is translated into languages with complex plural rules, the localization team should expand those keys as needed (e.g., add _few, _many, etc.).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In theory, the localization team should handle this.&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;But in reality, many translators are native speakers with no knowledge of i18next or plural formatting logic. If they see two keys in the source, they’ll likely return just two — even when the target language requires more.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This creates bugs or incorrect grammar in your app.&lt;/p&gt;

&lt;p&gt;✅ That’s why &lt;strong&gt;l10n.dev&lt;/strong&gt;, an AI-powered &lt;a href="https://l10n.dev" rel="noopener noreferrer"&gt;i18n translation service&lt;/a&gt;, automatically adds all required plural forms for the target language during translation. So even if your source file is minimal, the translated result is fully structured — and your app works correctly in every locale, with no guesswork or missing forms.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Context — Handling gender, tone, or grammatical cases
&lt;/h2&gt;

&lt;p&gt;Context is used when a translation changes based on a situation — like &lt;strong&gt;gender, formal/informal speech&lt;/strong&gt;, or other &lt;strong&gt;grammatical variations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Example: Gender-based context&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "user_info": "User info",
  "user_info_male": "His info",
  "user_info_female": "Her info"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t('user_info', { context: 'male' })   // → His info
t('user_info', { context: 'female' }) // → Her info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works by appending &lt;strong&gt;_contextValue&lt;/strong&gt; to the base key. i18next will automatically resolve the correct variant.&lt;/p&gt;

&lt;p&gt;Common use cases for context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gender (he/she/they)&lt;/li&gt;
&lt;li&gt;Formal vs informal (_formal, _informal)&lt;/li&gt;
&lt;li&gt;Language-specific word forms (e.g., in Slavic languages)&lt;/li&gt;
&lt;li&gt;Role-based phrasing (admin vs user)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can combine both&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "greeting": "Hello, {{name}}!",
  "greeting_formal": "Good day, {{name}}!"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t('greeting', { name: 'Anton', context: 'formal' })
// → Good day, Anton!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interpolation&lt;/strong&gt; keeps your UI dynamic and localized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context&lt;/strong&gt; handles linguistic nuances correctly&lt;/li&gt;
&lt;li&gt;Both help translators deliver accurate, natural-sounding results — without complex logic in your code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yes — i18next handles all the fallback logic for you, so you don’t have to worry if a context variant is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI Makes It Easier
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://l10n.dev" rel="noopener noreferrer"&gt;AI-powered localization&lt;/a&gt; service like l10n.dev take the complexity out of localization by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Translating with &lt;strong&gt;context awareness&lt;/strong&gt;, not just word-for-word.&lt;/li&gt;
&lt;li&gt;Preserving placeholders and interpolations ({{count}}, {{name}}, etc.) so translations remain functional.&lt;/li&gt;
&lt;li&gt;Automatically adjusting &lt;strong&gt;date/number formats&lt;/strong&gt; to the target locale.&lt;/li&gt;
&lt;li&gt;Ensuring proper &lt;strong&gt;pluralization&lt;/strong&gt;, even for complex languages like Russian, Polish, or Arabic.&lt;/li&gt;
&lt;li&gt;Respecting &lt;strong&gt;original key ordering&lt;/strong&gt; in files for cleaner diffs and easier reviews.&lt;/li&gt;
&lt;li&gt;Letting you &lt;strong&gt;translate only new strings&lt;/strong&gt;, so you don’t retranslate your whole app every time.&lt;/li&gt;
&lt;li&gt;Keeping html, tabs, line breaks in translated text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All you need to do is follow best practices for i18n — and let AI handle the rest.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
