-
Notifications
You must be signed in to change notification settings - Fork 1
Locales and Translations
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).
- How it works
- Adding a new language
- Character encoding
- Fonts and non-Latin scripts
- Translating What's New content
- Text-based achievements — nothing to translate
- Validating your work
- Localising the title database
-
The machinery lives in
Locales/LocaleManager.lua, and it's locale-agnostic — it defines thens.Localesregistry, thens.Lproxy, 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 noGetLocale()guard, so every shipped language is always available to pick from, regardless of the player's actual game-client language.enGBis the base and always loads. Note thatGetLocale()returns"enUS"for both US and GB clients, soenGBcovers both. -
ns.Lis 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 thannil— easy to spot in-game if something's wrong. -
The active locale is a player preference, not the client's language.
Epithet:OnInitializeapplies the saved choice (EpithetDBgloballocale, default"auto"— account-wide, mirrors the client's language) vians.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.
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-
Copy
Locales/enGB.luatoLocales/<locale>.lua(e.g.deDE.lua). Valid locale codes:deDE,esES,esMX,frFR,itIT,koKR,ptBR,ruRU,zhCN,zhTW. -
Change the registration line at the top from
ns.Locales.enGB = Ltons.Locales.<locale> = L. That's the only structural change — everything else is translated content. -
Register the language in
LocaleManager.lua: add the code plus its own-script display name toLOCALE_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 aLANGUAGE_<NAME>key to every locale file and map the code to it inLANGUAGE_NAME_KEYS. -
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. -
Add the file to
Epithet.tocunder# Locale, afterenGB.lua. -
Add a
## Notes-<locale>line toEpithet.tocfor the localized addon description shown in the client's addon list. - Run the validator (below) and fix anything it flags before opening a Pull Request.
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.
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.
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.
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.
powershell -File scripts\validate\validate-locales.ps1This 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.
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'stools/schema.jsonlocaleOverlaysection 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.luais 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 bytitleID— 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.ps1This 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.