Files
MyIP/TRANSLATING.md
T
2026-10-22 11:06:30 +08:00

12 KiB

Translating MyIP

MyIP (demo website: IPCheck.ing) ships its UI in English, 简体中文, 繁體中文, Français, Русский and Português (Brasil). Adding a language is a two-file change, and a partial translation is a welcome first PR. You do not need to translate everything, and you do not need to know Vue.

Anything you leave untranslated falls back to English, so a pack with a hundred strings in it is genuinely useful on day one and can grow over several PRs. Untranslated strings stay in your pack as "", which is what keeps a translation PR readable: every diff line is a "" becoming a sentence.

General contribution rules (fork, branch from dev, pnpm check before you open the PR) live in CONTRIBUTING.md.

Which languages we take

New languages are added by visitor demand, not on request. Every pack has to follow every UI change — the English pack grew by roughly 45% over two months in 2026, and each shipped language followed in the same commits — and each language is one more variant of the docs site to look after. So a language opens only once it has a meaningful share of IPCheck.ing's visitors. The open Translations wanted issue lists the languages currently up for claiming; a pack for a language that isn't on it won't be merged, however good it is. Right-to-left languages also need layout work the app doesn't have yet.

If your language isn't on the list, a README translation is welcome in any language — that is a single file and costs nothing to keep (see CONTRIBUTING.md).


The smallest useful PR

pnpm i18n-new es-MX      # your language's code — see "Naming the code" below

That writes the two files a language needs and tells you what to do next:

  1. frontend/locales/<code>.json — every key en.json has, every value "". Fill in what you like and leave the rest empty; starting with just the page and nav sections is fine.
  2. One line in common/locale-registry.js with status: 'beta'. Read it over — the script guesses nativeName, flag and apiTag and can be wrong.

That's it. No backend change, no build config, no component edits. Everything else — the language picker, <html lang>, the fallback chain, lazy-loading your pack — is derived from that registry line.

Leave the keys you skip in place with their "". Deleting them fails the gate, and keeping them is the whole point: pnpm i18n-status can count what's left, and a reviewer can see at a glance which strings you actually wrote.

Where the copy lives

File What it is Required?
frontend/locales/<code>.json The main pack — every string in the app UI (~1,650 keys) Yes — all keys, values may be ""
frontend/locales/privacy/<code>.json Privacy policy copy (~50 keys) No — but whole or not at all
frontend/locales/security-checklist/<code>.json The Cybersecurity Checklist dataset (~1,080 keys, 258 items) No — but whole or not at all
frontend/data/changelog.json Release history, one string per language per entry No — beta languages are exempt

en.json is the reference for all of them: a translation may lag behind English, never contradict it.

The two optional files ship whole or not at all. Unlike the main pack, they load as one file each with no per-key fallback inside, so a half-finished one renders a half-English page. The gate rejects a "" in either.

  • Privacy copy is short — translate it in one sitting, or leave it out. Partial privacy files don't ship.
  • The checklist is long (258 items). Same rule: showing the full English checklist beats showing gaps, so leave the file out until it's done.

When you're ready to do one, scaffold it the same way you scaffolded the main pack:

pnpm i18n-new es-MX --privacy --checklist    # either flag, or both

Both need the language registered already (the plain pnpm i18n-new <code> run). The checklist skeleton keeps slug and priority filled in — those are data, not copy. Until every other value is filled in, pnpm test stays red: finish the file before you commit it, or delete it again.

The registry line

pnpm i18n-new writes this for you; this is what to check it against.

common/locale-registry.js:

{ code: 'es-MX', nativeName: 'Español (México)', flag: 'mx', apiTag: 'es', htmlLang: 'es-MX', status: 'beta' },
Column What to put in it
code The UI code and your JSON file name. See the naming rule below.
nativeName The language's name as its own speakers write it (Français, not French).
flag A lowercase two-letter circle-flags code for the picker icon.
apiTag The tag sent to upstream data sources. See below.
htmlLang The BCP-47 tag written to <html lang>. Be precise where script matters: zh declares zh-CN so Han glyphs stay Simplified on Japanese systems.
status 'beta' for a new language. Maintainers flip it to 'full' — see From beta to full.

Registry order is also the order the language picker renders, so add your entry where it reads best rather than always at the end.

Naming the code

  • The default variant of a language uses the bare code: zh is Simplified Chinese, pt would be European Portuguese.
  • Variants that arrive later use the full region code: zh-TW, pt-BR, es-MX.
  • If you are translating a variant whose base language is already registered, use the region code — your pack then inherits the base as a fallback (see below).

Picking apiTag

Geographic names (city, region names) come from upstream IP databases, which localize into a fixed set of tags: en, de, es, fr, ja, pt-BR, ru, zh-CN. Put the closest one of those in apiTag. If none is close, just repeat your own code — place names will render in English, which is expected and not a bug.

Country names are unaffected: they come from the browser's own Intl data and localize automatically for any language. Dates and times do too.

What users see for keys you haven't translated

An untranslated string — "" or a key that isn't there — walks a chain: your locale → its base language (if registered) → English.

  • fr → en
  • zh-TW → zh → en

It applies per key in the main pack and per entry in the changelog; the privacy copy and the checklist walk the same chain, but a whole file at a time. The app never renders a blank or a raw key path, and it logs no warnings — an incomplete beta pack is a supported state, not an error.

In the language picker, a beta language carries a small Beta badge (deliberately the English word, in every language).

Checking your work locally

pnpm install
pnpm test          # the hard gate — must be green
pnpm i18n-status   # progress report — never fails, just tells you where you are
pnpm dev           # then pick your language in Preferences

?hl=<code> also selects a language, but only before you've saved one in Preferences — a stored preference wins over the query param.

What pnpm test enforces

tests/locale-packs.test.js is the gate. Against English, your pack must:

  • Have exactly English's keys — no more (a typo'd path is the usual cause), no fewer (pnpm i18n-sync re-adds any you dropped). Untranslated ones stay as "".
  • Use no placeholder English doesn't provide. {count}, {name} and friends must be a subset of what the English string uses; keep them spelled exactly as-is, and move them around the sentence as your grammar needs. Empty values are skipped, naturally.
  • Keep slug and priority untranslated in the security checklist — those are data (URLs and badge colors), not copy.
  • Contain no "" in the two optional files. The privacy copy and the checklist ship finished or not at all; only the main pack may carry untranslated values.
  • Translate everything, if the language is full. A "" left in a full locale's main pack fails the build, and all three files have to be there. beta locales are free to leave as many "" as they like — in the main pack.

tests/locale-registry.test.js checks your registry line's shape, and tests/changelog.test.js only requires changelog history for full languages.

What pnpm i18n-status shows

Per language and per dataset: percentage translated, and the next keys to do. It also counts values that are byte-identical to English — sometimes right (MTR, product names), sometimes a copy-paste that never got translated.

pnpm i18n-status --locale pt-BR --limit 30

When English changes under you

New keys appear in en.json all the time. Realign every pack with one command:

pnpm i18n-sync

It adds English's new keys to your main pack as "", drops the ones English no longer has, and puts everything back in English's order. Run it whenever pnpm test says keys are missing, and after a rebase. It never overwrites a translation.

For the privacy copy and the checklist it only reports what moved — writing "" into them would fail the gate on your behalf. Translate the new keys, or drop the file.

Improving an existing language

Fixes and better phrasings for en / zh / zh-TW / fr / ru / pt-BR are just as welcome as new languages — edit the JSON and open the PR. Two things to keep in mind:

  • Prefer natural phrasing over literal translation; this is a networking tool, and the jargon most users know is often the English term.
  • Free-form input fields are autocomplete-hardened, and their placeholder copy avoids the words "address / 地址 / adresse / adresi" on purpose — iOS QuickType keys on them and offers to autofill a postal address into an IP field. When translating a placeholder, pick a wording that avoids your language's equivalent word too.

From beta to full

A full language is one every future copy change is required to land in, so promotion is a maintainer decision, made when the language is actually complete:

  • Main pack, privacy copy and security checklist all at 100% against en — not a single "" left (pnpm i18n-status shows this).
  • Changelog history back-filled — every entry in frontend/data/changelog.json.
  • Enough of a track record that copy changes will keep landing in it.

Once status flips to 'full', tests/locale-packs.test.js and tests/changelog.test.js start failing on any gap, which is exactly the point. Until then, no pressure: a beta language never breaks a build.

Known limits

Things that will look "untranslated" no matter how complete your pack is. None of them are bugs, and none of them block a PR:

  • Place names (city, region) come from upstream IP databases and only exist in de, en, es, fr, ja, pt-BR, ru, zh-CN. Other languages get English place names next to a fully translated UI.
  • The pre-Vue loading screen in index.html carries its own tiny set of strings — it runs before the app and its bundle exist. Beta languages fall back to English there; maintainers handle it at promotion time.
  • Some tool output is upstream data, not UI copy: Whois records, DNS answers, ASN organization names, service-status incident text. Those arrive in whatever language the source publishes.
  • The backend needs no change for a new language — it resolves whatever tag the UI sends onto the closest one its data sources actually have, so a translation PR never touches back-end code.
  • Documentation at docs.ipcheck.ing and the README translations are separate efforts — see CONTRIBUTING.md for README translations.

Questions

Open an issue. If you're planning a large translation, saying so up front is useful — it avoids two people translating the same 1,000 keys in parallel.