feat(i18n): decouple translation version with CalVer constant#41
feat(i18n): decouple translation version with CalVer constant#41
Conversation
The translations repo now nests JS files under a monorepo/ subdirectory per language folder. Without this fix, translation fetches silently 404. - Extract RxDBBackend to its own file with a testable buildUrl method - Add 14 unit tests covering CDN URL construction, caching, and fetch - Add 2 E2E tests for language switching via settings
📝 WalkthroughWalkthroughAdds a standalone RxDB-backed i18next backend (TRANSLATION_VERSION + RxDBBackend), moves backend init out of TranslationProvider, adds backend tests, E2E language settings tests, a workflow to update TRANSLATION_VERSION, adjusts string-extraction and a small drawer translation change, and updates an electron submodule reference. Changes
Sequence DiagramsequenceDiagram
participant App as Application
participant i18n as i18next
participant Backend as RxDBBackend
participant Cache as translationsState (RxDB)
participant CDN as jsDelivr CDN
participant Store as resourceStore
App->>i18n: changeLanguage(lang)
i18n->>Backend: read(lang, namespace)
activate Backend
Backend->>Cache: check cached translation
alt cached
Cache-->>Backend: cachedData
Backend->>i18n: callback(null, cachedData)
else no cached
Backend->>i18n: callback(null, undefined)
end
deactivate Backend
par background fetch
Backend->>CDN: fetch buildUrl(lang, namespace)
CDN-->>Backend: JSON bundle
Backend->>Backend: compare with Cache
alt changed
Backend->>Cache: set(lang, data)
Backend->>Store: addResourceBundle(lang, namespace, data, true, true)
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/core/src/contexts/translations/index.tsx (1)
17-51:⚠️ Potential issue | 🟡 MinorConsider whether the explicit
reloadResourcescall is necessary or just defensive.When
wcposVersionchanges, theuseMemorecreatesi18nInstancewith the new version in backend options, and theuseEffectimmediately callsreloadResources. Since the new instance will fetch resources with the correct version on next use (via lazy loading), the explicit reload appears to serve as proactive loading rather than fixing a functional issue. If the goal is to ensure translations are available eagerly when the version becomes available, add a comment clarifying this intent. If eager loading isn't necessary, consider removing the effect and relying on lazy loading instead.
🤖 Fix all issues with AI agents
In `@packages/core/src/contexts/translations/rxdb-backend.ts`:
- Line 51: Guard against a possible null this.services before calling
addResourceBundle in the read() path: replace the direct call to
this.services.resourceStore.addResourceBundle(...) with a safe check (e.g. if
(!this.services || !this.services.resourceStore) { /* log or throw a descriptive
error or return early */ } else {
this.services.resourceStore.addResourceBundle(language, namespace, data, true,
true); }) or use optional chaining
this.services?.resourceStore?.addResourceBundle(...), and ensure the error/log
references translationsState and suggests calling init() if needed so the
service is initialized.
🧹 Nitpick comments (6)
packages/core/src/contexts/translations/rxdb-backend.ts (2)
9-11: Consider adding type definitions for better type safety.The use of
anyfortranslationsState,version, andservicesreduces type safety and IDE support. Consider defining interfaces for these, especially sincetranslationsStateappears to have a specific shape (object with language keys and asetmethod).🔧 Proposed type definitions
+interface TranslationsState { + [language: string]: Record<string, string> | undefined; + set?: (language: string, updater: () => Record<string, string>) => void; +} + +interface BackendServices { + resourceStore: { + addResourceBundle: (lng: string, ns: string, resources: object, deep?: boolean, overwrite?: boolean) => void; + }; +} + export class RxDBBackend { static type = 'backend' as const; type = 'backend' as const; - private translationsState: any; - private version: string; - private services: any; + private translationsState: TranslationsState | null = null; + private version: string = '0.0.0'; + private services: BackendServices | null = null;
40-54: Unhandled background fetch may silently fail without any observability.The empty
.catch(() => {})swallows all errors, making it difficult to debug translation loading issues in production. Consider adding minimal logging or error tracking.🔧 Proposed improvement with optional logging
fetch(url) .then((response) => { if (!response.ok) return; return response.json(); }) .then((data) => { if (data && Object.keys(data).length > 0) { const current = this.translationsState?.[language]; if (JSON.stringify(current) !== JSON.stringify(data)) { this.translationsState?.set(language, () => data); } this.services.resourceStore.addResourceBundle(language, namespace, data, true, true); } }) - .catch(() => {}); + .catch((error) => { + // Silently fail for background fetch - translations will use cache or fallback + if (__DEV__) { + console.warn(`[RxDBBackend] Failed to fetch translations for ${language}/${namespace}:`, error); + } + });packages/core/src/contexts/translations/rxdb-backend.test.ts (1)
126-127: Consider using a more robust approach for awaiting promise resolution.Using
setTimeout(r, 0)works but can be flaky in some environments. Consider usingjest.runAllTimers()with fake timers, or a utility likeflushPromises.🔧 Example using a flushPromises helper
// Add at top of file const flushPromises = () => new Promise(jest.requireActual('timers').setImmediate); // Then use in tests: await flushPromises();apps/main/e2e/settings.spec.ts (3)
80-81: HardcodedwaitForTimeoutcan cause flaky tests.Using fixed delays like
page.waitForTimeout(500)is fragile. Consider waiting for a specific condition instead, such as the filtered results appearing.🔧 Proposed improvement
await searchInput.fill(search); - await page.waitForTimeout(500); - await page.getByText(label).click(); + // Wait for the option to appear in the filtered list before clicking + const option = page.getByText(label); + await expect(option).toBeVisible({ timeout: 5_000 }); + await option.click();
96-100: Consider using thechangeLanguagehelper here for consistency.The test duplicates the language change logic that's already encapsulated in the
changeLanguagehelper defined above. Using the helper would improve maintainability.🔧 Proposed refactor
// The General tab is shown by default with a Language combobox. - // The demo store defaults to "English (US)". - const languageTrigger = page.getByText('English (US)').first(); - await expect(languageTrigger).toBeVisible({ timeout: 10_000 }); - await languageTrigger.click(); - - // Search for French in the combobox popover - await page.getByPlaceholder('Search Languages').fill('French'); - await page.waitForTimeout(500); - - // Select "French (Français)" from the filtered list - await page.getByText('French (Français)').click(); + await changeLanguage(page, 'French', 'French (Français)');
134-135: Replace hardcoded delay with an explicit wait condition.The 1-second delay after closing the modal is fragile. Wait for the modal to be hidden or the POS page to be fully visible instead.
🔧 Proposed fix
// Close settings modal await page.getByRole('button', { name: /close|fermer/i }).first().click(); - await page.waitForTimeout(1_000); + // Wait for modal to close by checking the POS search is visible + await expect(page.getByPlaceholder(/Search Products|Rechercher des produits/i)).toBeVisible({ timeout: 10_000 });
| if (JSON.stringify(current) !== JSON.stringify(data)) { | ||
| this.translationsState?.set(language, () => data); | ||
| } | ||
| this.services.resourceStore.addResourceBundle(language, namespace, data, true, true); |
There was a problem hiding this comment.
Potential null reference when accessing this.services.resourceStore.
this.services could be undefined if init() wasn't called before read(). The code uses optional chaining for translationsState but directly accesses this.services.resourceStore.
🛡️ Proposed defensive fix
- this.services.resourceStore.addResourceBundle(language, namespace, data, true, true);
+ this.services?.resourceStore?.addResourceBundle(language, namespace, data, true, true);🤖 Prompt for AI Agents
In `@packages/core/src/contexts/translations/rxdb-backend.ts` at line 51, Guard
against a possible null this.services before calling addResourceBundle in the
read() path: replace the direct call to
this.services.resourceStore.addResourceBundle(...) with a safe check (e.g. if
(!this.services || !this.services.resourceStore) { /* log or throw a descriptive
error or return early */ } else {
this.services.resourceStore.addResourceBundle(language, namespace, data, true,
true); }) or use optional chaining
this.services?.resourceStore?.addResourceBundle(...), and ensure the error/log
references translationsState and suggests calling init() if needed so the
service is initialized.
📊 Test Coverage Report
Coverage Legend
|
🚀 Deployment Summary
🔗 Quick Links❌ Failed Tests (4 total)settings.spec.ts
📋 Full error details (first 5)settings.spec.ts - should change language to French and load translations from CDNProject: settings.spec.ts - should persist language after closing and reopening settingsProject: settings.spec.ts - should change language to French and load translations from CDNProject: settings.spec.ts - should persist language after closing and reopening settingsProject: 📸 Failure Screenshots🔗 Debug Links
🤖 Updated by GitHub Actions |
Switch to a standalone TRANSLATION_VERSION CalVer constant (2026.2.0) instead of deriving it from the connected site's wcpos_version. This lets translations release independently via the translations repo's dispatch workflow. - Add TRANSLATION_VERSION constant to rxdb-backend.ts - Remove v prefix from CDN URL (CalVer tags don't use it) - Simplify TranslationProvider by removing wcposVersion dependency - Add receiver workflow for translation-release dispatch events - Update electron submodule with i18next migration fixes
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In @.github/workflows/update-translations.yml:
- Around line 19-27: Replace the unsafe "git add ." and inline commit message
that embeds ${{ github.event.client_payload.version }} directly; instead stage
only the specific translation file(s) (replace the git add . with git add <the
translation file(s)>) and create a sanitized commit message file (e.g., echo
"$(printf 'chore: update translation version to %s' "$VERSION")" > msg) and run
git commit -F msg (or use git commit --message "$(printf '%s' "$VERSION")" if
you prefer) so the value from github.event.client_payload.version is
escaped/sanitized before passing to git and you avoid committing unintended
files.
- Around line 14-17: The workflow currently injects
github.event.client_payload.version directly into the run block which risks
command injection; change the step to pass the payload into an environment
variable and reference that variable inside the run script instead of
interpolating into the YAML. Concretely, add an env: block with VERSION: ${{
github.event.client_payload.version }} for the step, then update the run script
to use the shell variable $VERSION (e.g. set VERSION="$VERSION" and use sed to
replace TRANSLATION_VERSION with that shell variable) so the sed invocation uses
the runtime env var rather than direct YAML expansion of
github.event.client_payload.version.
🧹 Nitpick comments (1)
packages/core/src/contexts/translations/index.tsx (1)
42-46: Consider removing the redundant locale change effect.Since
localeis in theuseMemodependency array (line 37), a locale change already creates a new instance initialized withlng: locale. The effect's condition will always be false after instance recreation, making this block effectively dead code.♻️ Proposed simplification
If the locale-handling via useMemo recreation is intentional, consider removing this effect:
- /** - * Handle locale changes. - */ - React.useEffect(() => { - if (i18nInstance.language !== locale) { - i18nInstance.changeLanguage(locale); - } - }, [locale, i18nInstance]); -Alternatively, if you prefer using
changeLanguagefor locale updates, removelocalefrom the useMemo dependencies and rely solely on this effect.
The i18next migration removed _tags from t() calls, so the extraction script was only finding 7 strings instead of ~466. Switch to inferring the namespace from file path (apps/electron/ → electron, else → core) and remove the last _tags: 'core' references from _layout.tsx.
- Use env: block instead of inline ${{ }} interpolation to prevent
command injection from crafted dispatch payloads
- Scope git add to only the translation file instead of git add .






Summary
TRANSLATION_VERSIONCalVer constant (2026.2.0) torxdb-backend.ts, replacing the dynamicsite.wcpos_versiondependencyvprefix from CDN URL to match new CalVer release taggingTranslationProviderby removingwcposVersionobservable and reload effectupdate-translations.ymlreceiver workflow forrepository_dispatchevents from the translations repoTest plan
curl https://cdn.jsdelivr.net/gh/wcpos/translations@2026.2.0/translations/js/de_DE/monorepo/core.json🤖 Generated with Claude Code
Summary by CodeRabbit
Tests
Refactor
Chores