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

> Learn how ArmorPaint's extract_locales.js generates localized JSON assets by scanning C code for translation macros and preparing them for consumption by strings.c and translator.c.

- Repository: [Armory 3D/armorpaint](https://github.com/armory3d/armorpaint)
- Tags: internals
- Published: 2026-09-14

---

**ArmorPaint's [`extract_locales.js`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/translator.c) loads and [`strings.c`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/strings.c).

## How extract_locales.js Scans Source Files

The extraction process begins in [`base/tools/extract_locales.js`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/en.json) or [`fr.json`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/translator.c).

Lookup via the tr() Macro

The `tr()` macro—defined in [`paint/sources/strings.c`](https://github.com/armory3d/armorpaint/blob/main/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:

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

```

This command creates or updates [`paint/assets/locale/fr.json`](https://github.com/armory3d/armorpaint/blob/main/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:

```c
// 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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/translator.c)** loads these JSON assets at runtime via `iron_load_blob()` and populates an in-memory translation table.
- **[`strings.c`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/extract_locales.js) is a QuickJS build tool located at [`base/tools/extract_locales.js`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/strings.c) does not directly parse JSON; instead, it relies on the translation table populated by [`translator.c`](https://github.com/armory3d/armorpaint/blob/main/translator.c). The `tr()` macro in [`strings.c`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/en.json), [`fr.json`](https://github.com/armory3d/armorpaint/blob/main/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.