Skip to content
Donate

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.

' Simple translation
translate(translationKeys.ButtonPlay)
' With placeholders
translate(translationKeys.ErrorTypeNotYetSupported, [itemType])
' Plurals (zero/one/many)
translatePlural(translationKeys.LabelEpisodeCount, count, [stri(count).trim()])
  1. Add the key to locale/custom/en_US.json in alphabetical order:
"MyNewKey": "My new string with {0} placeholder"
  1. The BSC compiler plugin auto-generates translationKeys.MyNewKey at build time. No manual step needed — just rebuild and the constant is available with IDE autocomplete.

  2. Use it in code:

translate(translationKeys.MyNewKey, ["value"])
  1. Run npm run lint:translations to verify the key exists and is wired up correctly.

Keys use PascalCase with a category prefix:

PrefixUsageExample
ButtonButton labelsButtonPlay, ButtonCancel
LabelUI labels and headingsLabelSearch, LabelCinemaMode
MessageLonger descriptive textMessageRememberMe
ErrorError messagesErrorFailedToUpdateFavorite
SettingSetting titles/descriptionsSettingDescDisplayLanguage
DayDay namesDayMonday

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.

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])

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.

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 files follow standard conventions for Weblate compatibility:

TypeConventionExample
Base languagesLowercasefr.json, de.json
Regional variantsUnderscore + uppercase regionfr_CA.json, de_DE.json
Chinese (script codes)Underscore + script codezh_Hans.json, zh_Hant.json, zh_Hant_HK.json
Numeric regionsUnderscore + numberes_419.json

translate() uses a 3-level fallback:

  1. Active locale (e.g. fr_CA.json)
  2. English fallback (en_US.json)
  3. 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: zhzh_Hantzh_Hant_HK.

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-CNzh_Hans, pt-BRpt_BR).

Language changes take effect after leaving Settings (the reloadHome mechanism rebuilds the UI).

Translation integrity is enforced by a single script (scripts/update-translations.cjs) that runs as part of npm run lint:

CommandWhat it checks
lint:translationsen_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

The JellyRock bot (jellyrock-bot.yml) runs on every push to main:

  1. Removes orphan keys from en_US.json
  2. Sorts en_US.json keys alphabetically
  3. Syncs new locale files into languages.json so they appear in the language picker
  4. Pushes en_US.json and languages.json to the weblate branch 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.