Golden Crayon Translation System

Overview

Golden Crayon features a comprehensive translation system that supports both direct phrase mapping and advanced pattern matching. The system automatically translates all speech output and can handle complex text transformations.

Features

- Direct Translation: Exact phrase-to-phrase mapping
- Aliases / Pointers: One translation reused by many English phrases (key = @otherKey)
- Reusable Variables: Define a term once in [VARS] and reference it with ${name}
- Pattern Matching: Plain substring replacement for dynamic text
- Wildcard Patterns: Whole-phrase matching with capture groups (* and $1..$9)
- Automatic Speech Integration: All spoken text is automatically translated
- Hot Language Switching: Change languages instantly through settings

File Format

Translation files use the .lng extension and are stored in the translations/ folder. Each language has its own file (e.g., Spanish.lng, French.lng).

File Structure

[VARS]
// Reusable terms (optional). Define before they are referenced.
name = "value"

[DIRECT]
// Direct phrase translations and aliases
English phrase = "Translated phrase"

[PATTERN]
// Pattern-based replacements (plain and wildcard)
"English pattern" => "Translated pattern"

[REGEX]
// Regular-expression rules
"regex pattern" => "replacement with $1 backreferences"

[CONFIG]
// Per-file options, e.g. whether to log untranslated strings
nolog

Resolution Order

When a string is translated it is checked in this order, and the first hit wins:
1. Direct translations (exact match)
2. Case-insensitive direct match (fallback for #1; can be disabled)
3. Aliases (key = @otherKey)
4. Wildcard patterns (whole-phrase match; resolves once, never re-translated)
5. Regex rules (applied in order over the text)
6. Plain substring patterns (applied repeatedly until no further change)

If none of the above touch the string, it is left in English -- and, for a non-English language, recorded to the missing-translation log (see below).

Sections

VARS Section

Variables let you define a term once and reuse it anywhere with ${name}. They are expanded when the file loads, in every section, so they work in DIRECT values, PATTERN replacements, and even other variables. Because the file is read top to bottom, put [VARS] at the top of the file (before it is referenced).

Format:
name = "value"

Example:
[VARS]
app = "Crayón Dorado"

[DIRECT]
Welcome to Golden Crayon = "Bienvenido a ${app}"

DIRECT Section

Direct translations provide exact phrase-to-phrase mapping. These are processed first and take priority over pattern matches.

Format:
English text = "Translated text"

Example:
[DIRECT]
Loading. Please wait... = "Cargando. Por favor espere..."
Settings saved. = "Configuración guardada."
Golden Crayon Settings = "Configuración de Golden Crayon"

Aliases / Pointers

Inside [DIRECT], a value of the form @otherKey makes this phrase reuse another key's translation. This avoids repeating (and having to keep in sync) the same translation for phrases that should always read the same. Aliases may chain (an alias pointing at another alias); cycles are detected and stop safely.

Format:
English text = @AnotherEnglishKey

Example:
[DIRECT]
on = "encendido"
enabled = @on        // "enabled" now translates to "encendido"
active = @enabled    // chains through to "encendido"

PATTERN Section

Pattern translations handle dynamic text that contains variables or UI suffixes. There are two kinds:

1. Plain patterns (no *): a straight substring replacement. These are applied repeatedly, so a string may be touched by several plain patterns in turn.

2. Wildcard patterns (contain *): matched against the WHOLE string. Each * captures whatever appears in that position, and the captured spans are inserted into the replacement as $1, $2, ... (in left-to-right order, up to $9). A wildcard pattern resolves once and is not fed back through the other patterns, which prevents accidental double-translation. Because the match is anchored to the whole phrase, "Level *" matches "Level 7" but not "Go to Level 7 now".

Format:
"Search pattern" => "Replacement pattern"

Plain example:
[PATTERN]
"Master Volume" => "Volumen Principal"
"Sign sounds " => "Sonidos de señal "
" FPS" => " CPS"

Wildcard example:
[PATTERN]
"You have * coins" => "Tienes $1 monedas"
"* of *" => "$1 de $2"
"Level *" => "Nivel $1"

Pitfall: a plain pattern whose replacement contains its own search text

Plain patterns are applied repeatedly until nothing changes (so several patterns can each take a turn on the same string). This creates one trap: if a plain pattern's replacement CONTAINS its own search text, it keeps matching its own output and grows without bound.

Example -- translating "Map" to Spanish "Mapa":

[PATTERN]
"Map" => "Mapa"

"Mapa" contains "Map", so applied to "Map tab" it would loop:
"Map tab" -> "Mapa tab" -> "Mapaa tab" -> "Mapaaa tab" -> ... -> "Mapaaaaaaaaaaaaaaaaaaaa tab"

The engine guards against this: a plain pattern whose replacement contains its search text is applied at most once. So the result is simply correct:

"Map tab" -> "Mapa tab"

The guard stops the runaway, but a self-referential plain pattern is usually a sign you want a different tool:
- For a whole word or phrase spoken on its own, use [DIRECT] (exact match, never looped): Map = "Mapa"
- For a role word at the end of a control string (e.g. List -> "Lista", Edit -> "Edicion"), use a wildcard, which also never re-loops: "* List" => "$1 Lista"
The same trap applies whenever the translation merely adds letters around the original word (List/Lista, Cancel/Cancelar, Confirm/Confirmar) -- reach for [DIRECT] or a wildcard there.

Plurals (in wildcard replacements)

A wildcard replacement may pick between a singular and a plural word based on one of its captures, using the token {n|singular|plural}. Here n is the capture number; if that capture equals 1 the singular form is used, otherwise the plural.

Example:
[PATTERN]
"You have * coins" => "Tienes $1 {1|moneda|monedas}"
// "You have 1 coins"  -> "Tienes 1 moneda"
// "You have 5 coins"  -> "Tienes 5 monedas"

This is the common two-form case (used by English, Spanish, French, etc.). For a language with more complex plural rules, write specific wildcard patterns for the exact numbers that differ.

REGEX Section

For advanced cases, [REGEX] rules run a full regular-expression replacement over the text. Captured groups are written back into the replacement as $1, $2, ... Rules are applied in order, each over the result of the previous one. An invalid pattern is ignored rather than crashing.

Format:
"regex pattern" => "replacement"

Example:
[REGEX]
// Reformat "3:45" into spoken words
"(\d+):(\d+)" => "$1 horas y $2 minutos"
// Accept either spelling, normalise to one
"\bcolou?r\b" => "color"

Use [REGEX] sparingly: direct, alias and wildcard entries are easier to read and faster. Reach for regex only when a rule genuinely needs pattern logic.

Missing-Translation Log

While a non-English language is loaded, any string that no rule translated is appended to translations/<LanguageName>.missing.txt, already formatted as a [DIRECT] entry:

    Some untranslated text. = "Some untranslated text."

Each unique string is recorded once (it remembers what it already logged, even across sessions). To finish a translation, play through the game, then open that file, translate the right-hand sides, and paste the lines into your .lng file. This turns "find everything that still needs translating" into fill-in-the-blanks.

Logging is OFF by default. There are two ways to control it, in order of precedence:

1. The language file itself ([CONFIG] section -- see below). This wins over everything, so the file author decides.
2. The in-game setting: Settings > General > "Log untranslated text...". This is how a translator who is not a developer turns logging on while working, and it is remembered between sessions.

CONFIG Section

The optional [CONFIG] section lets a language file configure its own behaviour, so everything is controlled from the .lng file itself. For logging, a [CONFIG] directive overrides the in-game setting for that file, so a finished translation can ship with logging off (and a work in progress can force it on).

Directives:
    nolog                         // never log missing strings for this language
    log                           // always log missing strings for this language
    log_missing = true|false      // explicit form of the two above
    case_insensitive = true|false // case-insensitive direct fallback (default on)

Example (a completed translation that should never write a log):
[CONFIG]
nolog

Creating a New Translation

1. Copy the template: Start with translations/template.lng
2. Rename the file: Use the language name (e.g., German.lng)
3. Translate the content: Replace English text with your translations
4. Test the translation: Load it in-game through Settings > General > Language

Translation Guidelines

- Keep quotes: Maintain quote marks around pattern translations
- Preserve spacing: Keep leading/trailing spaces in patterns
- Test thoroughly: Verify all UI elements translate correctly
- Use UTF-8 encoding: Ensure proper character support

File Comments

- Lines starting with // are comments and are ignored
- Empty lines are ignored
- Use comments to organize sections and provide context

Language Selection

The language can be changed in-game:
1. Go to Settings > General
2. Select Language from the list
3. Click Save Settings

The change takes effect immediately for all new speech output.

Integration

Everything the game says is translated automatically as you play -- you only ever edit the .lng file. Adding or completing a language never requires any changes to the game itself.

Performance

- Direct translations are fastest (dictionary lookup)
- Pattern matching is optimized for common cases
- Large texts are automatically skipped to prevent delays

Troubleshooting

Translation Not Working
- Check file is in translations/ folder
- Verify .lng extension
- Ensure proper file format with [DIRECT] and [PATTERN] sections

Partial Translation
- Check for missing entries in template comparison
- Verify pattern syntax uses => not =
- Test with simple direct translations first

Text Still in English
- Confirm language is selected in settings
- Check if text exists in translation file
- Verify pattern matching covers dynamic text

Contributing Translations

To contribute a new language translation:

1. Use template.lng as your starting point
2. Translate all entries completely
3. Test thoroughly in-game
4. Submit the .lng file for inclusion

New translations help make Golden Crayon accessible to more users worldwide!
