207 lines
13 KiB
Markdown
207 lines
13 KiB
Markdown
# Translating wayBetter for X
|
|
|
|
wayBetter for X speaks the languages X's own website speaks. Every language starts as a machine
|
|
draft; people who use the app in their language make it right. This page is for them, and for
|
|
developers adding text to the app.
|
|
|
|
## Which languages
|
|
|
|
Exactly the UI languages X's web client offers, so "Follow X" in Settings → Language always finds a
|
|
match. The source is X's own client: the locale chunk loader in
|
|
`abs.twimg.com/responsive-web/client-web/main.<hash>.js` (`loadLanguage.registerChunkLoader([...])`),
|
|
read on 2026-09-29. It lists 50 languages besides English: Arabic, Arabic (Feminine) `ar-x-fm`,
|
|
Bangla, Basque, British English, Bulgarian, Catalan, Chinese (Simplified and Traditional), Croatian,
|
|
Czech, Danish, Dutch, Filipino, Finnish, French, Galician, German, Greek, Gujarati, Hausa, Hebrew,
|
|
Hindi, Hungarian, Igbo, Indonesian, Irish, Italian, Japanese, Kannada, Korean, Malay, Marathi,
|
|
Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Serbian, Slovak, Spanish, Swedish, Tamil,
|
|
Thai, Turkish, Ukrainian, Urdu, Vietnamese and Yoruba. (X's pseudo-locales `en-ss` and `en-xx` are
|
|
test builds, not languages. X's newer logged-out pages, `abs.twimg.com/x-web/…`, ship message sets
|
|
for 44 of them so far.)
|
|
|
|
X's own codes map to standard tags: `msa` → `ms`, `no` → `nb`, `zh-cn` → `zh-Hans`, `zh-tw` →
|
|
`zh-Hant`, `en-gb` → `en-GB`. `apps/desktop/tests/i18n.test.ts` fails if a catalog is added or
|
|
missing compared with this list; when X adds a language, update the list there and here.
|
|
|
|
### Tiers: supported and community
|
|
|
|
The app keeps all of these languages, in two tiers (`tier` in the generated `languages.json`):
|
|
|
|
- **Supported** (English, British English, Japanese, Spanish, Arabic, Indonesian, German,
|
|
Portuguese, Turkish, French, Thai, Korean, Dutch, Vietnamese, Traditional Chinese, Italian,
|
|
Hindi and Russian). "Follow X" and "Follow system" resolve to these, through the usual fallback chain
|
|
(`pt-BR` → `pt`, `zh-TW`/`zh-HK` → `zh-Hant`, `en-AU` → `en`). In Settings → Language they are
|
|
listed first.
|
|
- **Community**: every other language. These are machine drafts nobody has reviewed. They are listed
|
|
below the supported ones under "Community translations", with the Help translate link, and are
|
|
used only when someone picks them explicitly (a pick applies whatever its tier). If X or the
|
|
system is set to a community language, the app stays on English and Settings → Language says
|
|
"<Language> is available as a community translation" with a button to switch to it.
|
|
|
|
The source of truth is `supported` in `waybetter.i18n.json` (English is always supported; without the
|
|
list every language is). `node scripts/i18n-check.mjs --write` generates `tier` into `languages.json`,
|
|
and the check fails if `supported` names a language without a catalog.
|
|
|
|
**Promotion.** A community language becomes supported when a native speaker has reviewed it: the
|
|
catalog gets `"status": "reviewed"` and the reviewer in `reviewers` (see below), and a maintainer adds
|
|
its code to `supported` in `waybetter.i18n.json` and re-runs the check with `--write`.
|
|
|
|
## Where the words live
|
|
|
|
| File | What |
|
|
| --- | --- |
|
|
| `apps/desktop/locales/en.json` | The English source: every string of the shell, the panel, the drafts popover, the X runtime (`x.*`), bundled plugins (`plugin.<file>.*`) and native pieces (`native.*`). |
|
|
| `apps/desktop/locales/en.meta.json` | Notes for translators, per key: where it appears, length limits, sentence fragments. |
|
|
| `apps/desktop/locales/<tag>.json` | One file per language, same keys as English. |
|
|
| `apps/desktop/locales/languages.json` | Generated list of languages with status and coverage (Settings reads it). |
|
|
| `waybetter/packages/waybetter/locales/<tag>.json` | The framework's own words (plugin UI kit buttons, shell models, the Store, the panel, the Windows installer, and the shell frame: tabs, address bar, palette, account switcher, decks, workspaces, `wb.*`), shared by every wayBetter app. |
|
|
|
|
A catalog looks like this:
|
|
|
|
```json
|
|
{
|
|
"$schema": "../../../waybetter/packages/waybetter/i18n.schema.json",
|
|
"locale": "tr",
|
|
"name": "Türkçe",
|
|
"englishName": "Turkish",
|
|
"status": "reviewed",
|
|
"reviewers": ["@yourhandle"],
|
|
"messages": {
|
|
"shell.tab.new": "Yeni sekme",
|
|
"chip.dev.errors": "{count, plural, one {# hata} other {# hata}}"
|
|
}
|
|
}
|
|
```
|
|
|
|
`status` is `machine` (drafted by a machine, nobody has checked it; Settings shows a "machine
|
|
translation" badge and a Help translate link), or `reviewed` (people went through it; list yourself
|
|
in `reviewers`). Arabic, Persian, Hebrew and Urdu get `"dir": "rtl"`. `ar-x-fm` has
|
|
`"fallback": "ar"`: it only lists the strings that differ from Arabic. `status` is the review state
|
|
of the file; whether the language is supported or community is `tier`, set in `waybetter.i18n.json`.
|
|
|
|
## How to help
|
|
|
|
1. Open your language's file, for example `apps/desktop/locales/de.json`, next to `en.json` and
|
|
`en.meta.json`.
|
|
2. Fix what reads wrong. Keep the key order. A key you delete falls back to English.
|
|
3. When you have gone through the whole file, set `"status": "reviewed"` and add yourself to
|
|
`reviewers`.
|
|
4. Run `bun run i18n:check` (or `node scripts/i18n-check.mjs de` for one language; it is the
|
|
framework's `waybetter i18n check`, which also checks the framework's catalog). It must print
|
|
`0 error(s)`. Warnings are for review, not failures.
|
|
5. See it: `bun run --cwd apps/desktop dev`, then open `http://127.0.0.1:1420/?panel&lang=de` (the
|
|
panel) or `http://127.0.0.1:1420/?shell&tabs=5&lang=de` (the window chrome). In the app, Settings → Language switches
|
|
everything at once, without a restart.
|
|
6. Send a pull request (or an issue with the changed file) to the repository.
|
|
|
|
## Message syntax
|
|
|
|
Messages are text with a small part of ICU MessageFormat:
|
|
|
|
- `{name}`: a value. Keep every placeholder English has, spelled the same; you may move it.
|
|
- `{count, plural, one {# draft} other {# drafts}}`: plural forms. `#` is the number. Use the forms
|
|
your language has (CLDR categories: `zero`, `one`, `two`, `few`, `many`, `other`); `other` is
|
|
required. A form you leave out shows `other`, which the check reports as a warning. Languages
|
|
with a single form (Japanese, Chinese, Korean, Vietnamese, Thai, Malay, Indonesian, Yoruba) write
|
|
only `other`. `=0 {…}` and `=1 {…}` match exact numbers.
|
|
- An argument English only uses to choose a word ("Download {count, plural, one {it} other
|
|
{them}}") may be dropped when your language does not change there. A placeholder that puts text
|
|
or a number on screen (`{name}`, or a plural with `#`) must stay.
|
|
- `{kind, select, post {…} reply {…} other {…}}`: keep every option English has.
|
|
- `{n, number}`, `{n, number, percent}`, `{d, date, medium}`, `{d, time, short}`: formatted for the
|
|
language automatically.
|
|
- Apostrophes are plain text: `don't`, `l'app`, `developer's`. Only an apostrophe right before
|
|
`{`, `}`, `#` or another apostrophe quotes: `'{'` is a literal brace, `''` a literal apostrophe.
|
|
|
|
The check refuses markup (`<b>`, entities), control and bidi-override characters, unknown keys and
|
|
broken syntax. Catalogs are data: values are always inserted as text, never as HTML.
|
|
|
|
Some English sentences are split around a link or a button. `en.meta.json` says so, and when its
|
|
entry has `"allowEmpty": true` you may leave that piece empty if your word order puts the words
|
|
elsewhere.
|
|
|
|
## Native pieces
|
|
|
|
`native.*` keys reach places outside the web views:
|
|
|
|
- `native.notify.*`, `native.dialog.*`: notifications and file dialog titles. The shell hands these
|
|
strings to native code when the language changes (`i18n_set`), and they are kept for the next
|
|
start.
|
|
- `native.app.name`, `native.app.comment`: the app name and description in macOS
|
|
(`InfoPlist.strings`, `CFBundleLocalizations`) and the Linux `.desktop` file.
|
|
- The Windows installer (NSIS) is the framework's: its words are `wb.installer.*` in the framework
|
|
catalogs. Tauri ships installer text for some languages; the others are generated under
|
|
`waybetter/packages/waybetter/windows/languages/` (`node
|
|
waybetter/packages/waybetter/scripts/installer-languages.mjs --write`).
|
|
|
|
The macOS and Linux files are generated. After changing a catalog, run `node scripts/i18n-check.mjs --write`; the
|
|
plain check (part of `bun run test`) fails while they are stale.
|
|
|
|
## The website
|
|
|
|
waybetter.fyi is translated the same way, with its own catalogs. It is published in English (at the
|
|
root) and in 16 more languages under `/<tag>/`: `ja`, `es`, `ar`, `id`, `de`, `pt`, `tr`, `fr`, `th`,
|
|
`ko`, `nl`, `vi`, `zh-Hant`, `it`, `hi`, `ru` (the list is `LANGUAGES` in `apps/web/site.config.ts`).
|
|
|
|
| File | What |
|
|
| --- | --- |
|
|
| `apps/web/locales/en.json` | The English source. The pages' own text (`site.home.*`, `site.x.*`, `site.store.*`, `site.submit.*`, `site.themes.*`, `site.plugins.*`, `site.notFound.*`, `site.common.*`) is generated from the HTML; what the build and the scripts say (`site.meta.*` titles and descriptions, `site.gen.*` Store pages, `site.js.*`, `site.lang.*`) is written by hand. |
|
|
| `apps/web/locales/en.meta.json` | Notes per key: which page, which element, what each placeholder stands for, length limits. |
|
|
| `apps/web/locales/<tag>.json` | One file per language, same keys, same format and `status` as the app's catalogs. |
|
|
| `apps/web/locales/languages.json` | Generated list with coverage. |
|
|
|
|
What is different from the app:
|
|
|
|
- **Placeholders for markup.** A sentence with a link or emphasis in it is one message:
|
|
`{a1}…{a1End}` is a link, `{em1}…{em1End}` and `{b1}…{b1End}` emphasis, `{span1}…{span1End}` a
|
|
styled part; `{code1}`, `{kbd1}`, `{br1}` stand for a whole piece of code, a key or a line break.
|
|
Keep each one, in pairs; you may move them to where your sentence needs them. `{version}`,
|
|
`{host}`, `{themes}`, `{plugins}`, `{items}`, `{date}`, `{apps}` are values the build fills in (the
|
|
minimum app version is `MIN_APP_VERSION` in `apps/web/site.config.ts`; never write the number).
|
|
- **Not translated:** wayBetter, wayBetter for X, wayBetter Store, X, plugin and theme names,
|
|
commands (`npm create waybetter`), keyboard keys, file names and URLs. A Store item's own name,
|
|
description and tags stay in English on every page and are marked `lang="en"`.
|
|
- **Same words as the app.** Tab, pane, theme, plugin, Store, deck, draft, install, Settings,
|
|
Appearance and the names of fixes and tweaks are the words of `apps/desktop/locales/<tag>.json`
|
|
and `waybetter/packages/waybetter/locales/<tag>.json`.
|
|
- **Length.** Page titles stay within 60 characters and descriptions within 50 to 160; the check
|
|
reports a message that is over its limit.
|
|
|
|
To fix a translation: edit `apps/web/locales/<tag>.json`, run `bun run i18n:site` (it must print
|
|
`0 error(s)`; `node scripts/i18n-site.mjs de` checks one language), then see it with
|
|
`bun run --cwd apps/web dev` at `http://localhost:5173/de/` (every language page is served in
|
|
development too). `bun test` in `apps/web` builds the site and checks that no page mixes languages,
|
|
that every page links only to pages of its own language and that the language menu, `hreflang` and
|
|
the sitemap agree.
|
|
|
|
For developers: a page's text is marked in its HTML with `data-i18n="key"` (and
|
|
`data-i18n-<attribute>="key"` for `aria-label`, `alt`, `title`, `placeholder`); `translate="no"`
|
|
keeps a part as it is. After changing a page, `node scripts/i18n-site.mjs --write` regenerates the
|
|
pages' part of `en.json`, the notes and `languages.json`. Text the build or a script produces is
|
|
`t('site.gen.…')` / `t('site.js.…')` (`apps/web/i18n.ts`, `apps/web/src/i18n.ts`). CSS uses logical
|
|
properties, so Arabic is laid out right to left without its own stylesheet.
|
|
|
|
## For developers
|
|
|
|
- Shell, panel, popover: `const t = useT(); t('section.key', { name })` (`src/i18n/index.ts`). Keys
|
|
are typed from `en.json`. Dates, numbers, relative times: `formatDate`, `formatNumber`,
|
|
`relativeTime`.
|
|
- X runtime (`src/inject`): `t('x.…')` from `src/inject/i18n.ts`. Only `x.*`, `kit.*` and `plugin.*`
|
|
reach X tabs.
|
|
- Code shared by both (`src/core`): `coreText('x.…')`.
|
|
- Native: `desktop_i18n::tr(&app, "native.key", "English fallback")` / `trf(…, &[("name", value)])`.
|
|
The fallback must equal `en.json`.
|
|
- Add the English string to `en.json` (and a note to `en.meta.json` when the context is not
|
|
obvious), then run `bun run i18n:extract` to find lookups whose key is missing. Other languages
|
|
fall back to English until translated; drafting them is fine, marked `machine`.
|
|
- Plugins: `api.i18n.register({ en: {...}, tr: {...} })` and `api.i18n.t('key', params)`, plus
|
|
`locale`, `dir`, `onChange`, `number`, `date`, `relative`, `list` (see [PLUGINS.md](PLUGINS.md)
|
|
and `waybetter/packages/runtime/src/pluginI18n.ts`). Bundled plugins keep their English inline
|
|
between `// i18n:begin` and `// i18n:end` and are translated in the app catalogs as
|
|
`plugin.<file name>.<key>`; the check keeps the two in sync.
|
|
- Right-to-left: write CSS with logical properties (`margin-inline-start`, `padding-inline`,
|
|
`inset-inline-end`, `text-align: start`). A physical `left`/`right` margin, padding, border or
|
|
text-align needs a `/* rtl-ok: why */` comment; a test enforces it. The window frame (titlebar and
|
|
tab strips) stays left to right by design, like the OS window buttons.
|
|
- Language choice: Settings → Language offers Follow X (X's `<html lang>`, reported by X tabs),
|
|
Follow system (the OS languages, read natively) or one language. The first start follows X, then
|
|
the system, then English.
|