Skip to content

feat(i18n): migrate to symbolic translation keys#75

Merged
kilbot merged 4 commits intomainfrom
feature/symbolic-translation-keys
Feb 6, 2026
Merged

feat(i18n): migrate to symbolic translation keys#75
kilbot merged 4 commits intomainfrom
feature/symbolic-translation-keys

Conversation

@kilbot
Copy link
Copy Markdown
Contributor

@kilbot kilbot commented Feb 6, 2026

Summary

  • Migrates all ~800 t() calls across 170+ files from English string keys (t('Add to Cart')) to structured semantic keys (t('pos_cart.add_to_cart'))
  • Bundles en.json as fallback resource so symbolic keys never display to users
  • Fixes plural handling with proper _one/_other suffixes (e.g. product_found_locally)
  • Adds regional locale fallback in RxDB backend (e.g. fr_CAfr)
  • Renames countshown in 4 non-plural "Showing X of Y" locations to avoid triggering i18next plural resolution

Key format

feature.descriptor using dots as visual separators (flat keys, keySeparator: false):

pos_cart.add_to_cart        ← from screens/main/pos/cart/
products.search_placeholder ← from screens/main/products/
settings.language           ← from screens/main/settings/
auth.connect                ← from screens/auth/
common.cancel               ← shared across many locations

Changes

Area Detail
170+ source files t() calls rewritten to symbolic keys
locales/en/core.json New — 468 bundled English translations
translations/index.tsx fallbackLng: 'en', bundled resources
rxdb-backend.ts Regional locale fallback, removed debug logs
extract-js-strings.js Added symbolic key validation warning
.gitignore Added docs/
apps/electron Submodule pointer updated (see electron PR #134)

Test plan

  • Run app with English locale — all strings display correctly from bundled en.json
  • Switch to French Canadian — translations load from CDN, fall back to French if fr_CA unavailable
  • Switch to non-existent locale — falls back to English (no symbolic keys shown)
  • Barcode scan with 0, 1, and 2+ results — plural forms correct
  • "Showing X of Y" displays correctly in table footers
  • pnpm lint:fix && pnpm test

Follow-up

  • Re-key existing CDN translation files in wcpos/translations repo
  • Apply similar migration to JS code in the PHP plugin

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a comprehensive English locale and improved translation loading with English fallback and regional-locale fallback behavior for more reliable UI localization.
  • Bug Fixes

    • Improved authentication error handling to cover additional server responses (reducing unexpected sign-out/errors).
  • Tests

    • Expanded tests for translation fetch/caching and regional-fallback/error paths.
  • Chores

    • Updated repository ignore rules to exclude docs.

… keys

Replace all ~800 t() calls across 170+ files from raw English string keys
(e.g. t('Add to Cart')) to structured semantic keys (e.g. t('pos_cart.add_to_cart')).

- Bundle en.json fallback so symbolic keys never display to users
- Set fallbackLng: 'en' in i18next config
- Support plurals with proper _one/_other suffixes
- Handle regional locale fallback (e.g. fr_CA → fr) in RxDB backend
- Rename `count` → `shown` in non-plural "Showing X of Y" strings
- Add symbolic key validation to extract script
- Add docs/ to .gitignore
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 6, 2026

Warning

Rate limit exceeded

@kilbot has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 1 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds bundled English translations, converts many UI strings to namespaced i18n keys, refactors the RxDB translations backend to fetch exact and base-language fallbacks, updates i18n initialization, adjusts token-refresh handling for 403, and updates tests and extraction tooling.

Changes

Cohort / File(s) Summary
Repo config & submodule
/.gitignore, apps/electron
Ignored docs/; updated electron submodule commit pointer.
i18n init & English resources
packages/core/src/contexts/translations/index.tsx, packages/core/src/contexts/translations/locales/en/core.json
Added bundled English core.json; changed i18n init (fallbackLng: 'en', load: 'currentOnly') and preloaded en resources.
Translations backend (fetch + fallback)
packages/core/src/contexts/translations/rxdb-backend.ts, packages/core/src/contexts/translations/rxdb-backend.test.ts
Refactored backend: added getBaseLanguage() and fetchTranslations() helpers; read() now checks cache, fetches exact locale, falls back to base language when needed, caches results; expanded tests for cache, regional fallback, and error paths.
Massive UI string migration — core package
packages/core/src/** (many files, e.g., screens/, components/, hooks/, contexts/... )
Replaced hundreds of hard-coded strings with namespaced translation keys (common., auth., orders., pos_cart., products., reports., settings.*, etc.) across forms, dialogs, labels, buttons, toasts and hooks.
POS & Orders area
packages/core/src/screens/main/pos/..., packages/core/src/screens/main/orders/...
Updated cart/order UI and logs to use pos_* / orders.* keys; adjusted related log/toast messages and dialog labels.
Reports, Settings, Tax, and Misc screens
packages/core/src/screens/main/reports/..., .../settings/..., .../tax-rates/...
Replaced UI strings with reports., settings., tax_rates.* and common.* keys; updated table headers, modal titles, and settings labels.
Hooks, mutations, and utilities
packages/core/src/screens/main/contexts/..., packages/core/src/screens/main/hooks/...
Swapped error/success log messages and toast keys to common.* or domain-specific keys; minor early-return addition in one customer flow.
Token refresh & extraction tooling
packages/hooks/src/use-http-client/create-token-refresh-handler.ts, scripts/extract-js-strings.js
Treat 403 like 401 in refresh handler; added post-processing validation to warn about translation keys containing whitespace.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(63,81,181,0.5)
    participant Caller as i18n backend .read(lang, ns)
  end
  rect rgba(33,150,243,0.5)
    participant Cache as translationsState (in-memory)
  end
  rect rgba(0,150,136,0.5)
    participant CDN as Translation CDN (/{lang}/.../core.json)
  end

  Caller->>Cache: check cached[lang][namespace]
  alt cache hit
    Cache-->>Caller: return cached translations
  else cache miss
    Caller->>CDN: fetch /{lang}/.../{namespace}.json
    alt exact locale found
      CDN-->>Caller: JSON data
      Caller->>Cache: cache[lang][namespace]=data
      Caller-->>Caller: callback(data)
    else exact locale missing or empty
      Caller->>Caller: getBaseLanguage(lang)
      alt base language exists
        Caller->>CDN: fetch /{base}/.../{namespace}.json
        alt base found
          CDN-->>Caller: JSON data
          Caller->>Cache: cache[lang][namespace]=data  /* cache under original regional key */
          Caller-->>Caller: callback(data)
        else base not found
          Caller-->>Caller: callback({})
        end
      else no base
        Caller-->>Caller: callback({})
      end
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hopping through keys both clear and new,
I stitched the strings so every locale grew.
English bundled, fallbacks tuned with care —
Now buttons, toasts, and labels speak everywhere. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% 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: a migration from English string keys to symbolic translation keys across the codebase.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/symbolic-translation-keys

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 6, 2026

❌ Test Failures

@wcpos/core: RxDBBackend > read — cache behavior > calls callback with null (no data) when cache is empty

Error: expect(jest.fn()).toHaveBeenCalledWith(...expected)

Expected: null

Number of calls: 0
    at Object.<anonymous> (/home/runner/work/monorepo/monorepo/packages/core/src/contexts/translations/rxdb-backend.test.ts:70:21)
    at Promise.finally.completed (/home/runner/work/monorepo/monorepo/node_modules/jest-circus/build/jestAdapterInit.js:1557:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (/home/runner/work/monorepo/monorepo/node_modules/jest-circus/build/jestAdapterInit.js:

@wcpos/core: RxDBBackend > read — cache behavior > calls callback with null when translationsState is null

Error: expect(jest.fn()).toHaveBeenCalledWith(...expected)

Expected: null

Number of calls: 0
    at Object.<anonymous> (/home/runner/work/monorepo/monorepo/packages/core/src/contexts/translations/rxdb-backend.test.ts:81:21)
    at Promise.finally.completed (/home/runner/work/monorepo/monorepo/node_modules/jest-circus/build/jestAdapterInit.js:1557:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (/home/runner/work/monorepo/monorepo/node_modules/jest-circus/build/jestAdapterInit.js:

@wcpos/core: RxDBBackend > read — fetch behavior > updates cache and resource store when fetch returns new data

Error: expect(jest.fn()).toHaveBeenCalledWith(...expected)

Expected: "es_ES", "core", {"Goodbye": "Adiós", "Hello": "Hola"}, true, true

Number of calls: 0
    at Object.<anonymous> (/home/runner/work/monorepo/monorepo/packages/core/src/contexts/translations/rxdb-backend.test.ts:116:53)

@wcpos/core: RxDBBackend > read — fetch behavior > skips cache update when fetched data matches current cache

Error: expect(jest.fn()).toHaveBeenCalled()

Expected number of calls: >= 1
Received number of calls:    0
    at Object.<anonymous> (/home/runner/work/monorepo/monorepo/packages/core/src/contexts/translations/rxdb-backend.test.ts:144:53)

📊 Test Coverage Report

Package Statements Branches Functions Lines
@wcpos/core 🔴 37.3% 🔴 43.8% 🔴 43.2% 🔴 36.0%
@wcpos/components 🔴 44.9% 🟡 60.9% 🔴 24.2% 🔴 47.3%
@wcpos/database 🔴 33.2% 🔴 35.9% 🔴 40.3% 🔴 32.9%
@wcpos/hooks 🔴 45.5% 🔴 46.8% 🔴 44.8% 🔴 45.7%
@wcpos/utils 🔴 35.0% 🔴 0.0% 🔴 50.0% 🔴 33.3%
@wcpos/query 🟡 68.0% 🔴 52.6% 🟡 66.1% 🟡 68.0%
Average 🔴 44.0% 🔴 40.0% 🔴 44.7% 🔴 43.9%
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/21754021878)

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

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/screens/main/contexts/use-pull-document.ts (1)

64-73: ⚠️ Potential issue | 🟡 Minor

Potential runtime error when accessing err.message without type checking.

On line 65, err.message is accessed directly without verifying that err is an Error object. If err is undefined, null, or a non-Error type, this will throw. The same file already uses the safe pattern on line 72.

Proposed fix for consistent error handling
-			syncLogger.error(t('common.failed_to_save_to_local_database', { error: err.message }), {
+			const errorMessage = err instanceof Error ? err.message : String(err);
+			syncLogger.error(t('common.failed_to_save_to_local_database', { error: errorMessage }), {
 				showToast: true,
 				saveToDb: true,
 				context: {
 					errorCode: ERROR_CODES.TRANSACTION_FAILED,
 					documentId: id,
 					collectionName: collection.name,
-					error: err instanceof Error ? err.message : String(err),
+					error: errorMessage,
 				},
🤖 Fix all issues with AI agents
In `@packages/core/src/screens/auth/hooks/use-api-discovery.ts`:
- Around line 281-283: In the catch block inside the use-api-discovery hook,
guard access to err.message so accessing a non-Error doesn't throw; replace
err.message usage with a safe extraction (e.g., use optional chaining like
err?.message or coerce with String(err) and fallback to
t('auth.failed_to_discover_api_endpoints')) and then call setError(safeMessage)
and setStatus('error') — update the lines that reference err, setError, and
setStatus so they never assume err is an Error instance.

In `@packages/core/src/screens/auth/hooks/use-site-connect.ts`:
- Around line 263-266: In the catch block, accessing err.message can throw if
err is not an Error; update the logic around err, errorMessage, setError and
setStatus in use-site-connect (the catch where errorMessage is derived) to
safely extract a message: check if err is an instance of Error or a non-null
object with a string 'message' property and use that, otherwise fall back to
t('auth.failed_to_connect_to_site') (or String(err) as a final fallback); then
call setError(errorMessage) and setStatus('error') as before.
🧹 Nitpick comments (7)
packages/hooks/src/use-http-client/create-token-refresh-handler.ts (1)

161-172: Limit 403 handling to explicit auth-expired signals.

Refreshing on every 403 can add an extra refresh+retry round-trip for genuine authorization failures and may repeat on each request. Consider guarding 403 handling with an explicit auth-expired marker (error code/header) to avoid unnecessary refresh cycles.

✅ Example guard (adjust codes/headers to your API)
-        canHandle: (error) => {
-            const status = error.response?.status;
-            return status === 401 || status === 403;
-        },
+        canHandle: (error) => {
+            const status = error.response?.status;
+            if (status === 401) return true;
+            if (status === 403) {
+                const code = error.response?.data?.code;
+                const authHeader = error.response?.headers?.['www-authenticate'];
+                return code === 'jwt_auth_invalid_token' || authHeader?.includes('invalid_token');
+            }
+            return false;
+        },
packages/core/src/screens/main/components/product/filter-bar/brands-pill.tsx (1)

56-56: Consider a more descriptive key name than common.id_2.

The key common.id_2 with an interpolated { id } parameter works functionally, but the _2 suffix is opaque. A more descriptive key like common.id_label or common.id_colon would improve maintainability and make the translation file easier to navigate.

packages/core/src/screens/main/components/product/tax-based-on/index.tsx (1)

22-29: Consider using a single translation key for the full label.

String concatenation with ${t('common.tax_based_on')}: ${taxBasedOnSetting} may not translate well for languages with different grammatical structures or word orders. A dedicated key like common.tax_based_on_label with the setting as a placeholder would be more i18n-friendly.

♻️ Suggested approach
-	const taxBasedOnLabel = `${t('common.tax_based_on')}: ${taxBasedOnSetting}`;
+	const taxBasedOnLabel = t('common.tax_based_on_label', { setting: taxBasedOnSetting });

This would require adding a new translation key like "tax_based_on_label": "Tax based on: {{setting}}" to the locale file.

packages/core/src/screens/main/logs/filter-bar.tsx (1)

29-38: Minor namespace inconsistency.

The error label uses common.error while other log levels use the logs.* namespace (logs.warning, logs.info, etc.). This is likely intentional if common.error is shared across the app, but for maintainability, consider using logs.error to keep all log-level labels in the same namespace.

packages/core/src/screens/auth/components/url-input.tsx (1)

45-45: Consider including the colon in the translation.

Appending : via string concatenation outside the translation can cause formatting issues in some locales (e.g., French uses a non-breaking space before colons). Consider including it in the translation string itself for proper localization.

packages/core/src/screens/main/pos/cart/cells/fee-name.tsx (1)

54-54: Consider a more descriptive translation key name.

The key common.edit_2 with the _2 suffix is unconventional. A more self-documenting key like common.edit_item or common.edit_named would better convey that this translation includes an interpolated name parameter (e.g., "Edit {name}").

packages/core/src/contexts/translations/locales/en/core.json (1)

101-101: Normalize non‑ASCII en‑dash characters in key names.
Keys like common.enjoy_more_with_pro_–_upgrade embed a Unicode en dash in the key, which is easy to mistype and can lead to missing translations. Consider renaming to ASCII-only keys and updating call sites.

♻️ Suggested key normalization
-"common.enjoy_more_with_pro_–_upgrade": "Enjoy More with Pro – Upgrade Today!",
+"common.enjoy_more_with_pro_upgrade": "Enjoy More with Pro – Upgrade Today!",
...
-"common.support_future_updates_–_get_pro": "Support Future Updates – Get Pro Now!",
-"common.support_our_development_–_upgrade_to": "Support Our Development – Upgrade to Pro!",
-"common.support_our_work_–_go_pro": "Support Our Work – Go Pro Today!",
+"common.support_future_updates_get_pro": "Support Future Updates – Get Pro Now!",
+"common.support_our_development_upgrade_to": "Support Our Development – Upgrade to Pro!",
+"common.support_our_work_go_pro": "Support Our Work – Go Pro Today!",

Also applies to: 260-262

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 6, 2026

🚀 Deployment Summary

Item Status
Preview URL https://wcpos--q8ib9z5fg0.expo.app
E2E Tests ❌ Failed
Commit 99df995

🔗 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

kilbot and others added 3 commits February 6, 2026 16:06
Cover the full fallback flow: fr_CA → fr → bundled en.json,
including cache hits, 404s, empty responses, and network errors.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@kilbot kilbot merged commit 6ce02eb into main Feb 6, 2026
1 check failed
@kilbot kilbot deleted the feature/symbolic-translation-keys branch February 6, 2026 15:09
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