Add a new translation key to JellyRock's custom JSON i18n system. Walks locale/CLAUDE.md (pick the right key prefix, add to locale/custom/en_US.json alphabetically, handle plurals via Zero/One/Many suffixes, handle placeholders via indexed {0} {1}, add ISO-639-2 aliases for track-language keys, run npm run lint:translations + lint:language-coverage). Use when adding a new user-visible string. The BSC plugin watches en_US.json and regenerates the translationKeys constants live, so the BS-side ...
Installs into .claude/skills of the current project.
Are you the author of Translation Add?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/jellyrock-translation-add)
---
name: translation-add
description: Add a new translation key to JellyRock's custom JSON i18n system. Walks locale/CLAUDE.md (pick the right key prefix, add to locale/custom/en_US.json alphabetically, handle plurals via Zero/One/Many suffixes, handle placeholders via indexed {0} {1}, add ISO-639-2 aliases for track-language keys, run npm run lint:translations + lint:language-coverage). Use when adding a new user-visible string. The BSC plugin watches en_US.json and regenerates the translationKeys constants live, so the BS-side reference becomes available without a manual codegen step.
model: sonnet
effort: low
---
# /translation-add — guided workflow
Wraps [`locale/CLAUDE.md`](../../../locale/CLAUDE.md) and [`docs/dev/translations.md`](../../../docs/dev/translations.md). Translation data lives in `locale/custom/<locale>.json`; English (`en_US.json`) is the source of truth, the Weblate bot keeps non-English locales in sync.
## Step 1 — Pick the key prefix
Per `locale/CLAUDE.md`, keys are PascalCase with a category prefix:
| Prefix | For |
|---|---|
| `Button*` | Button labels |
| `Label*` | UI labels and headings |
| `Message*` | Longer descriptive text |
| `Error*` | Error messages |
| `Setting*` | Setting titles and descriptions |
| `Tab*` | Tab labels |
| `Header*` | Section headers |
| `Tooltip*` | Tooltip text |
| `Language*` | Track language names (audio / subtitle labels) |
If none fit cleanly, pick the closest match — don't invent a new prefix without a `decisions.md` entry. The convention is enforced by `npm run lint:translations`.
## Step 2 — Add the key to `en_US.json` (alphabetically)
Edit [`locale/custom/en_US.json`](../../../locale/custom/en_US.json). Keys must be alphabetically sorted (the lint enforces this). For a single non-plural key:
```json
"ButtonSaveChanges": "Save changes",
```
Match the surrounding entries' indentation and quote style. Don't reformat the file — `npm run lint:translations -- --fix` handles sort drift if you misplace it.
## Step 3 — Handle plurals (if applicable)
JellyRock uses a simple zero/one/many model — three keys per logical phrase:
```json
"LabelEpisodeCountZero": "no episodes",
"LabelEpisodeCountOne": "{0} episode",
"LabelEpisodeCountMany": "{0} episodes",
```
Languages with richer plural forms (Polish, Russian, Arabic) read this approximately — full Unicode CLDR plural support is a known limitation. Don't add `Few` / `Two` / etc. variants without a decisions.md entry that broadens the rule project-wide.
In BS, call `translatePlural(translationKeys.LabelEpisodeCount, count, [stri(count).trim()])`. Note: pass the BASE key (no suffix); `translatePlural` picks the right variant.
## Step 4 — Handle placeholders (if applicable)
Use indexed placeholders: `{0}`, `{1}`, `{2}`. Every locale file MUST preserve the same placeholder set as `en_US.json` — placeholder parity is enforced by `npm run lint:translations`.
```json
"MessageCouldNotReachServer": "Couldn't reach '{0}'.",
```
In BS: `translate(translationKeys.MessageCouldNotReachServer, [serverUrl])`.
## Step 5 — Track-language keys (Language* prefix only)
If your new key is in the `Language*` family (audio/subtitle track labels), you also need to add the corresponding 3-letter ISO 639-2 codes to [`source/utils/languages.bs`](../../../source/utils/languages.bs)'s alias map. Without this, ffmpeg-tagged tracks for that language fall through to the English label in every UI locale.
`npm run lint:language-coverage` enforces this — the build fails if alias coverage is missing. So skipping this step gets caught at build time, not at runtime.
## Step 6 — Use the key in BS
The BSC translation-keys plugin ([`scripts/bsc-plugins/translation-keys.cjs`](../../../scripts/bsc-plugins/translation-keys.cjs)) watches `en_US.json` and regenerates the `translationKeys` virtual file live. After your edit, the new key is available immediately — no manual codegen step.
```brightscript
m.label.text = translate(translationKeys.ButtonSaveChanges)
```
The `translate(...)` and `translatePlural(...)` helpers live in [`source/utils/translate.bs`](../../../source/utils/translate.bs).
## Step 6b — Look for an existing translation in another Jellyfin client
Another Jellyfin client's community has often translated the same string already. Check before the key ships untranslated in every locale:
```bash
npm run translations:seed -- suggest --key <YourKey>
```
If a candidate means the same thing AND has the same shape (a bare label vs `Codec: %1$s`; a standalone label vs a word from inside a sentence), add a reviewed entry to [`locale/seed/keymap.yml`](../../../locale/seed/keymap.yml), run `npm run translations:seed` and read the values it would write. Release prep seeds it from then on. Full checklist: [`docs/dev/translations.md`](../../../docs/dev/translations.md#seeding-translations-from-other-jellyfin-clients). No candidate is a normal outcome — Weblate translators take it from there.
## Step 7 — Verify
```bash
npm run lint:translations # sort + parity
npm run lint:language-coverage # only relevant for Language* keys
npm run validate # confirms the BS reference resolves
```
If any fail with auto-fixable issues, run `npm run lint:translations -- --fix`. The pre-push hook also runs these so a missed run gets caught at push time.
## Step 8 — Don't touch non-English locale files
Don't manually edit `locale/custom/<other>.json`. Translators own wording in Weblate; the only other writers are `translations:seed` (Step 6b) and release prep's merge.
## When NOT to use
- The string is dev-only (debug log, error message that only an agent would see) — don't translate. Use a plain English string in the source.
- The string is a path / identifier / API field name — those are not translatable; they're protocol-level constants.
- You're translating an existing key — that's Weblate's job, not yours. Just edit `en_US.json` if the source-of-truth English needs revision.
## Sub-agent invocation
To invoke from a sub-agent: parent passes `Read .claude/skills/translation-add/SKILL.md and follow the steps for $ARGUMENTS=<key-name-or-string>; surface the diff against locale/custom/en_US.json but do NOT commit` in the Task prompt.