Skip to content

Translate ReputeCast

How-to guide
For:
Developers

ReputeCast’s own interface comes in English and Vietnamese: the app, the sign-in and invitation pages, the server’s messages to members (errors with their fix, “who can help”, notifications) and the emails ReputeCast sends to members. This guide shows translators and developers how to translate ReputeCast into another language, or finish one, and how to check the result.

What a workspace sends its contacts (campaigns, transactional email, public pages and forms) is translated by the workspace itself, as language versions in the composer. See Send each reader their language.

  • A local copy of ReputeCast that runs from source (Run ReputeCast from source).
  • The glossary of your language, if one exists. The Vietnamese one, docs/i18n/glossary-vi.md, fixes the words for every screen. Translate the glossary’s terms first, then the screens.
What English source Translation
The web app web/src/**/messages.ts, web/src/**/<screen>.messages.ts messages.<code>.ts next to the source
Server messages and member emails internal/<package>/messages/en.json internal/<package>/messages/<code>.json
The words ReputeCast uses docs/i18n/glossary-<code>.md

A web source declares its messages with an ID (namespace.key):

web/src/features/campaigns/messages.ts
export const m = defineMessages('campaigns', {
newCampaign: 'New campaign',
blockingIssues: '{count, plural, one {# blocking issue} other {# blocking issues}}',
})

Its translation repeats the keys it translates:

web/src/features/campaigns/messages.vi.ts
export default translate(m, {
newCampaign: 'Chiến dịch mới',
blockingIssues: '{count} lỗi chặn gửi',
})

A source that defines several groups (mb, m, mp) gets one translate() per group, spread together: export default { ...translate(mb, { … }), ...translate(m, { … }) }.

A server catalog is a JSON object of "key": "text", or "key": { "message": "text", "note": "for translators" } when the English needs explaining. Read the notes: they say what an argument holds. A few keys are made from data, such as short.<event type> in internal/notify/messages. A new event type shows its English from the code until its key is added to en.json, so add it there when you add the type.

A key missing from a translation shows in English, so a screen is never blank.

Messages use ICU MessageFormat:

  • Arguments in braces are filled in by the app: {name}, {count}, {date}. Keep them spelled exactly as in the English and move them to where your language puts them: edited {time} by {name} becomes {name} sửa lúc {time}.
  • Plurals: {count, plural, one {# day} other {# days}}. Use the plural categories of your language (CLDR). Vietnamese has only other, so {count} ngày is enough. =1 {…} matches an exact number when the wording changes.
  • Choices: {status, select, drafts {…} sent {…} other {…}}. Keep the values on the left (drafts, sent, other) and translate the text in braces.
  • Tags mark text the app styles or links: <b>…</b>, <link>…</link>, <mono>…</mono>, <k>…</k>, <kbd>…</kbd>. Keep each tag around the words that play the same part.
  • A message is a whole sentence or label. Don’t split one, and don’t count on two messages appearing side by side in a certain order.
  • Language names ({language}, {languages}) come in the label form your language uses on its own. Vietnamese writes “tiếng” in lower case inside a sentence, and the app does that from the argument’s place, so write {language}, never tiếng {language}.
  • An apostrophe is fine as it is (isn't, l'été). To show a brace as text, quote it: '{'.

Never translate workspace content (campaign, list, segment, template, form and custom role names), people’s names and email addresses, permission keys (campaigns:send), audit action names, API error codes, provider names and codes (Amazon SES, Postal, Postmark), product names (ReputeCast, HostRepute, Gmail, Outlook), DNS records and record types (SPF, DKIM, DMARC, BIMI, CNAME), headers (List-Unsubscribe), code samples, CLI output, and the terms MCP, 2FA, TOTP, IdP, SSO, JIT, A/B, HTML, CSS, API and webhook.

Numbers, dates and times come from the reader’s region format, never from a translation: write {date}, not a date. Region formats are separate from the language, so a member can read English with Vietnamese formats.

  1. Run the check from the web folder:

    Terminal
    cd web
    pnpm run i18n:check # every missing message, by file
    pnpm run i18n:check --lang vi # one language
    pnpm run i18n:check --summary # one line per language
    pnpm run i18n:check --hardcoded # every hard-coded string, with its line

    It reads the web sources and translations and the server’s catalogs. It lists the messages each language misses, grouped by the file that defines their English, and counts the hard-coded text left in the screens (JSX text, and plain strings in attributes people read, such as title, label, placeholder and aria-label). A literal that isn’t text to translate, such as a format, goes in an expression: placeholder={'xxxx-xxxx-xxxx'}.

  2. Fix what fails. The check fails on a message that isn’t valid ICU, a translated key the English doesn’t have, and an argument or tag the English doesn’t use. A server catalog is also checked when its package loads, so a broken translation stops the server and every Go test at once.

  3. Commit web/src/i18n/status.gen.ts when it changes. The check writes it with the languages that are complete in this build, and every build runs the check first.

  4. Look at the screens. Switch your own app language in Account & security, then Language & region, and read the stress-test screens (Campaigns, Review & send and Settings, then Team) at 1440 and 390 px. Long words must wrap or shorten, never spill or get cut off; the “Text expansion” section of docs/dev/CONVENTIONS.md shows how.

Find untranslated text with the pseudo-locale

Section titled “Find untranslated text with the pseudo-locale”

The Pseudo (+40%) pseudo-locale accents every letter, makes each message about 40% longer and wraps it in ⟦ ⟧: “⟦Šçĥéðûļé šéñð ţö 31,604 ŕéçîþîéñţš ·········⟧”. Text without the brackets didn’t go through the translation system; a missing ⟧ is text that was cut off.

It is in the language picker in development. On a staging server, turn it on with i18n.pseudo_locale (REPUTECAST_I18N__PSEUDO_LOCALE=true; see the configuration reference).

  1. Write the glossary first, docs/i18n/glossary-<code>.md, following the sections of the Vietnamese one.

  2. Add the language to web/src/i18n/languages.ts (APP_LANGUAGES, with its name in its own language) and to internal/i18n/locale.go (appLocales), and pick the region format it uses by default (defaultRegionFor in the web app, DefaultRegion on the server). Plural rules come from CLDR on both sides.

  3. If its region format is missing, add it to internal/i18n/region.go and web/src/i18n/regions.ts, with test vectors in internal/i18n/testdata/regions.json that both sides must pass.

  4. Add messages.<code>.ts files for the web app and messages/<code>.json for the server packages, and run pnpm run i18n:check --lang <code> until it lists nothing.

  5. Check the fonts. IBM Plex covers Latin, Vietnamese, Cyrillic and Greek; another script needs a font subset in web/src/styles/fonts.css.

A native speaker reviews a language before it ships in a release. ReputeCast doesn’t support right-to-left languages yet.

  • pnpm run i18n:check --lang <code> --summary reports nothing missing and no hard-coded text.
  • With your language picked in Account & security, then Language & region, the screens you translated show no English and nothing in ⟦ ⟧ brackets.
  • A screen still shows English: its key is missing from your file (the check lists it), or the text is hard-coded in the screen (--hardcoded lists it).
  • The server won’t start after a change to a .json catalog: the catalog has invalid ICU, an unknown key or an argument the English doesn’t have. The error names the key.
  • Your language isn’t in the picker on a release build: it isn’t complete yet. Development builds show every language.