Skip to content
Back to skills

Localization Patterns

ASecurity

Localization code patterns: Godot tr() and TranslationServer usage, PO file structure, ICU plurals, font fallback chains for multi-script text, RTL layout mirroring, pseudo-localization, and string key naming. Use when making a game translatable or fixing text that breaks in another language.

  • 65 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 1, 2026
developmentgonodetesting

Security analysis

A100/100

Scanned October 1, 2026

npx -y skills add HermeticOrmus/claude-code-game-development --skill localization-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Localization Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Localization Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hermeticormus-localization-patterns/badge)](https://www.skillsdirectory.com/skills/hermeticormus-localization-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: localization-patterns
description: "Localization code patterns: Godot tr() and TranslationServer usage, PO file structure, ICU plurals, font fallback chains for multi-script text, RTL layout mirroring, pseudo-localization, and string key naming. Use when making a game translatable or fixing text that breaks in another language."
---

# Localization Patterns

## Godot TranslationServer and tr() Usage

```gdscript
# Correct tr() usage patterns
class_name LocalizableUI extends Control

func _ready() -> void:
    # Static string - wrap in tr()
    $Label.text = tr("MENU_PLAY_BUTTON")

    # Dynamic string with variable - use format, not concatenation
    var score := 1500
    $ScoreLabel.text = tr("SCORE_DISPLAY") % score
    # PO file: msgid "SCORE_DISPLAY" msgstr "Score: %d"

    # Plural-aware string
    var item_count := 3
    $ItemLabel.text = tr_n("ITEM_COUNT_SINGULAR", "ITEM_COUNT_PLURAL", item_count) % item_count
    # PO file:
    # msgid "ITEM_COUNT_SINGULAR"
    # msgid_plural "ITEM_COUNT_PLURAL"
    # msgstr[0] "%d item"
    # msgstr[1] "%d items"

    # Listen for locale changes (font swaps, layout updates)
    TranslationServer.connect("locale_changed", _on_locale_changed)

func _on_locale_changed() -> void:
    # Refresh all translatable strings
    _ready()
    _update_fonts_for_locale()

func _update_fonts_for_locale() -> void:
    var locale := TranslationServer.get_locale()
    if locale.begins_with("ja") or locale.begins_with("zh") or locale.begins_with("ko"):
        $Label.add_theme_font_override(&"font", preload("res://fonts/NotoSansCJK.ttf"))
    elif locale.begins_with("ar") or locale.begins_with("he"):
        $Label.add_theme_font_override(&"font", preload("res://fonts/NotoSansArabic.ttf"))
        # Mirror layout for RTL
        layout_direction = Control.LAYOUT_DIRECTION_RTL
    else:
        $Label.remove_theme_font_override(&"font")
        layout_direction = Control.LAYOUT_DIRECTION_AUTO
```

## PO File Structure

```po
# messages.pot (template - generated, not translated)
# and messages.fr.po (French translation)

# Header - required, specifies plural rules
msgid ""
msgstr ""
"Project-Id-Version: MyGame 1.0\n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"Content-Type: text/plain; charset=UTF-8\n"

# Simple string
msgid "MENU_PLAY_BUTTON"
msgstr "Jouer"

# Contextual disambiguation
msgctxt "main_menu"
msgid "BACK"
msgstr "Retour"

msgctxt "character_anatomy"
msgid "BACK"
msgstr "Dos"

# Plural forms
msgid "ITEM_COUNT_SINGULAR"
msgid_plural "ITEM_COUNT_PLURAL"
msgstr[0] "%d objet"
msgstr[1] "%d objets"

# String with variables (preserve format specifiers)
msgid "SCORE_DISPLAY"
msgstr "Score : %d"

# String with named variables (safer than positional)
msgid "PLAYER_GREETING"
msgstr "Bonjour, {player_name} !"
```

## ICU Message Format for Plurals

```gdscript
# Custom ICU-style formatter for Godot (simplified)
# For full ICU support, use Unity Localization or custom GDExtension

class_name ICUFormatter
# Handles {count, plural, one{...} other{...}} syntax

static func format(template: String, vars: Dictionary) -> String:
    var result := template
    # Replace simple variables
    for key in vars:
        result = result.replace("{%s}" % key, str(vars[key]))
    return result

# Usage:
# tr("FOUND_ITEMS").format({"count": 5}) -> "Found 5 items"
# PO: msgid "FOUND_ITEMS" msgstr "Found {count} items"

# Plural with tr_n (Godot built-in):
func items_text(count: int) -> String:
    # tr_n selects singular or plural form based on count
    return tr_n("ITEM_SINGULAR", "ITEM_PLURAL", count) % count
```

## Font Fallback Chain for Multi-Script Support

```gdscript
# Set up font fallback chain covering Latin, CJK, and Arabic
func setup_global_font() -> void:
    var theme := ThemeDB.get_project_theme()
    var font := FontFile.new()
    font.load_dynamic_font("res://fonts/NotoSans-Regular.ttf")

    # CJK fallback - activates for CJK codepoints not in base font
    var cjk_font := FontFile.new()
    cjk_font.load_dynamic_font("res://fonts/NotoSansCJK-Regular.ttf")
    font.add_fallback(cjk_font)

    # Arabic fallback
    var arabic_font := FontFile.new()
    arabic_font.load_dynamic_font("res://fonts/NotoSansArabic-Regular.ttf")
    font.add_fallback(arabic_font)

    theme.default_font = font
    ThemeDB.get_project_theme().default_font = font
```

## RTL Layout Mirroring

```gdscript
# Apply RTL layout to all controls in scene
func apply_rtl_layout(root: Control, is_rtl: bool) -> void:
    var direction := Control.LAYOUT_DIRECTION_RTL if is_rtl else Control.LAYOUT_DIRECTION_LTR
    _set_layout_recursive(root, direction)

func _set_layout_recursive(node: Control, direction: Control.LayoutDirection) -> void:
    node.layout_direction = direction
    for child in node.get_children():
        if child is Control:
            _set_layout_recursive(child, direction)

# RTL detection from locale
func is_rtl_locale(locale: String) -> bool:
    const RTL_LANGUAGES: Array[String] = ["ar", "he", "ur", "fa", "dv", "yi"]
    var language := locale.split("_")[0]  # "ar_SA" -> "ar"
    return language in RTL_LANGUAGES
```

## Pseudo-Localization for Early Testing

```gdscript
# Manual pseudo-localization (Godot has built-in, but this shows the logic)
class_name PseudoLocalizer
const CHAR_MAP := {
    "a": "á", "b": "ƀ", "c": "ç", "d": "ď", "e": "é",
    "f": "ƒ", "g": "ĝ", "h": "ĥ", "i": "í", "j": "ĵ",
    "k": "ķ", "l": "ĺ", "m": "m", "n": "ñ", "o": "ó",
    "p": "ƥ", "q": "q", "r": "ŗ", "s": "ŝ", "t": "ţ",
    "u": "ú", "v": "v", "w": "ŵ", "x": "x", "y": "ý",
    "z": "ź",
}

static func pseudo_localize(text: String) -> String:
    var result := "["
    for char in text:
        result += CHAR_MAP.get(char.to_lower(), char)
    # Add 40% length padding to simulate German text expansion
    result += "_".repeat(int(text.length() * 0.4))
    result += "]"
    return result
```

## String Key Naming Convention

```
# Hierarchy: SCREEN_SECTION_ELEMENT_VARIANT
MENU_MAIN_PLAY_BUTTON          # Main menu, Play button
MENU_MAIN_SETTINGS_BUTTON      # Main menu, Settings button
HUD_HEALTH_LABEL               # HUD, health label
INVENTORY_ITEM_NAME_SWORD      # Inventory, sword item name
DIALOGUE_NPC_MERCHANT_GREETING # Merchant NPC greeting line
ERROR_SAVE_FAILED              # Error message for save failure
TUTORIAL_MOVEMENT_HINT         # Tutorial hint for movement
```

## Anti-Patterns

- **String concatenation**: `"Enemy: " + enemy.name` - word order varies by language. Use named substitution: `tr("ENEMY_LABEL").format({"name": enemy.name})`.
- **Hardcoded plural**: `"%d items"` doesn't work in languages with 3+ plural forms (Russian, Polish). Always use `tr_n()`.
- **Empty msgstr**: translators leaving msgstr blank - Godot falls back to msgid. Build QA catches this.
- **Format string reordering**: `"%s found %d items"` - translators need to reorder but `%s %d` positional args can't reorder. Use named: `"{player} found {count} items"`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…