Translations
JellyRock uses a custom JSON based translation system that replaces Roku’s built-in tr() / XML locale mechanism. Translation data is stored as flat JSON files in locale/custom/, loaded into m.global as roAssociativeArrays for O(1) key lookups.
Quick Reference
Section titled “Quick Reference”' Simple translationtranslate(translationKeys.ButtonPlay)
' With placeholderstranslate(translationKeys.ErrorTypeNotYetSupported, [itemType])
' Plurals (zero/one/many)translatePlural(translationKeys.LabelEpisodeCount, count, [stri(count).trim()])Adding a New Translated String
Section titled “Adding a New Translated String”- Add the key to
locale/custom/en_US.jsonin alphabetical order:
"MyNewKey": "My new string with {0} placeholder"-
The
BSCcompiler plugin auto-generatestranslationKeys.MyNewKeyat build time. No manual step needed — just rebuild and the constant is available with IDE autocomplete. -
Use it in code:
translate(translationKeys.MyNewKey, ["value"])- Run
npm run lint:translationsto verify the key exists and is wired up correctly.
Key Naming Conventions
Section titled “Key Naming Conventions”Keys use PascalCase with a category prefix:
| Prefix | Usage | Example |
|---|---|---|
Button | Button labels | ButtonPlay, ButtonCancel |
Label | UI labels and headings | LabelSearch, LabelCinemaMode |
Message | Longer descriptive text | MessageRememberMe |
Error | Error messages | ErrorFailedToUpdateFavorite |
Setting | Setting titles/descriptions | SettingDescDisplayLanguage |
Day | Day names | DayMonday |
Plurals
Section titled “Plurals”Plural strings use a zero/one/many suffix convention. Define three keys in en_US.json:
"LabelEpisodeCountZero": "No episodes","LabelEpisodeCountOne": "1 Episode","LabelEpisodeCountMany": "{0} Episodes"Call with the base key (without suffix):
translatePlural(translationKeys.LabelEpisodeCount, item.childCount, [stri(item.childCount).trim()])translatePlural() selects Zero, One, or Many based on the count, then delegates to translate() for the actual lookup and placeholder substitution. The BSC plugin auto-generates the base key constant (LabelEpisodeCount) from the suffix variants.
Placeholders
Section titled “Placeholders”Use indexed {0}, {1}, etc. in translation values. Pass replacements as a string array:
' en_US.json: "ErrorTypeNotYetSupported": "This type is not yet supported: {0}."translate(translationKeys.ErrorTypeNotYetSupported, [selectedItemType])Component Imports
Section titled “Component Imports”source/ files auto-scope together, so translate() and translationKeys are available everywhere in source/ without imports.
Component files (components/) need explicit imports:
import "pkg:/source/utils/translate.bs"import "pkg:/source/translationKeys.bs"translationKeys.bs is a virtual file generated by the BSC plugin — it doesn’t exist on disk but is resolved by the compiler.
Settings Integration
Section titled “Settings Integration”Every entry in settings/settings.json has titleKey and descriptionKey fields that reference en_US.json keys. The settings UI calls translate(item.titleKey) to render translated titles and descriptions.
When adding a new setting, include both fields:
{ "title": "Cinema Mode", "description": "Play custom intros before the main feature.", "titleKey": "LabelCinemaMode", "descriptionKey": "MessageBringTheTheaterExperienceStraightTo", "settingName": "playbackCinemaMode", "type": "bool", "default": "false"}The title and description fields are kept as the English source of truth alongside the keys. See the Adding User Settings Guide for the full process.
Locale Filename Convention
Section titled “Locale Filename Convention”Locale files follow standard conventions for Weblate compatibility:
| Type | Convention | Example |
|---|---|---|
| Base languages | Lowercase | fr.json, de.json |
| Regional variants | Underscore + uppercase region | fr_CA.json, de_DE.json |
| Chinese (script codes) | Underscore + script code | zh_Hans.json, zh_Hant.json, zh_Hant_HK.json |
| Numeric regions | Underscore + number | es_419.json |
Fallback Behavior
Section titled “Fallback Behavior”translate() uses a 3-level fallback:
- Active locale (e.g.
fr_CA.json) - English fallback (
en_US.json) - The key itself (makes missing translations visible during development)
Regional locales (e.g. fr_CA) layer over their base language (fr), so users get the maximum number of translated strings. Chinese locales use 3-layer loading: zh → zh_Hant → zh_Hant_HK.
Locale Resolution
Section titled “Locale Resolution”The locale is resolved at two points in the app life cycle:
Pre-login: Roku device locale → en_US
Post-login: User setting → Jellyfin server language → Roku device locale → en_US
Server language codes are normalized via normalizeLocaleCode() to match our filename convention (e.g. zh-CN → zh_Hans, pt-BR → pt_BR).
Language changes take effect after leaving Settings (the reloadHome mechanism rebuilds the UI).
CI Validation
Section titled “CI Validation”Translation integrity is enforced by a single script (scripts/update-translations.cjs) that runs as part of npm run lint:
| Command | What it checks |
|---|---|
lint:translations | en_US.json sort order and orphans; all code translate() / translationKeys.* references exist; no hardcoded string literals; locale JSON validity; placeholder parity; plural completeness; coverage; languages.json alignment |
Bot Automation
Section titled “Bot Automation”The JellyRock bot (jellyrock-bot.yml) runs on every push to main:
- Removes orphan keys from
en_US.json - Sorts
en_US.jsonkeys alphabetically - Syncs new locale files into
languages.jsonso they appear in the language picker - Pushes
en_US.jsonandlanguages.jsonto theweblatebranch for community translators
Missing keys are caught at build time — the BSC plugin generates translationKeys constants from en_US.json, so referencing a key that doesn’t exist is a compile error.
Run locally with npm run update-translations.