How Godot's TranslationServer Handles Localization and Plural Rules: A Deep Dive into the i18n Architecture

Godot's TranslationServer centralizes internationalization by parsing and normalizing locale strings through standardize_locale(), comparing locale compatibility via the compare_locales() scoring algorithm, and resolving Unicode CLDR plural rules from static data tables compiled into core/string/locales.h.

The TranslationServer serves as the backbone of internationalization (i18n) in the Godot game engine, managing everything from locale normalization to plural form resolution. Understanding how the TranslationServer handles localization and plural rules is essential for developers building multilingual applications, as it governs how the engine selects languages, matches regional variants, and computes grammatical number forms. This analysis examines the core implementation in core/string/translation_server.cpp and its accompanying data tables.

Initializing Locale Data at Server Startup

When the TranslationServer constructor runs, it immediately invokes init_locale_info() to populate static HashMaps with localization metadata derived from CLDR (Common Locale Data Repository) and Unicode standards【/tmp/instagit_ldgju1gx/core/string/translation_server.cpp#L41-L86】. These maps drive every subsequent localization operation:

  • language_map – Maps ISO-639-1 codes to human-readable language names
  • script_map – Maps ISO-15924 codes to script names
  • locale_rename_map – Handles legacy Windows locale identifiers
  • country_name_map and country_rename_map – Manage ISO-3166 country codes and temporary aliases
  • variant_map – Tracks regional variant tokens
  • plural_rules_map – Associates locales with CLDR plural expressions
  • num_system_map – Stores numeric system data including digit symbols and percent signs

Data Sources in locales.h

All static tables reside in core/string/locales.h. The plural_rules array specifically stores GNU gettext-style plural expressions (e.g., "nplurals=2; plural=(n != 1);") compiled from Unicode CLDR release-47 and GNU gettext data【/tmp/instagit_ldgju1gx/core/string/locales.h#L1824-L1855】.

Normalizing Locale Strings with standardize_locale()

The standardize_locale() method converts arbitrary locale identifiers into canonical form suitable for the engine. It creates a temporary Locale object (constructed in translation_server.cpp【/tmp/instagit_ldgju1gx/core/string/translation_server.cpp#L69-L96】) and executes the following normalization pipeline:

  1. Replace dashes with underscores to handle macOS-style locales (en-USen_US)
  2. Split components into language, script, country, and variant segments
  3. Apply rename maps to convert legacy identifiers using locale_rename_map and country_rename_map
  4. Validate script codes against script_map, clearing unsupported entries
  5. Add defaults when enabled: look up preferred scripts from locale_script_info and fill default countries when scripts are present but countries are missing

The resulting canonical string (e.g., "sr_Latn_RS" or "en_US") is used consistently throughout the engine's translation system.

Scoring Locale Compatibility with compare_locales()

The engine determines translation fallback priority using compare_locales(), which assigns numerical scores based on component matching【/tmp/instagit_ldgju1gx/core/string/translation_server.cpp#L19-L75】:

Score Condition
10 Exact string match
5 Same language with no additional components
+1 / -1 Bonus or penalty for matching/mismatching script, country, or variant
0 Different languages (no compatibility)

Results are cached in locale_compare_cache to optimize repeated lookups during translation resolution.

Resolving CLDR Plural Rules

The plural_rules Data Structure

The static plural_rules array in locales.h pairs locale patterns with GNU gettext expressions that define how many plural forms a language uses and the boolean conditions for selecting each form. For example, Russian uses "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" to handle its three grammatical forms.

Lookup Algorithm in get_plural_rules()

The get_plural_rules() function implements a hierarchical fallback chain【/tmp/instagit_ldgju1gx/core/string/translation_server.cpp#L5-L22】:

String TranslationServer::get_plural_rules(const String &p_locale) const {
    const String *rule = plural_rules_map.getptr(p_locale);
    if (rule) return *rule;

    Locale l = Locale(*this, p_locale, false);
    // Try language+country, then language only
    if (!l.country.is_empty()) {
        rule = plural_rules_map.getptr(l.language + "_" + l.country);
        if (rule) return *rule;
    }
    rule = plural_rules_map.getptr(l.language);
    if (rule) return *rule;

    return String(); // No rule found
}

The function first attempts an exact match (e.g., "fr_CA"), then falls back to language-country combinations, and finally to language-only codes. This ensures broad coverage while allowing regional overrides when specific plural behavior differs (such as French in Canada versus France).

Practical Implementation Examples

Setting a Locale and Retrieving Canonical Names


# Normalize and identify a locale

TranslationServer.set_locale("pt_BR")
var canonical = TranslationServer.get_locale_name(TranslationServer.get_locale())
print(canonical)  # → "Portuguese (Latin), Brazil"

This sequence calls set_localestandardize_localeLocale constructor, then queries language_map, script_map, and country_name_map to assemble the human-readable identifier.

Accessing Plural Rules for Grammatical Number Handling

var locale = "ru"
var rule = TranslationServer.get_plural_rules(locale)
print(rule)   

# → "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"

var plural = PluralRules.new(rule)
print(plural.get_plural_form(5))   # → 2 (Russian plural form for "many")

The get_plural_rules method queries plural_rules_map and falls back to language-only entries when specific regional variants lack dedicated rules.

Comparing Locales for Fallback Selection

var score = TranslationServer.compare_locales("en_US", "en_GB")
print(score)   # → 6 (language matches: 5, script empty: 0, country differs: -1, variant empty: 0)

This scoring mechanism enables the engine to select the best available translation when an exact locale match does not exist in the loaded translation resources.

Summary

  • Data-driven architecture: The TranslationServer relies on static tables in core/string/locales.h compiled from CLDR and Unicode standards rather than external dependencies.
  • Robust normalization: The standardize_locale() method handles legacy identifiers, variant tags, and platform-specific formatting (dashes vs. underscores) to produce canonical locale strings.
  • Hierarchical plural resolution: get_plural_rules() implements a three-tier fallback system (exact → language-country → language-only) to ensure grammatical number rules are always available.
  • Optimized matching: The compare_locales() scoring algorithm caches results to efficiently determine translation fallback priority based on linguistic similarity.
  • GDScript accessibility: All core functionality is exposed to scripting, allowing runtime locale manipulation and custom plural form calculations via the PluralRules class.

Frequently Asked Questions

How does the TranslationServer normalize locale strings like "en-US" versus "en_US"?

The standardize_locale() method in core/string/translation_server.cpp creates a temporary Locale object that replaces dashes with underscores, splits the identifier into components, applies rename maps for legacy identifiers, validates script codes against script_map, and optionally adds default scripts and countries from locale_script_info.

What scoring system does compare_locales use to determine locale compatibility?

compare_locales() assigns 10 points for exact matches, 5 points for matching languages with no additional components, then adds or subtracts 1 point for each matching or mismatching script, country, or variant. Scores below 0 indicate incompatible languages. The engine uses this score to select the best available translation when exact locale matches are unavailable.

Where does Godot store plural rule definitions used by the TranslationServer?

Plural rules reside in the static plural_rules array inside core/string/locales.h, indexed by plural_rules_map during server initialization. These rules follow GNU gettext syntax (e.g., "nplurals=2; plural=(n != 1);") and are compiled from Unicode CLDR release-47 data paired with GNU gettext's plural-table definitions.

How can developers access plural rules in GDScript to handle grammatical numbers correctly?

Developers call TranslationServer.get_plural_rules(locale) to retrieve the plural expression string, then instantiate a PluralRules object with that string. The get_plural_form(n) method returns the integer index of the correct grammatical form for the given number, enabling dynamic selection of singular, dual, paucal, or plural translations as required by the target language's grammar rules.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →