fix(platform): restore i18n keys lost to orphan detector false positives#1642
Conversation
The orphan-key test introduced in #1632 only recognised translation aliases destructured directly from `useT(...)` in the current file, so 109 in-use keys consumed via callback parameters, custom-hook returns, lookup tables, ternaries, or `*Key()` helpers were classified as unused and stripped from en/de/fr.json. Hardens the detector with: - per-file namespace search for dotted suffixes (`'headers.product'` inside a file binding `tables` resolves to `tables.headers.product`), - `tFoo` heuristic that registers callback / parameter aliases, - single-segment lookup-table candidates (`*Key:` properties, `keys: { ... }` blocks, `as const` casts, `*_I18N_KEY` records, `*Key()` function returns), - ternary-aware call-body capture for `t(cond ? 'a' : 'b')`, - bare-`t` and `TFunction` cross-file fallback for files consuming translation keys via passed-in functions. The per-file restriction prevents the previous over-permissive matching from re-appearing — coincidental literals like `'pii.blocked'` (a discriminator code in chat governance) no longer resurrect genuinely orphan translations such as `governance.pii.blocked`. Verified against all 1063 keys removed in #1632: 109 restored as genuinely used, the remaining 961 confirmed orphan.
📝 WalkthroughWalkthroughThis PR expands i18n message coverage across English, German, and French locales with new UI labels and strings for table operations, password flows, team management, Microsoft Entra SSO configuration, API key management, conversation filtering, import/export workflows, two-factor authentication, and moderation provider presets. Concurrently, the i18n test utility is enhanced to detect translation key usage through sophisticated pattern matching, including inferred namespace aliases from function calls and indirect string key resolution from constants and type-annotated properties, with cross-file context awareness. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@services/platform/lib/i18n/messages-usage.test.ts`:
- Around line 209-213: Add a one-line clarifying comment above the dynamic regex
construction for callBodyRe and innerLiteralRe: note that callBodyRe's inner
pattern uses mutually-exclusive alternatives (?:[^()]|\([^()]*\)) so nested
quantifiers do not produce catastrophic backtracking and that the interpolated
alias is constrained to \w-only captures (no injection risk); also add a short
note that innerLiteralRe is intentionally greedy to capture all quoted tokens
within the call body (which may produce false positives that are later filtered
by allFlatKeys). This comment should be placed next to the callBodyRe and
innerLiteralRe declarations so future readers understand the safety and the
trade-off.
- Around line 352-370: The custom body scanner around
KEY_FUNCTION_HEADER_RE/RETURN_LITERAL_RE is vulnerable because the brace counter
(the loop using bodyStart/depth/i over content) does not skip string literals,
template literals, or comments, causing mis-counts; update the scanner in
messages-usage.test.ts to, when scanning from bodyStart, recognize and skip over
single-quoted, double-quoted and backtick template literal ranges (including
escaped chars and ${...} nested expressions), skip // line comments and /* block
comments, and also ignore regex literals if feasible, so that '{' and '}' inside
those constructs do not affect depth; while here, extend KEY_FUNCTION_HEADER_RE
to also detect arrow-function forms of *Key (e.g., identifiers followed by = or
const/let var patterns with =>) and broaden RETURN_LITERAL_RE (or post-process
matched body) to detect simple ternary return patterns so returned literal
branches are captured.
In `@services/platform/messages/en.json`:
- Around line 2822-2823: The file defines bilingual keys titleOne/titleOther for
lowBackupCodes and grace which hardcodes a one/other plural split; replace these
two-key patterns with a single ICU plural key (e.g. "title") using the {count,
plural, ...} form in services/platform/messages/en.json and update call sites
that currently choose between 'titleOne' and 'titleOther' (the conditional like
count === 1 ? 'titleOne' : 'titleOther') to request the single ICU plural key
with the count argument so the i18n library can select the correct plural form;
make the same replacement for the other occurrence mentioned (lines for
lowBackupCodes and grace).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9f6d7baf-a863-46dc-958e-dab281ed9d70
📒 Files selected for processing (4)
services/platform/lib/i18n/messages-usage.test.tsservices/platform/messages/de.jsonservices/platform/messages/en.jsonservices/platform/messages/fr.json
Summary
PR #1632 introduced an orphan-key test that wrongly classified 109 in-use translations as unused and stripped them from
en/de/fr.json. This PR hardens the detector and restores the lost keys.The detector previously only recognised translation aliases destructured directly from
useT(...)in the current file. It missed:({ tTables }) => tTables('headers.product')where the alias arrives via a destructured callback parameter (use-products-table-config.tsx,use-customers-table-config.tsx,use-vendors-table-config.tsx).use-sso-config-form.tsdoesconst { t } = useT('settings')and returnst; consumers likesso-config-dialog.tsxdestructure it from the hook return without a localuseT(...).*Key:properties,keys: { ... }blocks,as constcasts, or const records named*_I18N_KEY(e.g.CATEGORY_I18N_KEYinsanitize-chat-error.ts).t(...)—t(isDisabled ? 'disabled' : 'noMembership')indashboard/$id.tsx.*Key()functions returning translation-key suffixes consumed viat(fn(args))(statusLabelKeyintodo-list-card.tsx).Detector changes — services/platform/lib/i18n/messages-usage.test.ts
'headers.product'in a file bindingtablesresolves totables.headers.product.tFooheuristic —tTables,tCustomers, etc. register as aliases for the corresponding namespace when called, even withoutuseT(...)in the same file. Skipped for ambiguous names liketEntity.xxxKey:property values,keys: { ... }blocks,as constcasts, and*_I18N_KEY-named const records. Per-file namespace resolution by default; global fallback only when the file has no namespace bound (cross-file consumer pattern).t(...)calls now scan the whole argument body (one level of nested parens) for string literals.*Keyfunction body parsing — header regex + brace-counting walk to extract the true function body and find everyreturn '...'literal inside.TFunction/ parameter-tfallback — files importingTFunctionor declaring at: (key: string) => stringparameter trigger global namespace search for their dotted literals.The per-file restriction prevents the previous over-permissive matching from re-appearing — coincidental literals like
'pii.blocked'(a discriminator code in chat governance) no longer resurrect genuinely orphan translations such asgovernance.pii.blocked.Restoration
Restored 109 keys to en.json, de.json, fr.json by reading their values from the pre-#1632 commit. Examples:
tables.headers.{product,description,email,scanned,stock,website,actions,created,title,message}andtables.cells.noEmailsettings.integrations.sso.*consumed bysso-config-dialog.tsxvia the hook patternchat.modelSelector.tags.*(6 keys) consumed vialabelKey:lookupchat.errorHint*(10 keys) andchat.errorGeneratingDescriptionconsumed viaCATEGORY_I18N_KEYmapcustomers.delete{Confirmation,Error,Warning},vendors.delete{Confirmation,Error},websites.toast.deleteErrorconsumed viauseDeleteDialogTranslationstodoList.status*(5 keys) consumed via thestatusLabelKeyfunctionaccessDenied.noMembershipconsumed via a ternaryauth.forcedChange.{description,descriptionAdminSet},conversations.priority.*,conversations.category.*,documents.sourceType.*,governance.defaultModels.*ConflictWarning*,governance.moderationProvider.preset*,twoFactor.{grace,lowBackupCodes}.title{One,Other}, and the*.import.errorCodes.*clustersVerification on the remaining 961 deletions
I ran four independent audits to confirm nothing else is in use:
'<full.key>') across all source — 0 hits.automations.execution.*,automations.monitoring.*,chat.share.*,auth.signup/login/oauth2.*,governance.tabs.*,governance.modelAccess.{allowlist,blocklist},documents.rag.*,mcpServers.oauth2.*,settings.integrations.{circuly,gmail,outlook,protel,shopify}.*, etc.) — none referenced at runtime; all stale entries from features that were renamed or removed.Pre-PR checklist
bun run check(format, lint, typecheck, all tests) — 21/21 tasks pass.services/platform/messages/{en,de,fr}.json.docs/{,de/,fr/}for every user-visible change — N/A (restores translations the runtime already expects; no UI/text changes).bun run --filter @tale/docs lint— N/A.README.md,README.de.md,README.fr.md— N/A.Test plan
bunx vitest run lib/i18n/— 8/8 (orphan-key test passes against the restored JSON).bunx tsc --noEmit— clean.npm run lint --workspace=@tale/platform— 0 warnings, 0 errors.bun run check— all 21 workspace tasks succeed.Summary by CodeRabbit
Documentation
Tests