One catalogue says what this client displays. An entry names the string the client would have drawn and the surface it is drawn at. A translation is a JSON file your addon ships, loaded from Lua. No permission. Client-local. Releasing it or disabling your addon puts the client’s English back.
hafen.locale():load{
text = {
button = { Cancel = "Cancelar" },
["window.title"] = { Inventory = "Inventario" },
},
}:install()
Reload, open a window, and the caption is in your words. hafen.locale():release() restores the client’s own.
| Rule | Detail |
|---|---|
| One catalogue per addon | Handed back by identity from hafen.locale(). :install() replaces the displayed one whole. :load(document) on an installed catalogue is what the client says from that line on: no re-apply verb, nothing to restart. |
A key is the string as the surface composed it. A tooltip row carries the number the player’s item put in it. A System line carries the name of the addon that wrote it. So write the file from what the client offered. Install a catalogue that names nothing, play, and read back what missed.
hafen.locale():load({}):install() -- names nothing, and records everything that missed
hafen.console():on("dump", function()
local catalogue = hafen.store():var("catalogue")
for _, miss in ipairs(hafen.locale():miss():list()) do
catalogue[miss:surface()] = catalogue[miss:surface()] or {}
catalogue[miss:surface()][miss:text()] = miss:text() -- English to English, ready to edit
end
hafen.log():write(hafen.json():encode({ text = catalogue })) -- the file, on the terminal
end)
| Rule | Detail |
|---|---|
| The sweep | Open the windows you mean to translate, hover the items, right-click for a menu, type :dump. The terminal shows a JSON object keyed as a catalogue’s text is, every string doubled. Translate the right-hand side. |
| Kept in a var | Reached through hafen.store(), since the client’s strings are not one character’s. The live table grows across sweeps. |
| Rounds | :install() starts a fresh round, so re-installing between sweeps tells you what one sweep reached. locale:miss() is per addon, bounded, and holds neither a string yours matched nor anything an addon drew, so the same command is your progress report. |
Nothing in a catalogue is a handle or code, so the document is JSON your addon ships.
{ "text": {
"button": { "Cancel": "Cancelar", "Buy": "Comprar" },
"window.title": { "Inventory": "Inventario", "Character Sheet": "Hoja de personaje" },
"menu": { "Pick": "Recoger", "Destroy": "Destruir" },
"*": { "Water": "Agua" }
},
"pattern": [
{ "surface": "tooltip", "match": "Quality: (\\d+)", "text": "Calidad: %1$s" }
]
}
| Rule | Detail |
|---|---|
| The key is the surface | The surface’s own key is looked up first, "*" after it has missed. The surfaces a catalogue names draw text. A key naming a window’s frame or a checkbox raises listing them. |
| A tooltip is many rows | An item’s name, quality, wear and each bonus reach tooltip as separate strings. An entry names one row. Most are drawn by code inside the resource, keyed and answered like a button caption. |
A row carrying a number is a different string every time, so a pattern names its shape. A pattern is the surface, a match for the whole string, and the text to draw with %1$s where the first capture group goes.
hafen.locale():load{
pattern = {
{ surface = "tooltip", match = "(\\d+) uses left", text = "quedan %1$s usos" },
{ surface = "heading", match = "(\\w+) of (\\w+)", text = "%2$s: %1$s" },
},
}:install()
| Rule | Detail |
|---|---|
| Groups by number | %2$s is the second group wherever it stands, so your language orders the parts. |
| An ordered list | Two patterns can describe one string and position says which wins. Every exact key is consulted first. |
Write match tightly |
A pattern reaches a whole string, not a word. One wide enough to catch a composed line catches a typed one too. That is why a catalogue keys on button and menu rather than "*". |
local document = hafen.json():parse(hafen.asset():get("es.json"):text())
hafen.locale():load(document):install()
hafen.asset reads the file out of your folder as text, hafen.json parses it. A language picker is one addon choosing which file to read. locale:info() says whether yours is in force and how much it holds. Two addons’ catalogues stack, the last installed winning per entry. A string yours does not name falls through to the one beneath rather than to English.
| Not reached | Detail |
|---|---|
| Free text | A chat body, a kin name, a player’s words: a different string every time, so only a pattern around the varying part comes close. |
| What the user types | No catalogue matches an entry field, "*" included. textentry is refused as a key. |
| The model | A catalogue lands at the render. widget:text(), a [title=] selector, an action’s name and a petal’s answer the client’s English while yours is installed. That lets an addon that reads a caption and one that translates it run side by side, and it is why locale:miss() exists. |
Next: debugging — the reload loop, the inspector, and finding out why a string did not change.