Skip to content

feat(i18n): decouple translation version with CalVer constant#41

Merged
kilbot merged 4 commits intomainfrom
fix/translation-cdn-path-v187
Feb 6, 2026
Merged

feat(i18n): decouple translation version with CalVer constant#41
kilbot merged 4 commits intomainfrom
fix/translation-cdn-path-v187

Conversation

@kilbot
Copy link
Copy Markdown
Contributor

@kilbot kilbot commented Feb 4, 2026

Summary

  • Add standalone TRANSLATION_VERSION CalVer constant (2026.2.0) to rxdb-backend.ts, replacing the dynamic site.wcpos_version dependency
  • Remove v prefix from CDN URL to match new CalVer release tagging
  • Simplify TranslationProvider by removing wcposVersion observable and reload effect
  • Add update-translations.yml receiver workflow for repository_dispatch events from the translations repo
  • Update electron submodule with i18next migration fixes (PR feat(i18n): migrate Electron translations to i18next with symbolic keys electron#134)

Test plan

  • All 12 translation backend unit tests pass
  • Lint passes with no new warnings
  • Verify CDN URL resolves: curl https://cdn.jsdelivr.net/gh/wcpos/translations@2026.2.0/translations/js/de_DE/monorepo/core.json
  • Test language switching in app with non-English locale

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests

    • Added end-to-end tests for language settings (switching, CDN-loaded translations, and persistence) and comprehensive unit tests for the translation backend.
  • Refactor

    • Translation system now uses an RxDB-backed backend to cache and load CDN translations more robustly.
  • Chores

    • Added an automated workflow to update translation version and updated an Electron submodule reference.

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
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
RxDB Backend
packages/core/src/contexts/translations/rxdb-backend.ts
New exported TRANSLATION_VERSION and RxDBBackend implementing i18next backend: sync-cache reads, background CDN fetch, diff-based cache updates, and RxDB resourceStore writes.
Backend Tests
packages/core/src/contexts/translations/rxdb-backend.test.ts
New unit tests covering static type, URL building, cached reads, background fetch behavior, cache/update semantics, resourceStore interactions, and error handling.
Translations Context
packages/core/src/contexts/translations/index.tsx
Refactored to import/use external RxDBBackend; removed inline backend implementation and related version/reload logic and some observable usage.
E2E Tests
apps/main/e2e/settings.spec.ts
Added "Language Settings" suite and changeLanguage helper; tests change language to French (assert CDN-loaded translations) and verify persistence across settings reopen.
Extraction Script
scripts/extract-js-strings.js
String extraction reworked to group by namespace (electron/core) instead of tags; removes tag logic and writes per-namespace JSON under .translations.
CI Workflow
.github/workflows/update-translations.yml
New workflow triggered by repository_dispatch to update TRANSLATION_VERSION via payload-driven in-file replacement and conditional commit/push.
UI minor
apps/main/app/(app)/(drawer)/_layout.tsx
Removed _tags: 'core' from several t() calls in Drawer.Screen options.
Misc / Manifest
package.json, apps/electron (submodule)`
Minor package.json change and updated electron submodule commit reference.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped to fetch words from a CDN shore,
Cached bits in burrows, then wrote tests galore,
French bloomed in UI, persisted with cheer,
A backend, a workflow, and changes now here —
🥕 the rabbit applauds, translations appear.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: decoupling translation version management by introducing a CalVer constant (TRANSLATION_VERSION), which is the core objective of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/translation-cdn-path-v187

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Consider whether the explicit reloadResources call is necessary or just defensive.

When wcposVersion changes, the useMemo recreates i18nInstance with the new version in backend options, and the useEffect immediately calls reloadResources. 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 any for translationsState, version, and services reduces type safety and IDE support. Consider defining interfaces for these, especially since translationsState appears to have a specific shape (object with language keys and a set method).

🔧 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 using jest.runAllTimers() with fake timers, or a utility like flushPromises.

🔧 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: Hardcoded waitForTimeout can 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 the changeLanguage helper here for consistency.

The test duplicates the language change logic that's already encapsulated in the changeLanguage helper 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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 4, 2026

📊 Test Coverage Report

Package Statements Branches Functions Lines
@wcpos/core 🔴 37.1% 🔴 43.8% 🔴 42.8% 🔴 35.9%
@wcpos/components 🔴 44.9% 🟡 60.9% 🔴 24.2% 🔴 47.3%
@wcpos/database 🔴 33.2% 🔴 35.9% 🔴 40.3% 🔴 32.9%
@wcpos/hooks 🔴 45.4% 🔴 46.4% 🔴 44.8% 🔴 45.6%
@wcpos/utils 🔴 35.0% 🔴 0.0% 🔴 50.0% 🔴 33.3%
@wcpos/query 🟡 68.0% 🔴 52.6% 🟡 66.1% 🟡 68.0%
Average 🔴 43.9% 🔴 39.9% 🔴 44.7% 🔴 43.8%
Coverage Legend
  • 🟢 Good (≥80%)
  • 🟡 Moderate (60-79%)
  • 🔴 Needs improvement (<60%)
  • ⚪ No coverage data
--- 🤖 Updated by GitHub Actions • [View full report](https://github.com/wcpos/monorepo/actions/runs/21683985815)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 4, 2026

🚀 Deployment Summary

Item Status
Preview URL https://wcpos--rcboisw3b3.expo.app
E2E Tests ❌ Failed
Commit 5be2ef3

🔗 Quick Links

❌ Failed Tests (4 total)

settings.spec.ts

  • [free-authenticated] should change language to French and load translations from CDN

    Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

  • [free-authenticated] should persist language after closing and reopening settings

    Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

  • [pro-authenticated] should change language to French and load translations from CDN

    Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed
    ...and 1 more in this file

📋 Full error details (first 5)

settings.spec.ts - should change language to French and load translations from CDN

Project: free-authenticated

Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: getByText('Produit')
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 15000ms�[22m
�[2m  - waiting for getByText('Produit')�[22m

settings.spec.ts - should persist language after closing and reopening settings

Project: free-authenticated

Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: getByText('Produit')
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 15000ms�[22m
�[2m  - waiting for getByText('Produit')�[22m

settings.spec.ts - should change language to French and load translations from CDN

Project: pro-authenticated

Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: getByText('Produit')
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 15000ms�[22m
�[2m  - waiting for getByText('Produit')�[22m

settings.spec.ts - should persist language after closing and reopening settings

Project: pro-authenticated

Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: getByText('Produit')
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 15000ms�[22m
�[2m  - waiting for getByText('Produit')�[22m

📸 Failure Screenshots

📷 settings Language Settings 2a494 sing and reopening settings free authenticated

settings Language Settings 2a494 sing and reopening settings free authenticated

📷 settings Language Settings 2a494 sing and reopening settings free authenticated

settings Language Settings 2a494 sing and reopening settings free authenticated

📷 settings Language Settings 53f0f load translations from CDN free authenticated

settings Language Settings 53f0f  load translations from CDN free authenticated

📷 settings Language Settings 53f0f load translations from CDN free authenticated

settings Language Settings 53f0f  load translations from CDN free authenticated

📷 settings Language Settings 53f0f load translations from CDN pro authenticated

settings Language Settings 53f0f  load translations from CDN pro authenticated

📷 settings Language Settings 53f0f load translations from CDN pro authenticated

settings Language Settings 53f0f  load translations from CDN pro authenticated

🔗 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
@kilbot kilbot changed the title fix(i18n): update CDN path for translations v1.8.7 feat(i18n): decouple translation version with CalVer constant Feb 4, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 locale is in the useMemo dependency array (line 37), a locale change already creates a new instance initialized with lng: 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 changeLanguage for locale updates, remove locale from 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 .
@kilbot kilbot merged commit 4e7cc4c into main Feb 6, 2026
11 of 13 checks passed
@kilbot kilbot deleted the fix/translation-cdn-path-v187 branch February 6, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant