Skip to content

Plugin Internationalization

This page explains how a plugin provides multi-language text: which fields are resolved, where resource files live, how the host selects a language and falls back, how runtime messages are formatted, and how to validate all of it with the Devkit.

Internationalization affects presentation only. Request parameters, returned song metadata, lyrics, written audio tags, and plugin behavior do not change with the language.

Text values: @ references and literals

Every user-visible text in the manifest (plugin name, description, setting titles, option text) is a plain string, and its prefix decides how it is read:

ValueMeaningShown in the UI
"@plugin.name"References key plugin.name in the language resourcesThe text for the current language, for example “示例搜索源”
"@@price"Plain text that begins with @Without one @, so @price
"Example Source"Plain textAs written; resources are not consulted

Three rules:

  • References resolve exactly once. A value inside a resource file is never treated as another reference, so "tip": "@plugin.name" is shown literally as @plugin.name.
  • A reference cannot be empty; "@" is invalid.
  • Using a reference requires an i18n declaration and minHostApiVersion of at least 4, otherwise installation fails.

Which fields are resolved

LocationResolvedNotes
manifest.nameYesPlugin name
manifest.descriptionYesPlugin description
Setting titleYesSetting title
Setting summaryYesText below the title
Setting groupYesThe UI groups by the raw value and shows the resolved text as the card title, see below
Option label, option summaryYesDropdown option text
defaultValue of a markdown settingYesBody text; keep the whole body in the resource
defaultValue of any other settingNoBusiness default such as "cn" or "true"; it cannot be translated
Setting key, option value, dependencyNoStable identifiers, must stay as written
id, entry, icon, author, versionCode, versionName, apiVersion, capabilitiesNoNever shown as settings text

group is both the grouping identity and the card title: the UI groups settings by the raw group string and shows its resolved text as the title. Every setting in one group must therefore use exactly the same group value (normally the same @group.xxx reference); never use one value per language. Settings with an empty group fall into the app's built-in “Basic” group.

Adding a language

This walkthrough adds Simplified Chinese.

1. Create the resource file locales/zh-Hans.json in the plugin root:

json
{
  "plugin.description": "示例搜索源插件",
  "config.region.title": "地区",
  "config.region.summary": "接口使用的地区代码",
  "group.request": "请求",
  "config.region.option.cn": "中国大陆",
  "config.region.option.us": "美国"
}

2. Replace the text you want translated with references and declare the resources in the manifest:

json
{
  "id": "com.example.source",
  "name": "Example Source",
  "description": "@plugin.description",
  "versionCode": 2,
  "versionName": "1.1.0",
  "apiVersion": 4,
  "minHostApiVersion": 4,
  "configFields": [
    {
      "key": "region",
      "title": "@config.region.title",
      "summary": "@config.region.summary",
      "group": "@group.request",
      "type": "dropdown",
      "defaultValue": "cn",
      "options": [
        { "value": "cn", "label": "@config.region.option.cn" },
        { "value": "us", "label": "@config.region.option.us" }
      ]
    }
  ],
  "i18n": {
    "defaultLocale": "en",
    "resources": {
      "en": "locales/en.json",
      "zh-Hans": "locales/zh-Hans.json"
    }
  }
}

Each of those fields is still a string: write a @ reference when the text should be translated, and a literal when it should not. A reference needs no copy of the original text — a missing translation falls back to the default resources. Language-neutral text such as a brand name or a size (for example Example Source, 500 × 500) stays a literal.

3. Create the default resources locales/en.json. The file named by defaultLocale must contain every referenced key, and it is the fallback for all other languages:

json
{
  "plugin.description": "Example source plugin",
  "config.region.title": "Region",
  "config.region.summary": "Region code used by the API",
  "group.request": "Requests",
  "config.region.option.cn": "Mainland China",
  "config.region.option.us": "United States"
}

4. Validate:

bash
node tools/plugin-devkit/src/cli.js validate ./my-plugin
node tools/plugin-devkit/src/cli.js inspect ./my-plugin --locales zh-CN

inspect --locales prints the resolved plugin name for the given language preferences; add --json to also see the resolved description and the full settings text, which confirms the translation took effect.

Resource file conventions

text
<plugin-root>/
├── manifest.json
├── source.js
├── locales/
│   ├── en.json
│   ├── zh-Hans.json
│   └── zh-Hant.json
└── icon.png
  • One UTF-8 JSON file per language, holding a key-to-string dictionary; values must be strings.
  • Use canonical BCP 47 tags such as en, ja, pt-BR, zh-Hans, zh-Hant; the tag is the key in resources.
  • Each file must stay under 512 KiB; a plugin may declare 1 to 64 languages.
  • Paths must be relative .json paths inside the plugin directory: no absolute paths, no \, no ...
  • Resources ship inside the plugin package; do not list them in includeDirs — they are not scripts.

Key names are free-form. Grouping them by where they are used keeps them maintainable:

PrefixPurpose
plugin.*Name and description
config.<setting key>.*Titles, summaries, and options (config.<key>.option.<value>, body text as config.<key>.content)
group.<group id>Group titles
error.*, warn.*, status.*Runtime messages, see below

Key rules:

  • The default resources must contain two kinds of keys: every key referenced with @, and every key that appears in any language file.
  • Translation files may omit keys; a missing key is looked up in parent resources and then the default resources. They must not add keys the default resources do not have.
  • For runtime messages with placeholders, the index and type of every %s and %d must match across languages.
  • In UI text, %, %s, and %20 are ordinary characters and are not placeholder-checked.

Language matching and fallback

The host tries the app's language preferences in order: exact tag match → a resource with the same language and script → a regional resource with the same script → defaultLocale.

For a plugin that only ships en, zh-Hans, and zh-Hant:

App languageSelected resourcesWhy
zh-CNzh-HansSame language and script
zh-TW, zh-HKzh-HantSame language and script
en-US, en-GBenExact tag or same language
de-DE, ja-JPenNo match, falls back to the default resources

Scripts (Hans for Simplified, Hant for Traditional, and so on) come from the system ICU data, so matching depends only on the resource tags; rare languages may differ between systems.

After a language is selected, a missing key is looked up in this order:

  1. The selected language file
  2. Parent resources: zh-Hant when the selection is zh-Hant-TW, for example
  3. The default resources

References are resolved when the UI renders, and the database keeps the raw manifest text, so switching languages needs no reinstall or migration.

Runtime messages

Messages that a script shows to the user also come from resources, read through Platform.i18n:

FunctionBehavior
Platform.i18n.getLocale()Returns the selected language tag, for example "zh-Hans"; returns "und" when the plugin has no i18n
Platform.i18n.t(key, ...args)Returns the text of key; formats placeholders when arguments are passed, and returns the text unchanged when none are
javascript
// locales/en.json:      "error.candidateFailed": "Candidate %1$s failed: %2$s"
// locales/zh-Hans.json: "error.candidateFailed": "候选 %1$s 获取失败:%2$s"
try {
  // ...
} catch (e) {
  Platform.log.warn("Example", Platform.i18n.t(
    "error.candidateFailed",
    String(song.title || song.id || ""),
    String(e && e.message ? e.message : e)
  ));
}

Placeholder rules:

  • %s, %d, %1$s, %2$d, and %% are supported, with up to 64 arguments.
  • A single argument may omit the index; multiple arguments must use positional indexes starting at 1, and indexed and unindexed placeholders cannot be mixed.
  • %s accepts strings only; %d accepts integers within JavaScript's safe range. Fractions, numeric strings, and empty values throw.
  • Pass arguments variadically, not as an array; the count must match the number of declared positions.
  • Translations may reorder or repeat an index, but must not add, remove, or retype arguments.
  • Floating point, width, date formatting, and plural rules are not supported.

Calling notes:

  • A key missing from the selected language, its parents, and the default resources throws Unknown plugin string: <key>; a wrong argument count or type throws as well, and the plugin may catch it.
  • Runtime keys are referenced only from scripts, so the manifest cannot check them: keep script keys and resource keys in sync. Any key declared in one language must also exist in the default resources, and keys with placeholders must match by signature.
  • Language must not drive business logic. Do not use getLocale() to pick request parameters such as an API language; make that a separate setting, otherwise switching the UI language changes search results and cache keys.
  • Platform.i18n is part of host API 4, so plugins that call it should declare minHostApiVersion: 4. See API Version History for the difference between the two version fields.

Validating with the Devkit

Run these commands from the plugin repository root:

bash
node tools/plugin-devkit/src/cli.js validate ./my-plugin
node tools/plugin-devkit/src/cli.js inspect ./my-plugin --locales zh-CN,en
node tools/plugin-devkit/src/cli.js test ./my-plugin searchSongs --keyword "晴天" --locales zh-CN,en
node tools/plugin-devkit/src/cli.js pack ./my-plugin
CommandWhat it checks
validateResource paths, canonical language tags, the presence of defaultLocale, coverage of every referenced key by the default resources, extra keys that the default resources do not declare, placeholder signatures of runtime keys, and the minHostApiVersion >= 4 requirement for references
inspect --localesPrints the resolved plugin name for the given preferences; --json also shows the resolved description and full settings text
test --localesRuns the real script and shows runtime message output in the target language
packShips locales/ inside the plugin package

See Debug Plugins Locally for all Devkit commands and its limitations.

Pre-release checklist

  1. Every text that needs translating uses a @ reference; language-neutral text stays a literal.
  2. The default resources cover every referenced key and declare nothing that other languages do not have.
  3. Every setting in one group uses exactly the same group value.
  4. Runtime message placeholders use the same indexes and types in every language.
  5. Every runtime key used by the script exists in the resource files.
  6. Plugins that use references or Platform.i18n declare minHostApiVersion: 4.
  7. validate passes, and inspect --locales and test --locales were run for the target languages.

FAQ

Q: The UI still shows @config.region.title. Why?

That key did not resolve. Check that i18n is declared, that defaultLocale is one of the resources keys, and that the default resources contain the key — a missing default key normally fails at installation, so a raw @ should never reach the screen. If the text really must begin with @, write @@....

Q: Why can some fields not be translated?

key, option value, dependency, and the defaultValue of ordinary settings are stable identifiers or business values, and the host never resolves them. If a default needs multiple languages, turn it into a dropdown option and put the text in its label.

Q: How do I translate a markdown setting?

Treat the body as an ordinary resource: set "defaultValue": "@config.help.content" and write the body under config.help.content in every language file. Markdown syntax such as #, -, and links stays inside the resource value.

Q: May translations omit keys?

Yes. Missing keys fall back to parent resources and then the default resources. They must not add keys that the default resources do not have — usually a typo, which validate reports directly.

Q: Will pasting text that contains % fail validation?

Not in UI text: only runtime resources with recognizable %s/%d placeholders are signature-checked, so 100% and %20 are treated as ordinary characters. To show a literal percent sign in a runtime message, write %%.

Q: How do I debug a key that does not take effect?

Run validate first, which names the key and the reason; then use inspect --locales <tag> to see the resolved text, and test --locales to see runtime messages. Unknown plugin string: <key> in the logs means the script references a key that the resources do not contain.

Q: A user renamed the plugin. Does that override the translation?

A user-defined name wins over the translation; every other text still follows the language.

Q: What happens to older plugins without i18n?

They keep using the raw manifest text and behave exactly as before. i18n is only required once a field contains a @ reference.