Date: February 24, 2026
Branch base: main
Total scope: ~3,460 string changes across 19 PRs (18 done, 1 remaining)
Prerequisite: Lingui v5 infrastructure already deployed (PR #19175)
If you're an agent starting a PR:
- Find your PR in the PR Breakdown section
- Read the Scope to know which files/directories to touch
- Follow the Implementation Workflow step-by-step
- Refer to the Migration Pattern Reference for syntax
- Run all Verification checks before committing
- Use the PR Description Template for the PR body
Two types of work per PR:
- String wrapping — wrap hardcoded user-facing strings with Lingui
t/Trans/msgmacros - i18next migration — convert
useXxxTranslation()+t('key')calls to Lingui macros, delete hooks when all consumers are migrated
PO catalog grew by +1,063 msgids (4,611 → 5,674) from these merged PRs:
| PR | What it did | Impact |
|---|---|---|
| feat(i18n): Workspace (#19932 + #19933) | Migrated useWorkspaceTranslation |
PR 1a: DONE |
| feat(i18n): SCIM + Permissions (#19919) | Migrated useScimTranslation + usePermsTranslation |
PR 1b: DONE |
| feat(i18n): Word Add-In Part X (#19941) | Partial Word add-in migration | PR 5a/5b: DONE |
| Localize UI components (#19833) | Wrapped UI primitive strings | PR 14: DONE |
| [vault] localize vault files (#19908) + #20007 | Wrapped vault file strings + migrated useCommonTranslation | PR 7: DONE |
| localization: nudge route strings (#19877) | Converted nudge strings to msg |
misc |
| localization: knowledge source strings (#19872) | Wrapped knowledge source strings | PR 6: partial |
| Localize date/time strings (#19818) | Date/time formatting | misc |
| Localize string sorting/collation (#19820) | Intl.Collator adoption | misc |
| Use msg macro for deferred translations (#19972) | Fixed module-scope t → msg |
fix |
| feat(i18n): migrate assistant to Lingui v5 (#19958) | Migrated useAssistantTranslation (cross-area + workflows) |
PR 4a+4b: DONE |
| feat(i18n): wrap hardcoded strings in UI primitives (#20008) | 30 more UI primitive strings (tooltip, combobox, pagination, etc.) | PR 14: supplement |
| feat(i18n): migrate common area to Lingui v5 (#20025) | Migrated all 14 useCommonTranslation consumers, deleted hook, wrapped ~120 hardcoded strings |
PR 6a+6b: DONE |
| feat(i18n): wrap hardcoded strings in workflows (#20030) | 49 hardcoded strings across 8 workflow files, string wrapping only | PR 12: DONE |
| feat(i18n): migrate sidebar, filter, compare, client-matters (#20032) | 10 i18next→Lingui conversions + 35 hardcoded strings wrapped | PR 11: supplement |
| feat(i18n): wrap workspace settings hardcoded strings (#20035) | Workspace settings hardcoded strings wrapped with Lingui macros | PR 1c: DONE |
| localize: add missing Lingui descriptions for 6 KS (#20083) | 6 knowledge sources missing from translation pipeline | misc |
| feat(i18n): migrate spaces/sharing to Lingui v5 (#20036) | 31 useSpacesTranslation + 3 useSharingTranslation consumers migrated, hooks deleted |
PR 8: DONE |
| feat(i18n): clean up legacy i18next test patterns in library + playbooks (#20026) | Removed i18nNamespaces + getTranslation from 6 test files | PR 13: DONE |
| refactor(i18n): migrate settings groups and export-templates to lingui v5 (#20093) | Migrated useGroupsTranslation (6 consumers) + useExportFormatsTranslation (5 consumers), deleted both hooks, wrapped hardcoded strings |
PR 2: DONE |
| feat(i18n): migrate profile + external connections to Lingui v5 (#20100) | Migrated useProfileTranslation (3 consumers) + useExternalConnectionsTranslation (2 consumers), deleted both hooks |
PR 3: DONE |
Hooks deleted: useWorkspaceTranslation, useScimTranslation, usePermsTranslation, useAssistantTranslation, useVaultTranslation, useVaultReviewTranslation, useWordTranslation, useOutlookTranslation, useWorkflowsTranslation, useLibraryTranslation, useCommonTranslation, useSpacesTranslation, useSharingTranslation, useGroupsTranslation, useExportFormatsTranslation, useProfileTranslation, useExternalConnectionsTranslation
JSON-only keys remaining: 1,542 total (down from 1,783). Most are dead — only tokens.json (37 keys) has live consumers
i18next hook consumers remaining: ~2 consumer files across 1 hook (useTokenTranslation: 2 consumers in workspace-details-tokens). useSettingsTranslation and useAssistantTranslation have definitions but zero real consumers.
| PR | Status | Notes |
|---|---|---|
| 1a (Workspace i18next) | ✅ Done | Merged #19932 + #19933 |
| 1b (SCIM + permissions) | ✅ Done | Merged #19919 |
| 1c (Workspace hardcoded) | ✅ Done | Merged #20035. All workspace settings hardcoded strings wrapped |
| 2 (Settings: integrations/groups/export) | ✅ Done | Merged #20093. useGroupsTranslation + useExportFormatsTranslation deleted (0 consumers) |
| 3 (Settings: remaining customer-facing) | ✅ Done | Merged #20100. useProfileTranslation + useExternalConnectionsTranslation deleted (0 consumers) |
| 4a (Assistant: doc-editor + hooks) | ✅ Done | Merged as part of #19958 |
| 4b (Assistant: workflows + features) | ✅ Done | Merged as #19958 (combined with 4a). 1 commented-out consumer remains in harvey-doc-toolbar.tsx |
| 5a (Word add-in: taskpane) | ✅ Done | Merged across #19671, #19873, #19941. 1 trivial string remains |
| 5b (Word add-in: drafting + remaining) | ✅ Done | Zero i18next imports, ~1 hardcoded string left |
| 6a (Common: sharing/editor/flows) | ✅ Done | Merged #20025. useCommonTranslation deleted (0 consumers) |
| 6b (Common: remaining components) | ✅ Done | Merged as part of #20025 (combined with 6a) |
| 7 (Vault + vault-review) | ✅ Done | Merged #20007. useVaultTranslation + useVaultReviewTranslation deleted (0 consumers) |
| 8 (Spaces/Shared + sharing) | ✅ Done | Merged #20036. useSpacesTranslation + useSharingTranslation deleted (0 consumers) |
| 9 (Utils + types) | Remaining | Hardcoded only, no i18next hooks |
| 10 (Workflow builder) | ✅ Done | Zero i18next imports, fully on Lingui. 133 JSON-only keys are dead (no hook) |
| 11 (Other: office/outlook/sidebar) | ✅ Done | Merged #19431, #19873, #20032. Fully on Lingui. office/outlook JSON-only keys dead |
| 12 (Workflows) | ✅ Done | Merged #20030. 49 hardcoded strings wrapped |
| 13 (Library + Playbooks) | ✅ Done | PR #20026. Component code already migrated; cleaned up 6 test files (removed i18nNamespaces + getTranslation) |
| 14 (UI primitives) | ✅ Done | Merged #19833 + #20008 (30 more strings) |
Summary: 18 done, 1 remaining
| Metric | Count | % |
|---|---|---|
| Lingui PO catalog | 5,807 | 86.2% of total |
| Legacy i18next (live, needs migration) | ~37 | 0.5% |
| Hardcoded (lint-scanned, needs wrapping) | ~1,951 | 29.0% |
| Grand total (customer-facing) | ~6,735 | |
| Dead JSON keys excluded | 1,468 | (17 hooks dead — only useTokenTranslation has real consumers) |
- i18n coverage: 86% — ~1,951 customer-facing strings have zero i18n wrapping (lint-verified)
- Lingui migration: 99.4% — only ~37 live i18next keys remain (2 token files)
- Note: hardcoded count revised upward from ~900 estimate to ~1,951 after full ESLint lint scan with
lingui/no-unlocalized-stringsontos/lingui-lint-enforcementbranch
PR 1a (Settings: workspace i18next migration) ──── independent
PR 1c (Settings: workspace hardcoded strings) ──── independent
PR 1b (Settings: SCIM + permissions) ──────────── independent
PR 2 (Settings: integrations/groups/export) ────── independent
PR 3 (Settings: remaining customer-facing) ──────── independent
PR 4a (Assistant: doc-editor + hooks) ─────────── independent
PR 4b (Assistant: workflows + features) ───────── after PR 4a
PR 5a (Word add-in: taskpane features) ─────────── independent
PR 5b (Word add-in: drafting + remaining) ──────── independent
PR 6a (Common: sharing + editor + flows) ──────── independent
PR 6b (Common: remaining components) ──────────── after PR 6a
PR 7 (Vault + vault-review) ────────────────────── independent
PR 8 (Spaces/Shared + sharing) ─────── independent (useAssistantTranslation dead after #19958)
PR 9 (Utils + types) ───────────────────────────── independent
PR 10 (Workflow builder) ───────────────────────── independent
PR 11 (Other: office, outlook, sidebar, etc.) ──── independent
PR 12 (Workflows) ──────────────────────────────── independent
PR 13 (Library + Playbooks) ────────────────────── independent
PR 14 (UI primitives) ──────────────────────────── independent
Most PRs are independent and can be worked in parallel. PR 4b depends on 4a (shared hook changes). PR 6b depends on 6a (shared hook changes). PR 8 no longer blocked — useAssistantTranslation effectively dead after PR 4a+4b (#19958).
Merged as: #19932 + #19933 | Reviewer: @harveyai/platform
Scope: Convert all useWorkspaceTranslation() + t('key') calls to Lingui macros.
src/components/settings/workspace/— 32 consumer files usinguseWorkspaceTranslationsrc/components/settings/configure-sso/— 2 files usinguseWorkspaceTranslationsrc/components/settings/user-management/— 3 files usinguseWorkspaceTranslation
i18next migration (208 JSON-only keys):
| Namespace | JSON-only keys | Hook | Consumer files |
|---|---|---|---|
workspace |
208 | useWorkspaceTranslation |
37 |
String wrapping: None — hardcoded strings are handled in PR 1c.
JSON namespace file:
public/locales/en-US/workspace.json
Hook deletion: DELETE useWorkspaceTranslation ONLY IF all consumers (including cross-area) are migrated. If SCIM/perms files still import it, defer deletion to PR 1b.
Size: M (~180 strings) | Reviewer: @harveyai/platform
Scope: Wrap all remaining hardcoded user-facing strings in workspace settings directories.
src/components/settings/workspace/src/components/settings/configure-sso/src/components/settings/user-management/- Any other customer-facing settings files with hardcoded strings not in PRs 1a, 1b, 2, or 3
i18next migration: None — i18next migration is handled in PR 1a.
String wrapping (~180 hardcoded): Wrap user-facing strings in the above directories. Skip internal admin subdirectories:
src/components/settings/internal-admin/**— SKIPsrc/components/settings/experiment/**— SKIPsrc/components/settings/vault-inspector/**— SKIPsrc/components/settings/vault-testing/**— SKIPsrc/components/settings/vault-review-jobs/**— SKIPsrc/components/settings/document-debugger/**— SKIPsrc/components/settings/file-upload-debugger/**— SKIPsrc/components/settings/incident-management/**— SKIPsrc/components/settings/pwc/**— SKIPsrc/components/settings/models/**— SKIP
Merged as: #19919 | Reviewer: @harveyai/platform
Scope:
src/components/settings/scim/— 5 consumer files usinguseScimTranslationsrc/components/settings/roles/— 4 consumer files usingusePermsTranslation
i18next migration (188 JSON-only keys):
| Namespace | JSON-only keys | Hook | Consumer files |
|---|---|---|---|
scim |
128 | useScimTranslation |
5 |
perms |
60 | usePermsTranslation |
4 |
String wrapping (~20 hardcoded): Wrap remaining user-facing strings in SCIM and permissions directories.
JSON namespace files:
public/locales/en-US/scim.jsonpublic/locales/en-US/perms.json
Hook deletion: DELETE useScimTranslation, usePermsTranslation, and useWorkspaceTranslation (if not deleted in PR 1a).
Size: M (~351 strings) | Reviewer: @harveyai/platform
Scope:
src/components/settings/integrations/— files usinguseIntegrationsTranslation(if hook exists) or i18next patternssrc/components/settings/groups/— 6 consumer files usinguseGroupsTranslationsrc/components/settings/export-templates/— 5 consumer files usinguseExportFormatsTranslation
i18next migration (151 JSON-only keys):
| Namespace | JSON-only keys | Hook | Consumer files |
|---|---|---|---|
integrations |
52 | (check for hook or direct useTranslation) |
~5 |
groups |
50 | useGroupsTranslation |
6 |
export_formats |
49 | useExportFormatsTranslation |
5 |
String wrapping (~200 hardcoded): Wrap remaining user-facing strings in the above directories.
JSON namespace files:
public/locales/en-US/integrations.jsonpublic/locales/en-US/groups.jsonpublic/locales/en-US/export_formats.json
Hook deletion: DELETE useGroupsTranslation, useExportFormatsTranslation.
Size: M (~189 strings) | Reviewer: @harveyai/platform
Scope:
src/components/settings/profile/— 3 files usinguseProfileTranslationsrc/components/settings/external-connections/— 2 files usinguseExternalConnectionsTranslationsrc/components/settings/sharing/— usesuseSharingTranslationsrc/components/settings/notifications/src/components/settings/announcements/src/components/settings/playbooks/— note: some files may still use i18nextsrc/components/settings/knowledge/- Any remaining customer-facing settings not covered by PRs 1-2
i18next migration: Profile (12 JSON-only), external_connections (25 JSON-only), collab_requests (17 JSON-only), announcements (6 JSON-only), plus any settings namespace keys.
String wrapping (~189 hardcoded): Remaining hardcoded strings in customer-facing settings.
JSON namespace files:
public/locales/en-US/profile.jsonpublic/locales/en-US/external_connections.jsonpublic/locales/en-US/collab_requests.jsonpublic/locales/en-US/announcements.jsonpublic/locales/en-US/settings.json(if exists)
Hook deletion: DELETE useProfileTranslation, useExternalConnectionsTranslation, useSettingsTranslation.
Merged as: #19958 (combined with 4b) | Reviewer: @harveyai/assistant
Scope:
src/components/assistant/doc-editor/— 4 filessrc/components/assistant/hooks/— hook files usinguseAssistantTranslationsrc/components/assistant/features/— non-workflow feature filessrc/components/assistant/*.tsx— top-level assistant component files (e.g., assistant-chat, assistant-header)- Cross-area consumers:
src/components/common/knowledge-sources/(2 files),src/components/sidebar/(1 file),src/components/workflow-builder/(1 file)
i18next migration (~100 JSON-only keys):
| Namespace | JSON-only keys | Hook | Consumer files |
|---|---|---|---|
assistant (partial) |
~100 | useAssistantTranslation |
~15 |
String wrapping (~80 hardcoded): Wrap user-facing strings in doc-editor, hooks, features, and top-level assistant files.
JSON namespace file:
public/locales/en-US/assistant.json
Hook deletion: Do NOT delete useAssistantTranslation yet — PR 4b still has consumers.
Merged as: #19958 (combined with 4a) | Reviewer: @harveyai/assistant
Scope:
src/components/assistant/workflows/— 25+ workflow component filessrc/components/assistant/workflows/hooks/— workflow hookssrc/components/assistant/workflows/components/— all workflow sub-components- Any remaining assistant files not covered by PR 4a
i18next migration (~152 JSON-only keys):
| Namespace | JSON-only keys | Hook | Consumer files |
|---|---|---|---|
assistant (remaining) |
~152 | useAssistantTranslation |
~19 |
String wrapping (~162 hardcoded): Wrap all remaining user-facing hardcoded strings in assistant workflow components.
JSON namespace file:
public/locales/en-US/assistant.json
Hook deletion: DELETE useAssistantTranslation after ALL consumers (including cross-area from PR 4a) are migrated.
Direct react-i18next consumers to also migrate:
src/components/assistant/workflows/components/assistant-feedback-popup-module.tsx(importsuseTranslationdirectly)src/components/assistant/workflows/hooks/use-feedback-submission.tsx(importsuseTranslationdirectly)
Merged across: #19671, #19873, #19941 | Reviewer: @harveyai/embedded-experience
Scope:
word-add-in/src/features/taskpane/— all taskpane feature components (panels, dialogs, navigation)word-add-in/src/features/taskpane/components/— sub-components
i18next migration (~50 JSON-only keys):
| Namespace | JSON-only keys |
|---|---|
word (partial) |
~50 |
Note: The Word add-in may use a different i18next setup. Check for useWordTranslation or direct useTranslation('word') calls.
String wrapping (~140 hardcoded): Wrap user-facing hardcoded strings in taskpane features — button labels, panel titles, status messages.
JSON namespace file:
public/locales/en-US/word.json
IMPORTANT: Read word-add-in/OFFICEJS.md before modifying any Office.js code.
Merged across: #19671, #19873, #19941 | Reviewer: @harveyai/embedded-experience
Scope:
word-add-in/src/features/drafting/— drafting-related componentsword-add-in/src/features/— all other feature directories (auth, settings, etc.)word-add-in/src/components/— shared Word add-in componentsword-add-in/src/utils/— utility files with user-facing stringsword-add-in/src/hooks/— hooks with user-facing strings
i18next migration (~54 JSON-only keys):
| Namespace | JSON-only keys |
|---|---|
word (remaining) |
~54 |
String wrapping (~124 hardcoded): Wrap all remaining user-facing hardcoded strings in Word add-in.
JSON namespace file:
public/locales/en-US/word.json
IMPORTANT: Read word-add-in/OFFICEJS.md before modifying any Office.js code.
Size: M (~155 strings) | Reviewer: @harveyai/frontend, @harveyai/platform
Scope:
src/components/common/sharing/— 3 files usinguseSharingTranslationsrc/components/common/editor/— editor-related common componentssrc/components/common/flows/— flow/step componentssrc/components/common/feedback/— feedback componentssrc/components/common/dropzone/— file upload dropzonesrc/components/common/maintenance-modal.tsx— direct react-i18next consumer
i18next migration (~70 JSON-only keys):
| Namespace | JSON-only keys | Hook | Consumer files |
|---|---|---|---|
common (partial) |
~70 | useCommonTranslation |
~8 |
String wrapping (~85 hardcoded): Wrap hardcoded strings in sharing, editor, flows, feedback, and dropzone components.
JSON namespace file:
public/locales/en-US/common.json
Hook deletion: Do NOT delete useCommonTranslation yet — PR 6b still has consumers.
Direct react-i18next consumers to also migrate:
src/components/common/maintenance-modal.tsx
Size: M (~138 strings) | Reviewer: @harveyai/frontend, @harveyai/platform
Depends on: PR 6a
Scope:
src/components/common/knowledge-sources/— knowledge source componentssrc/components/common/integrations/— integration componentssrc/components/common/product-tours/— product tour componentssrc/components/common/export/— export dialogs- All remaining
src/components/common/files not covered by PR 6a - Cross-area
useCommonTranslationconsumers: vault (1), settings (2), filter (1), client-matters (1), workflow-builder (2), shared (2)
i18next migration (~67 JSON-only keys):
| Namespace | JSON-only keys | Hook | Consumer files |
|---|---|---|---|
common (remaining) |
~67 | useCommonTranslation |
~8 + cross-area |
String wrapping (~71 hardcoded): Wrap remaining hardcoded strings in common components — product tours, export dialogs, knowledge sources, etc.
JSON namespace file:
public/locales/en-US/common.json
Hook deletion: DELETE useCommonTranslation after ALL consumers (including cross-area: vault, settings, filter, client-matters, workflow-builder, shared) are migrated.
Merged as: #19908 + #20007 | Reviewer: @harveyai/vault-eng
Scope:
src/components/vault/— vault query/file UI
i18next migration (209 JSON-only keys):
| Namespace | JSON-only keys |
|---|---|
vault |
176 |
vault-review |
33 |
Note: Check if useVaultTranslation still exists. If already deleted from a prior migration, this PR only needs to handle the JSON-only keys that don't have Lingui equivalents yet plus hardcoded strings.
String wrapping (48 hardcoded): Wrap remaining hardcoded strings in vault components.
JSON namespace files:
public/locales/en-US/vault.jsonpublic/locales/en-US/vault-review.json
Size: M (~185 strings) | Reviewer: @harveyai/platform, @harveyai/growth-eng
Depends on: PR 4 (Assistant) — because this PR deletes useAssistantTranslation if PR 4 didn't
Scope:
src/components/shared/— 31 consumer files usinguseSpacesTranslationsrc/components/common/sharing/— 3 files usinguseSharingTranslationsrc/components/sidebar/— 1 file usinguseAssistantTranslation, 1 usinguseSpacesTranslationsrc/components/client-matters/pages/ClientViewPage/— 1 file usinguseSpacesTranslationsrc/components/common/create-space-dialog/— 2 files usinguseSpacesTranslationsrc/components/common/add-all-resources-dialog/— 1 file usinguseSpacesTranslation
i18next migration (185 JSON-only keys):
| Namespace | JSON-only keys | Hook | Consumer files |
|---|---|---|---|
spaces |
120 | useSpacesTranslation |
31 |
sharing |
65 | useSharingTranslation |
3 |
String wrapping: None — these areas were covered in prior migration PRs.
JSON namespace files:
public/locales/en-US/spaces.jsonpublic/locales/en-US/sharing.json
Hook deletion: DELETE useSpacesTranslation, useSharingTranslation, and useAssistantTranslation (if not already deleted in PR 4).
Size: M (~168 strings) | Reviewer: @harveyai/frontend
Scope:
src/utils/— toast messages, error messages, date utils, task definitionssrc/types/— file type display names
i18next migration: None — these areas don't use i18next hooks.
String wrapping (168 hardcoded):
src/utils/utils.ts— download error/success messages shown in toastssrc/utils/task-definitions.ts— task type labels (~40 instances)src/utils/docx.ts— document export headings/textsrc/utils/markdown.ts— export section headingssrc/api/index.ts— file processing error messagessrc/services/files/file-processing-poller.ts— timeout/error messagessrc/hooks/use-voice-input.ts— voice recording messagessrc/types/file.ts— file type display names (FLAC Audio, Java Source File, etc.)
Note on module-scope constants: Many of these files define constants at module scope. Use msg (not t) for module-scope strings. Consumers must use useLingui() + _() to resolve. See "Critical Rules" section.
Size: M (~165 strings) | Reviewer: @harveyai/workflows
Scope:
src/components/workflow-builder/— 2 files usinguseCommonTranslation(cross-area)
i18next migration (137 JSON-only keys):
| Namespace | JSON-only keys |
|---|---|
workflow-builder |
137 |
Check for useWorkflowBuilderTranslation or similar hooks. If none, check for direct useTranslation('workflow-builder') calls.
String wrapping (28 hardcoded): Wrap remaining hardcoded strings in workflow builder components.
JSON namespace file:
public/locales/en-US/workflow-builder.json
Direct react-i18next consumer to also migrate:
src/components/workflow-builder/components/workflow-builder-file-sources-selector.tsx
Size: M (~224 strings) | Reviewer: @harveyai/embedded-experience, @harveyai/platform
Scope:
src/office/— Office shared featuresoutlook-add-in/src/— Outlook add-in UIsrc/components/sidebar/— 2 files using hookssrc/components/filter/— 1 file usinguseCommonTranslation,useSpacesTranslation,useSharingTranslationsrc/components/compare/- Any remaining files not covered by PRs 1-10
i18next migration (117 JSON-only keys):
| Namespace | JSON-only keys |
|---|---|
outlook |
16 |
office |
13 |
history |
10 |
matters |
10 |
workflows |
8 |
| Other (6 ns) | 60 |
String wrapping (107 hardcoded): Wrap remaining hardcoded strings across these areas.
Size: S (~56 strings) | Reviewer: @harveyai/workflows
Scope:
src/components/workflows/— workflow cards, browser UI
String wrapping (56 hardcoded): Wrap hardcoded strings in workflow components.
Merged as: #20026 | Reviewer: @harveyai/growth-eng
Scope:
src/components/library/— library components
i18next migration (84 JSON-only keys):
| Namespace | JSON-only keys |
|---|---|
library |
45 |
playbooks |
39 |
JSON namespace files:
public/locales/en-US/library.jsonpublic/locales/en-US/playbooks.json
Merged as: #19833 | Reviewer: @harveyai/frontend
Scope:
src/components/ui/— calendar, tooltip, combobox, data-table, search-input
String wrapping (30 hardcoded): Wrap user-facing strings in UI primitives (aria-labels, placeholder text, button labels).
# Find i18next hook consumers in your target area
rg "useXxxTranslation" src/components/<area>/ --files-with-matches
# Find direct react-i18next imports
rg "from 'react-i18next'" src/components/<area>/ --files-with-matches
# Find hardcoded strings (check eslint violations)
npx eslint --no-eslintrc -c eslint.lingui-scan.config.mts src/components/<area>/ 2>&1 | grep "lingui/no-unlocalized-strings"Do not partially migrate a file. Every file you touch must be fully converted.
For each file:
-
Remove old hook import and call:
// DELETE: import { useXxxTranslation } from 'components/.../hooks/use-xxx-translation' const { t } = useXxxTranslation()
-
Add Lingui import (third-party section):
import { t } from '@lingui/core/macro' // or if you also need plural/msg/Trans: import { msg, plural, t } from '@lingui/core/macro' import { Trans } from '@lingui/react/macro'
-
Convert
t('key')→t`English text`:- Look up the key in the JSON namespace file to get the English text
- Simple:
t('key')→t`English text` - Interpolation:
t('key', { name })→t`Hello ${name}` - Ensure variables are
string | number, notundefined. Add?? ''fallback.
-
Wrap hardcoded strings:
- Button labels, titles, headings, descriptions, tooltips, placeholder text, error messages, aria-labels
- Use
t`Text`for inline strings,msg`Text`for constants/descriptors
-
Convert plurals:
import { plural } from '@lingui/core/macro' plural(count, { one: '# file', other: '# files' })
-
Convert
<Trans>components:// Before (react-i18next): import { Trans } from 'react-i18next' <Trans i18nKey="vault.upload_count">Uploading <strong>{{count: n}}</strong> files</Trans> // After (Lingui): import { Trans } from '@lingui/react/macro' <Trans>Uploading <strong>{n}</strong> files</Trans>
-
Convert constants/static descriptors:
// Module-scope — MUST use msg, NOT t import { msg } from '@lingui/core/macro' const label = msg`Upload` // With ICU placeholders — MUST use object form: const err = msg({ message: 'Failed to load {name}' }) // Consumer resolves at render time: import { useLingui } from '@lingui/react' const { _ } = useLingui() <Button>{_(label)}</Button>
- Remove translation hook mocks (
vi.mock('components/.../use-xxx-translation', ...)) - Update assertions:
expect(screen.getByText('translation.key'))→expect(screen.getByText('English text')) - Remove unused translation imports
rg "useXxxTranslation" src/ --files-with-matches
# If zero results, delete the hook definition fileyarn lingui extract # Update PO catalogs
yarn typecheck # Zero type errors
yarn lint # Zero lint errors
yarn test # Tests pass| Pattern | Syntax |
|---|---|
| Simple string | t`Cancel` |
| Interpolation | t`Hello ${name}` (variable must be string | number) |
| Plural | plural(count, { one: '# item', other: '# items' }) |
| JSX with markup | <Trans>Click <strong>here</strong></Trans> |
| Static descriptor | msg`Upload` |
| Descriptor + ICU | msg({ message: 'Error with {name}' }) |
| Runtime render | _(descriptor) via useLingui() or i18n._(descriptor) outside React |
| Disambiguation | t({ message: 'Other', context: 'feedback.positive' }) |
-
Module-scope strings MUST use
msg, NOTt.tevaluates once at import time and produces stale translations on locale switch.msgcreates a deferred descriptor resolved at render time. -
@linguiimports go in the third-party section (alongside React, lodash, etc.), NOT where the old hook import was. -
Variables in tagged templates must be
string | number, notundefined. Add?? ''fallback. -
Plural branches use
#for the count, never${count}. -
plural()is standalone — do NOT nest insidet``. -
Do NOT interpolate hardcoded English words into
ttemplates. Enumerate full sentences per type instead. -
Look up the EXACT English text from the JSON namespace file. Do not guess or abbreviate.
-
Do NOT use
tin React dependency arrays — it's a macro, not a hook. -
Run
lingui extractbefore committing. -
Skip internal admin paths — anything gated behind
IsInternalAdminReaderdoes not need localization.
These directories contain internal-only tools and should NOT be localized:
src/components/settings/internal-admin/**
src/components/settings/experiment/**
src/components/settings/document-debugger/**
src/components/settings/file-upload-debugger/**
src/components/settings/incident-management/**
src/components/settings/vault-inspector/**
src/components/settings/vault-testing/**
src/components/settings/vault-review-jobs/**
src/components/settings/workflow-permissions/**
src/components/settings/workspace-workflows/**
src/components/settings/models/**
src/components/settings/pwc/**
src/components/settings/library/settings-library-events-manager.tsx
src/components/common/dev-auth-inspector/**
src/components/research/**
## Summary
Migrate **{area}** from i18next to Lingui v5 and wrap hardcoded user-facing strings.
Part of the [i18n completion project](link). PR {N} of 19.
### What changed
- Wrapped {X} hardcoded user-facing strings with Lingui `t`/`Trans`/`msg` macros
- Converted {Y} `useXxxTranslation()` + `t('key')` calls to Lingui source-string-as-ID macros
- {Deleted / Did NOT delete} hook files: {list}
- Updated PO catalogs via `lingui extract`
### What did NOT change
- i18next infrastructure remains (removed in a future phase)
- No behavioral changes — English strings render identically
- `public/locales/*.json` files untouched
- Internal admin pages not localized (by design)
## Test Plan
- [ ] `yarn typecheck` passes
- [ ] `yarn lint` passes
- [ ] `yarn test` passes
- [ ] `yarn lingui extract && git diff --exit-code` — no catalog drift
- [ ] Spot-check {area} in browser — strings render correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)- Zero imports of old hooks (
useXxxTranslation) remain in target area - Zero
react-i18nextimports remain in target area - No module-scope
tcalls — all module-scope strings usemsg - No
${count}in plural branches (use#) - No
undefinedvariables in tagged templates - No hardcoded English interpolated into
ttemplates -
yarn lingui extractruns without errors - Test files updated: no translation mocks, assertions use English text
-
@linguiimports are in the third-party section - Internal admin paths not touched
| # | PR Scope | Files | Hardcoded | i18next→Lingui | Total | Size |
|---|---|---|---|---|---|---|
| 1a | Settings: workspace i18next migration | ~37 | — | 208 | ~208 | ✅ |
| 1c | Settings: workspace hardcoded strings | ~40 | ~180 | — | ~180 | M |
| 1b | Settings: SCIM + permissions | ~25 | ~20 | 188 | ~208 | ✅ |
| 2 | Settings: integrations, groups, export | ~40 | ~200 | 151 | ~351 | M |
| 3 | Settings: remaining customer-facing | ~35 | ~189 | ~60 | ~249 | M |
| 4a | Assistant: doc-editor + hooks | ~20 | ~80 | ~100 | ~180 | ✅ |
| 4b | Assistant: workflows + features | ~65 | ~162 | ~152 | ~314 | ✅ |
| 5a | Word add-in: taskpane features | ~50 | ~140 | ~50 | ~190 | ✅ |
| 5b | Word add-in: drafting + remaining | ~53 | ~124 | ~54 | ~178 | ✅ |
| 6a | Common: sharing, editor, flows | ~35 | ~85 | ~70 | ~155 | M |
| 6b | Common: remaining components | ~35 | ~71 | ~67 | ~138 | M |
| 7 | Vault + vault-review | ~30 | 48 | 209 | ~257 | ✅ |
| 8 | Spaces/Shared + sharing | ~40 | — | 185 | ~185 | M |
| 9 | Utils + types | ~20 | 168 | — | ~168 | M |
| 10 | Workflow builder | ~15 | 28 | 137 | ~165 | M |
| 11 | Other (office, outlook, etc.) | ~30 | 107 | 117 | ~224 | M |
| 12 | Workflows | ~15 | 56 | — | ~56 | S |
| 13 | Library + Playbooks | ~20 | — | 84 | ~84 | S |
| 14 | UI primitives | ~10 | 30 | — | ~30 | ✅ |
| Total | 1,688 | 1,832 | 3,520 |
Sizing: S = <100 strings, M = 100–350, L = 350+ All PRs are now ≤65 files (benchmark: 50, with small overages for tightly coupled directories).
After completing each migration PR, spin up a review subagent to catch issues the implementer missed. This is a mandatory step — it has caught real regressions in every prior migration PR.
- After every commit within a migration PR (not just at the end)
- Especially after batch-converting files where it's easy to miss one
Use the compound-engineering:review:kieran-typescript-reviewer agent type (or compound-engineering:review:pattern-recognition-specialist) with this prompt:
Review the latest commit(s) on this branch for Lingui i18n migration correctness.
Check for these specific issues (ordered by severity — all are real bugs from prior PRs):
**P0 — Silent runtime bugs:**
1. Module-scope `t` calls: Any `export const ... = t`...`` outside a function/component body must be `msg`. `t` evaluates once at import time and produces stale translations on locale switch.
2. Consumer resolution: Every consumer of a `msg` descriptor must use `useLingui()` + `_()` to resolve it. Components expecting `string` props must receive `_(descriptor)`, not the raw descriptor.
3. Collateral deletion: Diff against main and check for removed non-i18n lines — interface fields, object properties, function args that were accidentally deleted alongside hook removals.
**P1 — Incorrect translations:**
4. Hardcoded English in `t` interpolation: If a `t` template interpolates a variable that holds hardcoded English (e.g., `'workflow'`, `'page'`), replace with full concrete sentences per type.
5. Bare template literals: Any `` `text ${var}` `` that is user-facing must be wrapped in `t` — bare template literals are invisible to translators.
6. Hardcoded fallback strings: Check for `?? 'Restricted'` or similar — these must be `` ?? t`Restricted` ``.
7. Untranslated button/label text: Inline text like `<Button>Copy link</Button>` must use `` {t`Copy link`} ``.
8. `t`Foo`.toLowerCase()`: Lowercasing a translated word is fragile across languages. Use a separate full sentence instead.
**P2 — Code quality:**
9. Import ordering: `@lingui/*` imports must be in the third-party section, not local.
10. Unused imports: If all usages were converted to `msg`, remove unused `t` import.
11. Missed files: Check if any sibling files have the same pattern but were not converted.
12. Type safety: Cypress/test updates — tests importing `msg` constants must use `.message` in assertions.
Run this BEFORE the review agent (it's instant, the agent takes minutes):
# For EVERY modified file, check diff contains ONLY i18n changes
for f in $(git diff --name-only origin/main); do
echo "=== $f ==="
git diff origin/main -- "$f" | grep '^[+-]' | grep -v '^[+-][+-][+-]' \
| grep -v '@lingui' | grep -v 'Translation' | grep -v 'translation' \
| grep -v "from '@lingui" | grep -v 'import { t }' | grep -v 'import { msg }' \
| grep -v 'import { plural }' | grep -v 'import { Trans }' \
| grep -v 'const { t }' | grep -v 'const { _ }' | grep -v 'useLingui' \
| grep -v 'react-i18next' | grep -v 'useTranslation'
done
# Any output = potential collateral damage. Investigate each line.These are real bugs found in production migration PRs. Every one of them will recur if you don't check for them.
The #1 review finding. Module-scope t calls evaluate once at import time and produce stale translations when the user switches locale.
// BAD — evaluates once at import time
export const FORM_HEADING = t`What is your job title?`
// GOOD — deferred descriptor, resolved at render time
export const FORM_HEADING = msg`What is your job title?`
// Consumer: const { _ } = useLingui(); <Modal heading={_(FORM_HEADING)} />Where to look: Any file that exports constants at module scope, defines option arrays/objects outside component bodies, or uses const HEADING = t...`` at the top of a file.
When a translated sentence varies by type, do NOT interpolate hardcoded English words as variables.
// BAD — 'workflow' and 'page' are hardcoded English
const type = shareType === ShareType.VAULT_PROJECT ? t`Vault`.toLowerCase() : 'workflow'
return t`This ${type} is only accessible to you`
// GOOD — each sentence is a complete, translatable unit
switch (shareType) {
case ShareType.VAULT_PROJECT:
return t`This vault is only accessible to you`
case ShareType.WORKFLOW_RUN:
return t`This workflow is only accessible to you`
}Detection: After migrating a file, grep for t`...${...}...` and trace each interpolated variable. If it holds hardcoded English (from a helper, switch, enum), that's a bug.
| Variable source | Safe? |
|---|---|
| User data (names, emails, workspace names) | Yes |
| Numbers / formatted sizes | Yes |
Already-translated strings (from another t call) |
Yes |
| Hardcoded English from a helper/switch/enum | No |
toLowerCase() / string manipulation of English |
No |
SomeDefinition[key].name (English product names) |
No |
When removing an old translation hook, nearby non-i18n code got accidentally deleted too.
// These were accidentally removed alongside the hook:
appLanguage?: string // ← interface field (not i18n!)
appLanguage: stepInput.appLanguage // ← object property (not i18n!)This silently broke the onboarding flow — users who selected an app language had their choice dropped.
Prevention: After each commit, run the collateral damage verification script above. Scan output for removed lines that are NOT i18n-related. Legitimate removals: hook imports, hook calls, t('key') calls. Illegitimate: interface fields, object properties, function arguments, any line without a translation key or hook name.
If using a source branch that has fallen behind main, NEVER checkout "dual-changed" files (changed on both source and main) from the source branch. The source version carries stale non-i18n changes that silently regress main.
Real regressions found:
resetEditState(true, true)instead ofresetEditState({ preserveEditSessions: true })— wrong call signature- Removed
useIsOutputStreaminghook — composer no longer blocked during streaming - Removed feature gates and flows that were added after the source branch diverged
- ~15 files had icon imports swapped (
central-icons-outlined→lucide-react)
Prevention: For dual-changed files, start from main's version and apply ONLY the i18n migration surgically. Never bulk-checkout from a stale branch.
// BAD — SpreadElement crashes babel-plugin-extract-messages
i18n._({ ...descriptor, values: { name } })
// GOOD — pass values as second argument
i18n._(descriptor, { name })// BAD — tagged template treats {name} as a JS expression
const err = msg`Failed to load {name}`
// GOOD — object form treats {name} as an ICU placeholder
const err = msg({ message: 'Failed to load {name}' })Preserve curly apostrophes (' / \u2019) in t template literals — they're typographically correct. Tests using getTranslation() (i18next) won't match curly apostrophes rendered by Lingui t. Fix on the test side.
vi.mock('@lingui/core/macro') conflicts with the Lingui Vite plugin's resolveId hook. Instead, wrap test components in LinguiTestWrapper from i18n/lingui-test-setup.
Lingui uses hashed IDs (e.g., 'yTAXUz'), NOT message text for .id. Tests asserting on descriptor metadata should use .message (the source English text), not .id.
// This is missed by single-line grep for t({ id:
t({
id: 'some.key',
message: 'Some text'
})Always use multiline regex: rg -U 't\(\{[\s\S]*?id:' to catch all explicit-ID calls.
After each PR merges, the next PR's PO catalogs will conflict. The resolution is always:
# On the next PR branch, after previous PR merged:
git rebase origin/main
# If PO conflicts: accept either side, then:
yarn lingui extract
git add locales/
git rebase --continue- Files that build i18next keys dynamically (string concatenation for keys) — flag for manual review
dev-auth-inspector.tsx— internal tool, no i18n neededguest-legal-interstitial.tsx— uses<Trans>with dynamici18nKey; needs special handling- Any file where
TFunctiontype from i18next is passed as a function parameter — needs refactoring, not just macro replacement
- Lingui Macros —
t,msg,plural,Trans - Lingui React —
useLingui,I18nProvider - Lingui ESLint Plugin —
no-unlocalized-strings - Lingui Testing Guide — test setup patterns
- Main migration plan:
Code/Docs/lingui-migration-claude/lingui-migration-plan.md