Interface Localisation
About 554 wordsAbout 2 min
The M9A project text rendered by clients — task names, option labels, descriptions, presets, resource and controller names — comes from interface.json and the files under tasks/. Those strings are localised through the MaaFramework Project Interface V2 i18n mechanism.
How it works
interface.json declares the available translation files:
"languages": {
"zh_cn": "i18n/zh_cn.json",
"en_us": "i18n/en_us.json"
}Paths are relative to the directory holding interface.json. M9A keeps these Project Interface assets in the root-level i18n/ directory rather than resource/, which is reserved for MaaFW runtime resources. Each translation file is a flat map of key to translated text:
{
"Task.Wilderness": "Collect Wasteland",
"Option.Wilderness.Wellspring": "Wellspring"
}Any i18n-capable field whose value begins with $ is treated as a key into those files. The client resolves it against the active language. Missing-key fallback is client-dependent and may expose the key or fall back to the stable name, so every reference must be covered by pnpm check:i18n.
{
"name": "收取荒原",
"label": "$Task.Wilderness",
"entry": "Wilderness"
}name is an ID, not a label
name is the stable identifier. It is written into user configuration files, referenced by preset task lists and by the option arrays on each task, so renaming it breaks existing user configurations. It stays as-is — in Chinese — and the display string is supplied by label instead.
The same applies to option map keys and to cases[].name. Add a label beside them; never rewrite the key itself.
A case whose name already reads the same in every language (Yes, No, 24h, MAX, a bare number) does not need a label — the client falls back to name. It may still need a key for its description.
Fields that take a key
label, description, desc, pattern_msg, icon, doc, plus the project-level title, contact, license and welcome. Note that pipeline_override is game-facing pipeline data and is never translated.
Key naming
Keys are ASCII and dot-separated, mirroring where the string appears:
| Kind | Pattern | Example |
|---|---|---|
| Task | Task.<Name> | Task.BalancedFarming |
| Task description | Task.<Name>.desc | Task.BalancedFarming.desc |
| Option | Option.<Task>.<Option> | Option.Combat.StageType |
| Option case | Option.<Task>.<Option>.<Case> | Option.Combat.StageType.MainStory |
| Option input | Option.<Task>.<Option>.<Input> | Option.Combat.StageCustom.Chapter |
| Preset | Preset.<Name> | Preset.DailyIdle |
| Interface level | Controller.*, Resource.*, Group.* | Resource.GlobalEn |
Append .desc for a description and .msg for a pattern_msg. Strings shared by several options (for example the formation slot numbers) use a Common.* key so they are translated once.
Adding a new task
- Write the task in
tasks/as usual, keepingnamein Chinese. - Add
"label": "$Task.<Name>", and"description": "$Task.<Name>.desc"if it has one. - Add the matching entries to both
i18n/zh_cn.jsonandi18n/en_us.json. - Run
pnpm check:i18n.
Validation
pnpm check:i18n runs as part of pnpm check and fails the build on:
- a
$keyreference with no entry in one of the language files - an entry in a language file that nothing references any more
- the two language files covering different key sets
- hard-coded Chinese remains in any i18n-capable field
Adding a language
Add the file to the languages block in interface.json, copy i18n/zh_cn.json as a starting point and translate the values. The validator requires the new file to cover exactly the same key set as the existing ones.
