Skip to content

Localization

APHONIC edited this page Sep 15, 2026 · 2 revisions

Localization

Localization.lua: a plain-table fallback lookup, and native ESO string-ID registration for addons that want the engine-native approach. Two different weights of the same job; pick whichever fits.

LibAPH.Localize(langTable, langCode, key, ...)

The simple option: a pure lookup function, no engine registration at all.

Chain: override language → client language (as resolved by the caller into langCode) → English → the raw key itself (so a typo'd key shows as the key instead of erroring). Extra varargs go through string.format.

function MyAddon.L(key, ...)
    local lang_code = MyAddon.settings.override_language or GetCVar("Language.2")
    return LibAPH.Localize(MyAddon.Lang, lang_code, key, ...)
end

langTable is your own {code -> {key -> string}} table, populated by your own lang/*.lua files: this function never touches those files, just the fallback chain over whatever you hand it.

LibAPH.LoadLocalization(addonPrefix, langTable, defaultLang, overrideLang)

Registers strings into ESO's native ZO_CreateStringId/SafeAddString/GetString() system, instead of the plain lookup table Localize above uses.

The collision problem this solves: ZO_CreateStringId(name, value) creates a global Lua variable, shared across every addon in the whole game session. Two unrelated addons' language files commonly reuse the same generic key name, like FIELD_WIZARD or BTN_CLOSE. Say MyAddon1 and MyAddon2 both ship a BTN_CLOSE key: feeding either raw table straight into the string-ID system unprefixed means whichever addon registers second silently overwrites the first one's text for that key. addonPrefix ("SI_MYADDON1_", "SI_MYADDON2_") is mandatory specifically to prevent this, since prefixing turns one shared global into two separate ones before registration ever happens.

Mechanism: prefers the separately-installed LibLanguage library if present (declared OptionalDependsOn, not required). Its SafeAddString wrapper works around a ZOS engine quirk where SafeAddString's own version-protection isn't reliably enforced. Falls back to registering directly via ZO_CreateStringId/SafeAddString if LibLanguage isn't installed, so this never hard-requires it.

overrideLang is a third registration pass, for a player-chosen language that isn't necessarily their client's own (an in-menu Language dropdown letting a player pick any contributed language regardless of client language), registered at version 3, above the default (1) and client-detected (2) passes, so it wins over both.

-- your own L() wrapper, called by every existing call site unchanged:
function MyAddon.L(key, ...)
    local id = _G["SI_MYADDON_" .. key]
    local str = id and GetString(id) or key
    if select("#", ...) > 0 then return string.format(str, ...) end
    return str
end

-- once, in init() after MyAddon.settings exists:
LibAPH.LoadLocalization("SI_MYADDON_", MyAddon.Lang, "en", MyAddon.settings.override_language)

Note: this doesn't require the $(language) manifest-token pattern (loading only the client's matching language file): both addons deliberately load every lang/*.lua file unconditionally instead, because a $(language)-gated manifest only ever loads one language's table, which breaks a manual override dropdown that needs every language available to switch into.

LibAPH.BuildLanguagePickerControls(opts)

Returns a ready-made LAM2 control list: a "current language" description, plus a dropdown and (gamepad only) an Apply button to override the client-detected language.

opts: L (your localize function), settings, addonPrefix, langTable, getAvailableLanguages, getLanguageDisplayName, formatCurrentLanguageText, isPad, reference, getPending/setPending (gamepad's pending-choice state).

local controls = LibAPH.BuildLanguagePickerControls({
    L = MyAddon.L,
    settings = MyAddon.settings,
    addonPrefix = "SI_MYADDON_",
    langTable = MyAddon.Lang,
    getAvailableLanguages = MyAddon.get_available_languages,
    getLanguageDisplayName = MyAddon.get_language_display_name,
    formatCurrentLanguageText = MyAddon.format_current_language_text,
    isPad = IsInGamepadPreferredMode(),
})
for _, c in ipairs(controls) do build_data[#build_data + 1] = c end

Home · Getting Started

Clone this wiki locally