Skip to content

Locales and Translations

Joe edited this page Aug 5, 2026 · 1 revision

Epithet is fully localizable, and its locale system is not AceLocale — it's a small, purpose-built registry. This page explains how it works and walks through adding or updating a language. It mirrors docs/LOCALIZATION.md in the repo, which is the source of truth if the two ever drift.

Epithet currently ships English (enGB, which also serves enUS), Russian (ruRU), and French (frFR).

Contents

How it works

  • The machinery lives in Locales/LocaleManager.lua, and it's locale-agnostic — it defines the ns.Locales registry, the ns.L proxy, the resolution API (ns.ApplyLocale, ns.ResolveLocaleCode, ns.GetAvailableLocales, ns.GetLocaleDisplayName), and shared punctuation glyphs (ns.Glyphs). It loads first, before any locale data file. Locale files themselves (enGB.lua, ruRU.lua, frFR.lua, …) contain only strings — no logic.
  • Every locale registers itself into ns.Locales[<code>]. Each file builds a plain table and assigns it — there's no GetLocale() guard, so every shipped language is always available to pick from, regardless of the player's actual game-client language. enGB is the base and always loads. Note that GetLocale() returns "enUS" for both US and GB clients, so enGB covers both.
  • ns.L is a read-only proxy, not a plain table. A lookup resolves in order: active overlay → English base → the key name itself. Untranslated keys silently fall back to English, and a key missing from every locale returns its own name rather than nil — easy to spot in-game if something's wrong.
  • The active locale is a player preference, not the client's language. Epithet:OnInitialize applies the saved choice (EpithetDB global locale, default "auto" — account-wide, mirrors the client's language) via ns.ApplyLocale. Players can override this from Options → Epithet → Language, and since every locale is always registered, the picker can offer any shipped language on any client. Changing it prompts a UI reload, since already-drawn text doesn't re-render live.

Load order matters

In Epithet.toc, LocaleManager.lua must come first (it defines the registry and proxy), then enGB.lua (the base every other locale falls back to), then everything else:

# Locale
Locales\LocaleManager.lua   # machinery — must be first
Locales\enGB.lua            # base — before other locales
Locales\ruRU.lua
Locales\frFR.lua
Locales\<yours>.lua

Adding a new language

  1. Copy Locales/enGB.lua to Locales/<locale>.lua (e.g. deDE.lua). Valid locale codes: deDE, esES, esMX, frFR, itIT, koKR, ptBR, ruRU, zhCN, zhTW.
  2. Change the registration line at the top from ns.Locales.enGB = L to ns.Locales.<locale> = L. That's the only structural change — everything else is translated content.
  3. Register the language in LocaleManager.lua: add the code plus its own-script display name to LOCALE_NAMES (the picker's fallback). If you'd like the name to follow the active UI language instead — e.g. showing "German" in an English UI but "Немецкий" in a Russian one — add a LANGUAGE_<NAME> key to every locale file and map the code to it in LANGUAGE_NAME_KEYS.
  4. Translate the right-hand side of each L["KEY"] = "..." line. Leave the keys themselves unchanged. You can safely delete lines you haven't translated yet — they'll fall back to English rather than break.
  5. Add the file to Epithet.toc under # Locale, after enGB.lua.
  6. Add a ## Notes-<locale> line to Epithet.toc for the localized addon description shown in the client's addon list.
  7. Run the validator (below) and fix anything it flags before opening a Pull Request.

Character encoding

WoW's Lua 5.1 does not support \xNN hex escapes. Write UTF-8 text directly in the file (as ruRU.lua and frFR.lua already do), or use decimal byte escapes for punctuation glyphs, e.g. \226\128\148 for an em dash. The validator flags any \x escape as an error.

Fonts and non-Latin scripts

The game client's own fonts (FRIZQT__.TTF, MORPHEUS.TTF) only contain glyphs for that client's own region. So when a player uses Epithet's language override to run the addon in a script their client font doesn't have — Russian on a Western client, for instance — text would render as empty boxes without help. Epithet works around this by loading a bundled Unicode font for those locales; see Fonts/README.md in the repo for which files to add and recommended OFL-licensed fonts. The locales that need a bundled font are listed in BUNDLED_FONT_LOCALES in Core/Theme.lua (currently ruRU). Western-European locales render fine on the client's own fonts; CJK scripts aren't bundled (the font files are too large) and only render correctly on a matching client.

Translating What's New content

Per-version release notes live in WhatsNew/Versions/*.lua. Their title and body fields can be either a plain string or a table keyed by locale:

title = { enUS = "Epithet 1.3.0", ruRU = "Epithet 1.3.0" },
body  = { enUS = [[ ... ]], ruRU = [[ ... ]], frFR = [[ ... ]] },

WhatsNew:LocalizeField resolves the active locale and falls back to enUS, so adding a translation is purely additive — just drop in a new locale key alongside the existing ones.

Text-based achievements — nothing to translate

Five Spotting achievements are decided by a title's text: lord_of_lords, masterclass, and slay look for a keyword in it; quite_a_mouthful and terse measure its length. These match the title's English catalogue name, not the name shown on screen, so they need no per-locale data and behave identically on every client — the Spotting Log is keyed by titleID (Blizzard's numeric ID, stable across every locale), and the bundled title database is always English.

That means the search words themselves are plain constants and never need translating. Only the achievement's displayed name and description are translated in the locale files. Because the match happens on the English name while the player sees a localised one, a non-English description should say so explicitly rather than naming a translated word — for example, the French description for Lord of Lords reads "Repérez 5 titres distincts dont le nom anglais contient « Lord »" ("...whose English name contains «Lord»"), not a French keyword.

Validating your work

powershell -File scripts\validate\validate-locales.ps1

This reports, per locale: translation coverage, untranslated keys (a warning — they simply fall back to English) and orphan keys not present in enGB (an error — usually a typo or a stale leftover key). It also lints every locale file for unsupported \x hex escapes. The script exits non-zero on any error, so it's suitable for gating CI.

Localising the title database

The title database is separate from the addon's own UI strings above, but follows the same base-plus-overlay shape.

Both the base file and its overlays are collector-generated by a sibling repo (TitlesDBCollector) — see that repo's tools/schema.json localeOverlay section and README. The field list and output shape are a shared contract between the two repos: changing one side without the other breaks translations silently until someone runs the validator below.

  • data/TitlesDB.enGB.lua is the canonical base: the full English dataset, keyed by title text, carrying every field. It's gitignored (collector output, not hand-edited here).
  • data/TitlesDB.<code>.lua (e.g. TitlesDB.ruRU.lua) are sparse overlays keyed by titleID — Blizzard's numeric ID, identical across every client locale, so it's the stable join key. An overlay only needs to translate the free-prose fields Epithet actually displays: obtainability_reason, achievement, quest, source_item. Any titleID or field an overlay omits falls back to the English base.

Not translated in an overlay: title names themselves — Epithet shows those via GetTitleName(), which the game client already localises, so they match the nameplate the player sees — and language-neutral data (type, q, exp, cat, kind, obtainable, …). The expansion/category/kind labels shown in the UI are translated in the addon's own locale files, since the database only ships stable codes, not display labels.

How it's wired: each overlay registers into ns.TitlesDBLocales[<code>] (Core/TitleData.lua). ns.ResolveTitlesDBOverlay() picks the overlay matching the active display language, and TitleData:Scan() prefers an overlay's field, falling back to the base. A language switch triggers a UI reload, so the scan simply re-runs fresh — no live-refresh path is needed.

Adding an overlay: copy data/TitlesDB.ruRU.lua.example to data/TitlesDB.<code>.lua, fill in its byID table, add it under # Bundled Data in Epithet.toc after the enGB base, then validate:

powershell -File scripts\validate\validate-titlesdb-locales.ps1

This reports, per overlay: coverage (how many titles are translated) and orphan titleIDs not present in the base (an error). It also lints for \x hex escapes and stray null literals (Lua has no null — it silently becomes nil, which is easy to miss). The script exits non-zero on any error, so it's suitable for gating CI.