Locale overlays

A curriculum can ship in more than one written language without maintaining separate copies of the whole content tree. English (or whichever language the curriculum was authored in) stays canonical; every other language is a sibling overlay. apprenti.dev's own reference curriculum already ships this way — English and Turkish, live today.

Content locale is not the app's UI language

These are two independent settings, and it's worth being precise about the difference:

  • Appearance language — the app's own chrome: buttons, navigation, labels. Set once, applies everywhere, has nothing to do with curriculum content.
  • Content locale — which language a curriculum's own content (titles, term headings, instructions) displays in. Chosen per curriculum, on the curriculum picker.

When an apprentice first attaches a curriculum, its content locale seeds from the current Appearance language — but only if that language is one the curriculum actually offers. After that first attach, the two settings are fully decoupled: switching Appearance later does not reload the Path or re-resolve task content into a different language. Don't assume a Turkish app UI means an apprentice wants Turkish task content, or the reverse — the picker is the only source of truth for content locale.

The pattern

For any canonical file, an overlay for locale tr has the same name with .tr inserted before the extension, sitting right next to it:

curriculum.json
curriculum.tr.json
curriculum/year-1/term-1/term.json
curriculum/year-1/term-1/term.tr.json
curriculum/.../y1t1-002/task.json
curriculum/.../y1t1-002/task.tr.json
curriculum/.../y1t1-002/instructions.md
curriculum/.../y1t1-002/instructions.tr.md

Never copy the curriculum folder into parallel per-locale folder trees, and never rename a canonical file to mark it as the English version — both break the overlay model and duplicate every task id.

Declaring which locales exist

On the manifest:

{
  "sourceLocale": "en",
  "contentLocales": ["en", "tr"]
}

Both are optional — no source locale means English; no declared content locales means "use overlays if present." This is separate from the field that lists programming languages the curriculum teaches (Python, C, …) and must never be reused for human locales.

What can and cannot be overlaid

Overlay files are sparse — only the translatable fields need to be present. Titles, objectives, "why this matters," evidence text, and acceptance criteria are typical overlay content.

Never overlay any of the following — these are identity and structure, not prose. apprenti creator's translator-facing fields disable them outright, and validation rejects an overlay that tries to write one anyway:

  • id
  • schemaVersion
  • term
  • module
  • estimatedHours
  • aiPolicy
  • instructionsFile
  • statusModel
  • any competency/resource id
  • any folder path
  • any URL

The URL restriction has one deliberate exception: an overlay may supply a genuinely localized resource URL (a translated article at a different address, say). Never translate a folder slug either — a term folder like "year-1/term-1" stays exactly that in every locale.

An overlay whose id doesn't match the canonical file's id is simply ignored by the app — Creator flags this as an error rather than letting a mismatched file silently do nothing.

Fallback

At read time: an overlay field that's present and non-empty wins; otherwise the canonical value is used. A missing overlay file is not an error — a repository with no translations at all behaves exactly as it always has.

List fields — evidence and acceptance criteria — don't merge item by item. A present, non-empty overlay list replaces the whole list. Search stays aware of both sides too: it indexes the resolved (locale-aware) title and instructions alongside the original English title, so switching content locale never makes a task unfindable.

Authoring overlays in apprenti creator

The locale board gives a coverage view — one column per configured locale, one row per manifest/term/task — colored green (complete), amber (missing, which is legal), or red (an id mismatch or an empty required key, which is not). The task editor's locale column lets a translator work source and translation side by side, field by field, with identity fields locked out.

A note on voice

When writing translations, keep Git terminology (commit, branch, merge, rebase, clone, pull, push, fork, working tree) in English regardless of target language — apprentices are expected to know these terms in their canonical form. Keep task ids, commands, code fences, and file names untouched. Empty overlay values are not acceptable placeholders; leave a field out of the overlay entirely rather than writing an empty string.