Languages and Translations

Horizon UI ships with eight locales:

Locale Native name
en English (source)
de Deutsch
es Español
fr Français
ja 日本語
ko 한국어
pt Português
zh-CN 中文(简体)

In the picker English sits on its own line at the top; the other seven are listed alphabetically by code so additions slot in predictably.

English is the immovable default. Every translatable string is authored in English first; the other locales are catalog overlays. Missing keys fall back to English at the leaf, never at the file — a half-translated catalog renders strictly better than English-only.

Coverage by scope

All eight languages are first-class across the surfaces Horizon controls. The scope decides where the translation resolves:

Scope Resolves Languages
UI chrome — buttons, labels, modals, login, topbar, sidebar In the browser (vue-i18n) All 8
Bundled layer-dashboard templates On the BFF, from sibling overlay catalogs All 8
Bundled overview templates On the BFF All 8
User-maintained dashboards saved to OAP On the BFF, from per-language translation overlays stored on OAP English + whatever the author provides
OAP-supplied data — service / instance / endpoint / alarm names, tags, log lines, trace ops not translated rendered verbatim

A missing key in any translated scope falls back to English at the leaf, so a partially-translated catalog renders better than English-only.

Picking a language

The locale picker lives in the topbar (next to the theme chip) and on the login page. Picks are stored in your browser’s local storage; the choice follows you across logout and back. A fresh device — one that has never set the locale picker — starts in English. The browser’s Accept-Language is not consulted; the project policy is “English by default, opt in to other locales via the picker” so a user on a non-English browser sees a known starting state and can switch explicitly.

Switching language re-renders the chrome immediately and triggers a silent re-fetch of any server-side content that ships translatable text (sidebar entries, layer dashboards, overview dashboards) so the new locale takes effect without a page reload.

What is and isn’t translated

Translated Not translated
UI chrome (buttons, labels, modals, time picker, login page) OAP-supplied data: service names, instance names, endpoint names, alarm rule names, tag values, log messages, trace span names
Bundled layer-dashboard templates (widget titles, KPI labels, group titles, tooltips) Layer keys (GENERAL, MESH, K8S_SERVICE, …)
Bundled overview templates Metric ids, MQE expressions
User-maintained templates (via per-language overlays on the Translations page) Units (ms, rpm, %) — abbreviated technical conventions
Layer alias and aliases.* (the display labels we author) OAP scope enums (Service, ServiceInstance, Endpoint, Process)
A layer’s naming-rule dimension label — the caption the topology and 3D map print beside a cluster (namespace, tenant) The naming rule’s pattern and its capture-group names
A tab widget’s panel names Widget ids — id names the widget a translation belongs to, so a value-map entry keyed exactly id is left in English

Product and project names — SkyWalking, Kubernetes, Envoy, Istio, OAP, MQE, eBPF, Zipkin, OpenTelemetry, gRPC, Apache — are kept in their original form across every locale. Same for technical abbreviations like RPM, SLA, Apdex, P50P99. Operators read these terms across docs, source, and other SkyWalking surfaces; they need to stay recognizable.

AI-assisted seeds

Initial translations for the seven non-English locales — de, es, fr, ja, ko, pt, zh-CN — were seeded with AI assistance. Modern models handle SkyWalking’s technical vocabulary competently, but native speakers may spot phrasings that could read more naturally. Pull requests with corrections are welcome — target the matching catalog file:

  • UI chrome: apps/ui/src/i18n/locales/<locale>.json
  • Shared widget vocabulary (the “lexicon”): apps/bff/src/i18n/lexicon/<locale>.json
  • Bundled layer dashboards: apps/bff/src/bundled_templates/layers/<key>.i18n.<locale>.json
  • Bundled overview dashboards: apps/bff/src/bundled_templates/overviews/<id>.i18n.<locale>.json

Translating a custom dashboard

Translations are not embedded in the dashboard you author. The English source dashboard (layer or overview) stays single-language; each locale is a separate translation overlay stored on OAP and edited on the Translations page, one language at a time.

To translate a dashboard you’ve published:

  1. Open the Translations page, pick the template and the Target language. The fields start from what that language renders today — the version published to OAP, or your own staged local draft where you have one. If nothing is published yet the fields start empty, because that language renders English; Reset to → bundled loads the shipped catalog as a starting point you can review and publish.
  2. Fill the translated fields — you fill only the fields you want translated. Anything you leave blank falls back to English at the leaf.
  3. Check diff & push shows a side-by-side diff of your draft against the version live on OAP and publishes the overlay. A template you haven’t edited shows no pending change; anything with no matching field in the source is dropped — the source is the schema.

A translation is attached to its widget, not to the widget’s place in the grid. Reordering, inserting or deleting widgets in the English source leaves every other widget’s translation exactly where it was; the widget you deleted simply takes its translation with it, and a widget you added starts out English until you fill it in. The same holds for the widgets inside a tab panel. A handful of lists have no per-entry identity — a widget’s expression labels and table headers, an overview tile’s KPI rows, a tab widget’s panels — and those still follow their position, so reordering them moves the text with the slot.

The overlay never travels inside the dashboard JSON, so a dashboard exported to another OAP arrives with its English source only; move its translations across on the Translations page if you need them there. Because a translation names its widget, a file exported from one OAP still lands on the right widgets after an import even if the target’s dashboard has since been re-laid-out.

Import / Export

On the Translations page, Export downloads the in-use translation for the picked template and Target language — the version live on OAP, or the shipped seed — as a JSON file. Import reads such a file and loads it as a local draft for that language; review it in the preview, then Check diff & push to publish. Import never writes OAP directly.

Each language is its own file and its own row, so you export/import one language at a time. Source templates and their translations are edited on separate pages, so their import/export are separate too: a template’s export carries the English source only — move its translations across here, per language.

Adding a new locale

Adding a locale Horizon doesn’t ship yet — say Italian (it) — is three steps:

  1. Add the locale to the Locale union and SUPPORTED_LOCALES list in both apps/ui/src/i18n/index.ts and apps/bff/src/i18n/types.ts.
  2. Drop in the catalog files:
    • apps/ui/src/i18n/locales/<locale>.json (UI chrome)
    • apps/bff/src/i18n/lexicon/<locale>.json (shared widget vocabulary)
  3. From apps/bff, run pnpm i18n:seed -- --locale <locale> to generate sibling overlays for every bundled layer / overview template. Fill the gaps that the lexicon couldn’t cover.

Two gates catch drift, and CI runs both. Run them together with:

pnpm lint:i18n

The template overlay half (pnpm --filter @skywalking-horizon-ui/bff i18n:validate) rejects catalog entries that no longer match the source template — a field the source has dropped, a widget it no longer has, two entries claiming the same widget — plus non-string values at translatable paths, and lexicon entries that aren’t present in the source en.json lexicon registry.

The UI catalog half checks apps/ui/src/i18n/locales/*.json and fails on four things: a string the UI asks for that en.json doesn’t carry; a Cluster Status module description that en.json doesn’t carry (those are authored on the server side, so no scan of the UI can find them); a key left in en.json that nothing renders any more, which every locale would otherwise carry forever — delete it from all eight catalogs; and any locale whose key set has drifted from en.json.

When you add a new layer, generating its template’s translations is part of that workflow — see Adding a New Layer.