feat(i18n): migrate Electron translations to i18next with symbolic keys#134
feat(i18n): migrate Electron translations to i18next with symbolic keys#134
Conversation
Replace @transifex/native with i18next using a custom ElectronStoreBackend that fetches translations from jsDelivr CDN and caches them in electron-store. Remove _tags from all t() calls since namespace is now set via defaultNS.
The ElectronStoreBackend.read() was calling i18next.addResourceBundle() on the global singleton, but the app creates a separate instance via createInstance(). Background CDN fetches were updating the wrong store.
The translations repo now nests JS files under a monorepo/ subdirectory. Also bump TRANSLATION_VERSION from 1.7.8 to 1.8.7.
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughReplaces Transifex Native with i18next and a new ElectronStore-backed backend, removes CustomCache, updates package.json dependency, replaces many translation keys (removing Changes
Sequence Diagram(s)sequenceDiagram
participant App as Application
participant I18n as i18nInstance
participant Backend as ElectronStoreBackend
participant Store as Electron Store
participant CDN as jsDelivr CDN
App->>I18n: loadTranslations(lang)
I18n->>Backend: read(language, namespace, callback)
Backend->>Store: check cache for language
alt Cache hit
Store-->>Backend: cached translations
Backend-->>I18n: callback(cached data)
I18n-->>App: translations available
Note over Backend,CDN: optionally refresh in background
Backend->>CDN: fetch fresh translations
CDN-->>Backend: fresh data
Backend->>Store: update cache
Backend->>I18n: addResourceBundle(fresh data)
else Cache miss
Backend->>CDN: fetch translations
CDN-->>Backend: fetched data
Backend->>Store: save cache
Backend-->>I18n: callback(fetched data)
I18n-->>App: translations available
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/main/config.ts`:
- Around line 53-60: The Prettier failure is caused by the multiline ternary
inside the translation call for message (the t('Hardware acceleration ...', {
enabled: enabled ? t('enabled') : t('disabled') }) expression). Fix it by
formatting the ternary as a single line so Prettier accepts it: locate the
translation call that sets message (the t(..., { enabled: ... }) block) and
collapse the enabled ternary into one line (enabled: enabled ? t('enabled') :
t('disabled')) while keeping surrounding indentation and commas intact.
In `@src/main/translations/index.ts`:
- Around line 36-64: The read method currently calls callback(null) immediately
on a cache miss causing first-run locales to render fallbacks; change
read(language, namespace, callback) so that when this.store.get(language) is
undefined you perform the fetch and only invoke callback(null, data) after the
fetch and JSON parse succeed (or callback(err) on fatal fetch/parsing error),
while preserving the existing behavior for cached entries (keep returning cached
via callback(null, cached) and still do the background fetch/refresh logic);
update references to this.store, the fetch URL using TRANSLATION_VERSION, and
this.services.resourceStore.addResourceBundle to be used inside the fetch
resolution path for the cache-miss case.
🧹 Nitpick comments (2)
src/main/update.ts (1)
120-122: Remove the empty options object in the update-installed message.The second argument is now empty after tag removal; dropping it keeps the callsite cleaner and consistent with other
t()calls in the codebase.♻️ Suggested cleanup
- message: t('Updates downloaded, application will restart for update to take effect.', { - }), + message: t('Updates downloaded, application will restart for update to take effect.'),src/main/translations/index.ts (1)
87-113: Nice: instance-scopedtkeeps lookups tied to the configured backend.Consider using
getFixedT(null, 'electron')as an alternative to binding if you want to add namespace scoping or namespace-specifictfunctions.
- Collapse ternary to single line in config.ts (Prettier fix) - Defer callback on cache miss until CDN fetch resolves so first-run non-English locales get translations immediately - Update TRANSLATION_VERSION to 2026.2.0 (CalVer) - Remove v prefix from CDN URL to match new release tagging
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/main/translations/index.ts`:
- Around line 64-67: The catch block currently calls callback(err, false) which
uses a non-standard i18next backend read signature; update the error path in the
promise rejection to invoke the read callback as callback(err) (or callback(err,
undefined/null)) instead of passing false so i18next treats it as an error with
no resource data — modify the .catch handler where log.error is called and the
callback is invoked (referencing log.error and the callback used in this module)
to pass only the error.
🧹 Nitpick comments (2)
src/main/translations/index.ts (2)
46-48: Silent failure path when response is not OK leaves fetch chain in ambiguous state.When
response.okis false,return;passesundefinedto the next.then(), which then falls through to theelse if (!cached)branch callingcallback(null, {}). This silently treats HTTP errors (404, 500, etc.) as "no translations available" rather than actual errors. Consider explicitly handling non-OK responses.♻️ Proposed fix to handle HTTP errors explicitly
fetch(url) .then((response) => { - if (!response.ok) return; + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } return response.json(); })
71-73: Stale comment references removed Transifex system.The comment still mentions "Transifex-compatible locale codes" but this PR migrates away from Transifex to i18next.
📝 Proposed fix
/** - * Map system locale codes to Transifex-compatible locale codes. + * Map system locale codes to supported translation locale codes. */
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/main/translations/index.ts`:
- Around line 110-114: Guard against an empty preferred-locale list by checking
the array returned from app.getPreferredSystemLanguages(); if systemLocales[0]
is undefined, fall back to 'en' before passing into getLocaleFromCode. Update
the code that assigns systemLocales and systemLocale (references:
app.getPreferredSystemLanguages(), getLocaleFromCode, variable systemLocale) so
you compute const systemLocale = getLocaleFromCode(systemLocales[0] ?? 'en') (or
equivalent null/empty check) and then call
i18nInstance.changeLanguage(systemLocale).
- Line 18: Update the TRANSLATION_VERSION constant (symbol: TRANSLATION_VERSION
in src/main/translations/index.ts) from '2026.2.0' to the correct release tag
'v1.8.7'; after changing the value, verify the CDN (jsDelivr) serves the file
and if it still returns 404 coordinate a redeploy of the wcpos/translations
release or request a jsDelivr cache refresh to ensure translations load
correctly.
🧹 Nitpick comments (1)
src/main/translations/index.ts (1)
36-56: Cache key should include namespace to avoid collisions.
readreceivesnamespacebut the store key uses onlylanguage, which will clash if you add more namespaces later. Consider keying by both values.♻️ Suggested change
read(language: string, namespace: string, callback: (err: any, data?: any) => void) { - const cached = this.store.get(language) as TranslationRecord | undefined; + const cacheKey = `${language}:${namespace}`; + const cached = this.store.get(cacheKey) as TranslationRecord | undefined; if (cached) { log.debug(`Loading ${language} translations from cache`); callback(null, cached); } // Fetch fresh translations from jsDelivr in the background const url = `https://cdn.jsdelivr.net/gh/wcpos/translations@${TRANSLATION_VERSION}/translations/js/${language}/monorepo/${namespace}.json`; fetch(url) .then((response) => { if (!response.ok) return; return response.json(); }) .then((data) => { if (data && Object.keys(data).length > 0) { - const current = this.store.get(language) as TranslationRecord | undefined; + const current = this.store.get(cacheKey) as TranslationRecord | undefined; if (JSON.stringify(current) !== JSON.stringify(data)) { - this.store.set(language, data); + this.store.set(cacheKey, data); } this.services.resourceStore.addResourceBundle(language, namespace, data, true, true); if (!cached) { callback(null, data); }
|
|
||
| const store = new Store<Record<string, TranslationRecord>>(); | ||
| const cache = new CustomCache(store); | ||
| const TRANSLATION_VERSION = '2026.2.0'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify CDN availability for candidate translation tags
for v in 2026.2.0 1.8.7 v1.8.7; do
url="https://cdn.jsdelivr.net/gh/wcpos/translations@${v}/translations/js/en/monorepo/electron.json"
printf "%s -> " "$v"
curl -s -o /dev/null -w "%{http_code}\n" "$url"
doneRepository: wcpos/electron
Length of output: 100
🏁 Script executed:
# Check for PR context, version references, or objectives in the repository
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "*.json" \) -not -path "./node_modules/*" -not -path "./.git/*" | head -20 | xargs grep -l "1.8.7\|2026.2.0\|TRANSLATION_VERSION" 2>/dev/null | head -10Repository: wcpos/electron
Length of output: 40
🏁 Script executed:
# Also check the index.ts file directly for context
cat -n src/main/translations/index.ts | head -30Repository: wcpos/electron
Length of output: 996
🏁 Script executed:
# Check package.json and lock files for translation version references
find . -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" | xargs grep -i "translat" 2>/dev/null | head -20Repository: wcpos/electron
Length of output: 40
🏁 Script executed:
# Check git tags or recent commits that might reference translations version
git log --oneline --all -20 | grep -i "translat\|version"Repository: wcpos/electron
Length of output: 124
🏁 Script executed:
# Check if there's any documentation or comments in the codebase about which version to use
rg "translations.*@" --type ts --type js -B 2 -A 2 2>/dev/null | head -40Repository: wcpos/electron
Length of output: 455
🏁 Script executed:
# Check what other versions might be available by looking at jsDelivr structure
# Try to list available tags from wcpos/translations repository via GitHub API
curl -s "https://api.github.com/repos/wcpos/translations/releases?per_page=10" 2>/dev/null | jq -r '.[].tag_name' 2>/dev/null || echo "GitHub API check failed"Repository: wcpos/electron
Length of output: 64
Fix TRANSLATION_VERSION to match the correct release tag.
The constant is set to 2026.2.0, which does not exist as a release. The correct tag is v1.8.7 (confirmed in wcpos/translations releases). However, the CDN currently returns 404 for this tag, which will break translation loading. Update the constant to the correct version:
-const TRANSLATION_VERSION = '2026.2.0';
+const TRANSLATION_VERSION = 'v1.8.7';If the CDN still returns 404 after this change, the release may need to be redeployed or the jsDelivr cache needs to be refreshed.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const TRANSLATION_VERSION = '2026.2.0'; | |
| const TRANSLATION_VERSION = 'v1.8.7'; |
🤖 Prompt for AI Agents
In `@src/main/translations/index.ts` at line 18, Update the TRANSLATION_VERSION
constant (symbol: TRANSLATION_VERSION in src/main/translations/index.ts) from
'2026.2.0' to the correct release tag 'v1.8.7'; after changing the value, verify
the CDN (jsDelivr) serves the file and if it still returns 404 coordinate a
redeploy of the wcpos/translations release or request a jsDelivr cache refresh
to ensure translations load correctly.
… fallback
Replace English string keys with structured feature.descriptor keys
(e.g. t('menu.check_for_updates') instead of t('Check for Updates')).
- Rewrite all t() calls to use symbolic keys
- Bundle en/electron.json as fallback (fallbackLng: 'en')
- Add regional locale fallback in backend (fr_CA → fr)
- Graceful error handling (return empty data, let fallback handle it)
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@src/main/config.ts`:
- Line 57: The translation keys used in the config block—t('enabled') and
t('disabled')—are missing from the locale resource; update the calls to use
namespaced keys (for example t('app.enabled') and t('app.disabled')) and add
corresponding entries to the locale JSON (electron.json) or alternatively add
the plain 'enabled' and 'disabled' keys to the locale file; ensure you update
every occurrence of t('enabled')/t('disabled') so the i18n lookup (function t)
resolves correctly.
- Around line 54-59: Prettier is failing on the multiline t(...) call used to
set message in config (the t function call with the ternary using enabled).
Reformat the call so it conforms to Prettier: put the t(...) call and its object
argument on a single logical line or align properties per Prettier rules, and
simplify the ternary expression (enabled ? t('enabled') : t('disabled')) to a
single expression without extra line breaks; update the message assignment where
t(...) is used to ensure consistent spacing and commas so Prettier CI passes.
In `@src/main/translations/index.ts`:
- Around line 46-51: The fetchTranslations method's parameter list needs to be
wrapped to satisfy the formatter: split the parameters of
fetchTranslations(language: string, namespace: string) across multiple lines
(one parameter per line) and adjust the method signature accordingly in the
fetchTranslations function so formatting checks pass; keep the same parameter
names and types and ensure the body still calls this.buildUrl(language,
namespace) and returns the same Promise<TranslationRecord | null>.
- Around line 54-86: The read method returns cached translations without
considering translation version, so stale translations persist after a
TRANSLATION_VERSION bump; update read (and any caching logic around this.store)
to include the translation version in the cache key or explicitly
invalidate/refresh cached entries when TRANSLATION_VERSION changes: e.g., derive
keys from `${language}:${TRANSLATION_VERSION}` when calling this.store.get/set
or check a stored version on load and call this.store.delete/refresh before
returning cached data; update functions fetchTranslations and any logic that
sets the cache so they use the same versioned key or invalidation flow
(referencing read, fetchTranslations, getBaseLanguage, and this.store).
In `@src/main/translations/locales/en/electron.json`:
- Around line 1-28: Add the missing translation keys for the
hardware-acceleration interpolation by defining "app.enabled" and "app.disabled"
in the electron.json locale (e.g., "app.enabled": "Enabled", "app.disabled":
"Disabled"), then update the code that calls t('enabled')/t('disabled') in
config.ts to use the namespaced keys (e.g., replace t('enabled') with
t('app.enabled') and t('disabled') with t('app.disabled')) so the interpolated
{enabled} value renders the localized string.
In `@src/main/update.ts`:
- Around line 120-122: The message call is passing an empty interpolation object
to the translator; remove the unused empty object so the call becomes
t('update.updates_downloaded_application_will_restart_for') instead of
t('update.updates_downloaded_application_will_restart_for', {}), i.e. update the
object property named message (where t(...) is invoked) to pass only the
translation key.
| { | ||
| "app.are_you_sure": "Are you sure?", | ||
| "app.cleared_app_data": "Cleared app data", | ||
| "app.could_not_clear_app_data": "Could not clear app data", | ||
| "app.hardware_acceleration": "Hardware Acceleration", | ||
| "app.hardware_acceleration_application_will_restart_for": "Hardware acceleration {enabled}, application will restart for update to take effect.", | ||
| "common.no": "No", | ||
| "common.yes": "Yes", | ||
| "menu.advanced": "Advanced", | ||
| "menu.check_for_updates": "Check for Updates", | ||
| "menu.clear_app_data": "Clear App Data", | ||
| "menu.documentation": "Documentation", | ||
| "menu.enable_hardware_acceleration": "Enable Hardware Acceleration", | ||
| "menu.f_a_q": "F.A.Q.", | ||
| "menu.file": "File", | ||
| "menu.performance": "Performance", | ||
| "menu.report_an_issue": "Report an Issue", | ||
| "menu.request_a_feature": "Request a Feature", | ||
| "menu.view": "View", | ||
| "update.a_new_version_is_available_do": "A new version ({version}) is available. Do you want to update now?", | ||
| "update.current_version_is_up-to-date": "Current version is up-to-date.", | ||
| "update.downloading_update": "Downloading update...", | ||
| "update.found_updates": "Found Updates", | ||
| "update.install_updates": "Install Updates", | ||
| "update.no_updates": "No Updates", | ||
| "update.remind_me_later": "Remind me later", | ||
| "update.updates_downloaded_application_will_restart_for": "Updates downloaded, application will restart for update to take effect." | ||
| } |
There was a problem hiding this comment.
Missing translation keys for enabled and disabled.
The config.ts file (line 57) calls t('enabled') and t('disabled') for the hardware acceleration interpolation, but these keys are not defined in this locale file. This will cause the raw keys to be displayed instead of translated text.
🔧 Proposed fix to add missing keys
{
"app.are_you_sure": "Are you sure?",
"app.cleared_app_data": "Cleared app data",
"app.could_not_clear_app_data": "Could not clear app data",
+ "app.disabled": "disabled",
+ "app.enabled": "enabled",
"app.hardware_acceleration": "Hardware Acceleration",Then update config.ts to use namespaced keys:
enabled: enabled ? t('app.enabled') : t('app.disabled'),🤖 Prompt for AI Agents
In `@src/main/translations/locales/en/electron.json` around lines 1 - 28, Add the
missing translation keys for the hardware-acceleration interpolation by defining
"app.enabled" and "app.disabled" in the electron.json locale (e.g.,
"app.enabled": "Enabled", "app.disabled": "Disabled"), then update the code that
calls t('enabled')/t('disabled') in config.ts to use the namespaced keys (e.g.,
replace t('enabled') with t('app.enabled') and t('disabled') with
t('app.disabled')) so the interpolated {enabled} value renders the localized
string.
- Fix Prettier formatting in config.ts and translations/index.ts
- Add missing app.enabled/app.disabled translation keys
- Use namespaced t('app.enabled')/t('app.disabled') in config.ts
- Remove empty interpolation object in update.ts
- Version cache key to invalidate stale translations on version bump
- Guard against empty getPreferredSystemLanguages() result
- Fix stale "Transifex-compatible" comment
Listens for translation-release dispatch from wcpos/translations repo and bumps TRANSLATION_VERSION in src/main/translations/index.ts.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @.github/workflows/update-translations.yml:
- Around line 13-18: Validate and sanitize the incoming VERSION before using it
in the sed substitution: add a check in the workflow that ensures the VERSION
env (from github.event.client_payload.version) matches an allowed pattern (e.g.,
semantic version or a strict /^[0-9A-Za-z._-]+$/ regex) and fail early if it
doesn't; then either escape shell-special characters (quotes, slashes,
ampersands) in VERSION or use a safer substitution mechanism to update
TRANSLATION_VERSION in src/main/translations/index.ts instead of interpolating
raw $VERSION into the sed command.
🧹 Nitpick comments (1)
.github/workflows/update-translations.yml (1)
7-11: Consider adding explicit permissions for least-privilege security.The workflow pushes commits but lacks an explicit
permissionsblock. Declaring minimum required permissions is a GitHub Actions security best practice.🔒 Proposed fix to add explicit permissions
jobs: update: runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v4
Reject non-CalVer input to prevent sed injection or malformed substitutions from repository_dispatch payloads.
Summary
ElectronStoreBackendmenu.check_for_updatesinstead ofCheck for Updates)en/electron.jsonas fallback — users never see raw symbolic keysfr_CA→fr)2026.2.0)Key format
feature.descriptorwith flat keys (keySeparator: false):menu.*— app menu itemsupdate.*— auto-updater dialogsapp.*— app-level operations (hardware accel, clear data)common.*— shared (Yes/No)i18next config
fallbackLng: 'en'with bundledresources: { en: { electron: en } }load: 'currentOnly'— backend handles regional→base fallback internally{variable}(not{{variable}})Test plan
fr_CAfalls back tofriffr_CAnot available)🤖 Generated with Claude Code
Summary by CodeRabbit
Chores
New Features