How ArmorPaint's extract_locales.js Generates Localized JSON Assets for strings.c

ArmorPaint's extract_locales.js is a QuickJS utility that scans the C codebase for tr("...") translation macros, extracts string literals while handling escape sequences, and generates sorted JSON locale files that translator.c loads and strings.c consumes through a runtime lookup table.

ArmorPaint implements a lightweight yet effective localization pipeline that bridges JavaScript-based build tools with C runtime consumption. The system automatically extracts translatable UI strings from source code and serves them to the application through JSON assets consumed by the tr() macro system defined in strings.c.

How extract_locales.js Scans Source Files

The extraction process begins in base/tools/extract_locales.js, a QuickJS script that performs static analysis on the ArmorPaint source tree. The script iterates over hard-coded source directories—including paint/sources, paint/sources/nodes_*, and related paths—to locate every .c file containing translatable content.

For each file encountered, the script searches for the token tr(" and parses the subsequent quoted string literal. The parser correctly handles escaped characters including \n, \t, and \\, ensuring that multi-line strings and special formatting survive the extraction process intact. This scanning logic is implemented in lines 31–68 of the script.

Building and Writing Locale JSON Files

Once string literals are extracted, extract_locales.js constructs a JSON object mapping original English strings to their localized equivalents. The script implements a preservation strategy for existing translations: if a key already exists in the target locale file, the existing translation value is retained; otherwise, the value is initialized as an empty string to indicate that translation work is required.

The resulting JSON object is sorted alphabetically by key and written to paint/assets/locale/<locale>.json (for example, en.json or fr.json). The output uses 4-space indentation for readability, making the files suitable for manual editing by translators. This write process occurs in lines 83–84, while the merge logic appears in lines 73–78.

Runtime Consumption in translator.c and strings.c

Loading Translations at Runtime

When a user selects a locale, the C runtime invokes translator_load_translations() defined in paint/sources/translator.c. This function constructs the asset path data/locale/<locale>.json and loads the file using iron_load_blob(). The blob is converted to a string via sys_buffer_to_string() and parsed into an in-memory translation table. This initialization sequence is found in lines 145–146 of translator.c.

Lookup via the tr() Macro

The tr() macro—defined in paint/sources/strings.c—serves as the primary interface for localized UI text. When invoked with an English key string, tr() performs a lookup in the translation table populated from the JSON. If a localized value exists, it returns that string; otherwise, it falls back to the original English key. This behavior ensures that the UI remains functional even when specific translations are missing.

Practical Workflow Examples

Generating a New Locale

To create or update a locale file for French, run the extraction tool from the repository root:

../make --js base/tools/extract_locales.js fr

This command creates or updates paint/assets/locale/fr.json, preserving existing French translations while adding empty entries for any new English strings discovered in the source.

Using Localized Strings in UI Code

Developers mark translatable text using the tr() macro throughout the C source:

// In any UI source file
ui_label(tr("Brush Size"));

When the French locale is active, tr("Brush Size") queries the loaded JSON table and returns "Taille du pinceau"; if the locale were English or the key missing, it returns "Brush Size" unchanged.

Summary

  • extract_locales.js scans paint/sources/ and related directories for tr("...") patterns, handling escape sequences and extracting raw string literals.
  • The script generates alphabetically sorted JSON files at paint/assets/locale/<locale>.json, preserving existing translations and flagging new entries with empty strings.
  • translator.c loads these JSON assets at runtime via iron_load_blob() and populates an in-memory translation table.
  • strings.c provides the tr() macro that performs constant-time lookups against the translation table, falling back to English keys when localized values are absent.

Frequently Asked Questions

What is the extract_locales.js script in ArmorPaint?

extract_locales.js is a QuickJS build tool located at base/tools/extract_locales.js that automates the extraction of translatable strings from the C codebase. It searches source files for the tr("...") macro pattern and generates JSON locale files used by the runtime translation system.

How does strings.c consume the JSON files generated by extract_locales.js?

strings.c does not directly parse JSON; instead, it relies on the translation table populated by translator.c. The tr() macro in strings.c performs lookups against this pre-populated table, returning localized strings when available or the original English key as a fallback.

Where are the localized JSON assets stored in the ArmorPaint repository?

Generated locale files are stored in paint/assets/locale/ with filenames following the pattern <locale>.json (e.g., en.json, fr.json). At runtime, the application loads these from the data/locale/ directory via the virtual filesystem.

How does the tr() macro handle missing translations?

The tr() macro implements a safe fallback mechanism: if the lookup key is not found in the current locale's translation table, the macro returns the original English string. This ensures the UI remains fully functional even when specific translations are incomplete or missing entirely.

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 →