Skip to content

Instantly share code, notes, and snippets.

@TosinAF
Created March 3, 2026 18:54
Show Gist options
  • Select an option

  • Save TosinAF/ebf1bd8d201ed5bbfc6682c53b986d8a to your computer and use it in GitHub Desktop.

Select an option

Save TosinAF/ebf1bd8d201ed5bbfc6682c53b986d8a to your computer and use it in GitHub Desktop.
i18n Completion PR Plan — structured plan for the final wave of localization PRs

i18n Completion PR Plan: Hardcoded Strings + i18next→Lingui Migration

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)


How to Use This Document

If you're an agent starting a PR:

  1. Find your PR in the PR Breakdown section
  2. Read the Scope to know which files/directories to touch
  3. Follow the Implementation Workflow step-by-step
  4. Refer to the Migration Pattern Reference for syntax
  5. Run all Verification checks before committing
  6. 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/msg macros
  • i18next migration — convert useXxxTranslation() + t('key') calls to Lingui macros, delete hooks when all consumers are migrated


Progress Since Last Update (Feb 24, 2026)

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 tmsg 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 Tracker

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

Completion Metrics

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-strings on tos/lingui-lint-enforcement branch

PR Breakdown (19 PRs)

Dependency Graph

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).


PR 1a: Settings — Workspace i18next→Lingui Migration ✅ DONE

Merged as: #19932 + #19933 | Reviewer: @harveyai/platform

Scope: Convert all useWorkspaceTranslation() + t('key') calls to Lingui macros.

  • src/components/settings/workspace/ — 32 consumer files using useWorkspaceTranslation
  • src/components/settings/configure-sso/ — 2 files using useWorkspaceTranslation
  • src/components/settings/user-management/ — 3 files using useWorkspaceTranslation

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.


PR 1c: Settings — Workspace Hardcoded Strings (~40 files)

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/** — SKIP
  • src/components/settings/experiment/** — SKIP
  • src/components/settings/vault-inspector/** — SKIP
  • src/components/settings/vault-testing/** — SKIP
  • src/components/settings/vault-review-jobs/** — SKIP
  • src/components/settings/document-debugger/** — SKIP
  • src/components/settings/file-upload-debugger/** — SKIP
  • src/components/settings/incident-management/** — SKIP
  • src/components/settings/pwc/** — SKIP
  • src/components/settings/models/** — SKIP

PR 1b: Settings — SCIM + Permissions ✅ DONE

Merged as: #19919 | Reviewer: @harveyai/platform

Scope:

  • src/components/settings/scim/ — 5 consumer files using useScimTranslation
  • src/components/settings/roles/ — 4 consumer files using usePermsTranslation

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.json
  • public/locales/en-US/perms.json

Hook deletion: DELETE useScimTranslation, usePermsTranslation, and useWorkspaceTranslation (if not deleted in PR 1a).


PR 2: Settings — Integrations, Groups, Export Formats

Size: M (~351 strings) | Reviewer: @harveyai/platform

Scope:

  • src/components/settings/integrations/ — files using useIntegrationsTranslation (if hook exists) or i18next patterns
  • src/components/settings/groups/ — 6 consumer files using useGroupsTranslation
  • src/components/settings/export-templates/ — 5 consumer files using useExportFormatsTranslation

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.json
  • public/locales/en-US/groups.json
  • public/locales/en-US/export_formats.json

Hook deletion: DELETE useGroupsTranslation, useExportFormatsTranslation.


PR 3: Settings — Remaining Customer-Facing

Size: M (~189 strings) | Reviewer: @harveyai/platform

Scope:

  • src/components/settings/profile/ — 3 files using useProfileTranslation
  • src/components/settings/external-connections/ — 2 files using useExternalConnectionsTranslation
  • src/components/settings/sharing/ — uses useSharingTranslation
  • src/components/settings/notifications/
  • src/components/settings/announcements/
  • src/components/settings/playbooks/ — note: some files may still use i18next
  • src/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.json
  • public/locales/en-US/external_connections.json
  • public/locales/en-US/collab_requests.json
  • public/locales/en-US/announcements.json
  • public/locales/en-US/settings.json (if exists)

Hook deletion: DELETE useProfileTranslation, useExternalConnectionsTranslation, useSettingsTranslation.


PR 4a: Assistant — Doc-Editor + Hooks ✅ DONE

Merged as: #19958 (combined with 4b) | Reviewer: @harveyai/assistant

Scope:

  • src/components/assistant/doc-editor/ — 4 files
  • src/components/assistant/hooks/ — hook files using useAssistantTranslation
  • src/components/assistant/features/ — non-workflow feature files
  • src/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.


PR 4b: Assistant — Workflows + Features ✅ DONE

Merged as: #19958 (combined with 4a) | Reviewer: @harveyai/assistant

Scope:

  • src/components/assistant/workflows/ — 25+ workflow component files
  • src/components/assistant/workflows/hooks/ — workflow hooks
  • src/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 (imports useTranslation directly)
  • src/components/assistant/workflows/hooks/use-feedback-submission.tsx (imports useTranslation directly)

PR 5a: Word Add-In — Taskpane Features ✅ DONE

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.


PR 5b: Word Add-In — Drafting + Remaining ✅ DONE

Merged across: #19671, #19873, #19941 | Reviewer: @harveyai/embedded-experience

Scope:

  • word-add-in/src/features/drafting/ — drafting-related components
  • word-add-in/src/features/ — all other feature directories (auth, settings, etc.)
  • word-add-in/src/components/ — shared Word add-in components
  • word-add-in/src/utils/ — utility files with user-facing strings
  • word-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.


PR 6a: Common — Sharing, Editor, Flows (~35 files)

Size: M (~155 strings) | Reviewer: @harveyai/frontend, @harveyai/platform

Scope:

  • src/components/common/sharing/ — 3 files using useSharingTranslation
  • src/components/common/editor/ — editor-related common components
  • src/components/common/flows/ — flow/step components
  • src/components/common/feedback/ — feedback components
  • src/components/common/dropzone/ — file upload dropzone
  • src/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

PR 6b: Common — Remaining Components (~35 files)

Size: M (~138 strings) | Reviewer: @harveyai/frontend, @harveyai/platform Depends on: PR 6a

Scope:

  • src/components/common/knowledge-sources/ — knowledge source components
  • src/components/common/integrations/ — integration components
  • src/components/common/product-tours/ — product tour components
  • src/components/common/export/ — export dialogs
  • All remaining src/components/common/ files not covered by PR 6a
  • Cross-area useCommonTranslation consumers: 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.


PR 7: Vault + Vault Review ✅ DONE

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.json
  • public/locales/en-US/vault-review.json

PR 8: Spaces/Shared + Sharing

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 using useSpacesTranslation
  • src/components/common/sharing/ — 3 files using useSharingTranslation
  • src/components/sidebar/ — 1 file using useAssistantTranslation, 1 using useSpacesTranslation
  • src/components/client-matters/pages/ClientViewPage/ — 1 file using useSpacesTranslation
  • src/components/common/create-space-dialog/ — 2 files using useSpacesTranslation
  • src/components/common/add-all-resources-dialog/ — 1 file using useSpacesTranslation

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.json
  • public/locales/en-US/sharing.json

Hook deletion: DELETE useSpacesTranslation, useSharingTranslation, and useAssistantTranslation (if not already deleted in PR 4).


PR 9: Utils + Types

Size: M (~168 strings) | Reviewer: @harveyai/frontend

Scope:

  • src/utils/ — toast messages, error messages, date utils, task definitions
  • src/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 toasts
  • src/utils/task-definitions.ts — task type labels (~40 instances)
  • src/utils/docx.ts — document export headings/text
  • src/utils/markdown.ts — export section headings
  • src/api/index.ts — file processing error messages
  • src/services/files/file-processing-poller.ts — timeout/error messages
  • src/hooks/use-voice-input.ts — voice recording messages
  • src/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.


PR 10: Workflow Builder

Size: M (~165 strings) | Reviewer: @harveyai/workflows

Scope:

  • src/components/workflow-builder/ — 2 files using useCommonTranslation (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

PR 11: Other (Office, Outlook, Sidebar, Filter, etc.)

Size: M (~224 strings) | Reviewer: @harveyai/embedded-experience, @harveyai/platform

Scope:

  • src/office/ — Office shared features
  • outlook-add-in/src/ — Outlook add-in UI
  • src/components/sidebar/ — 2 files using hooks
  • src/components/filter/ — 1 file using useCommonTranslation, useSpacesTranslation, useSharingTranslation
  • src/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.


PR 12: Workflows

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.


PR 13: Library + Playbooks ✅ DONE

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.json
  • public/locales/en-US/playbooks.json

PR 14: UI Primitives ✅ DONE

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).


Implementation Workflow (Per PR)

Step 1: Identify all files to migrate

# 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"

Step 2: For each file, apply both migrations

Do not partially migrate a file. Every file you touch must be fully converted.

For each file:

  1. Remove old hook import and call:

    // DELETE:
    import { useXxxTranslation } from 'components/.../hooks/use-xxx-translation'
    const { t } = useXxxTranslation()
  2. 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'
  3. 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, not undefined. Add ?? '' fallback.
  4. 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
  5. Convert plurals:

    import { plural } from '@lingui/core/macro'
    plural(count, { one: '# file', other: '# files' })
  6. 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>
  7. 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>

Step 3: Update test files

  1. Remove translation hook mocks (vi.mock('components/.../use-xxx-translation', ...))
  2. Update assertions: expect(screen.getByText('translation.key'))expect(screen.getByText('English text'))
  3. Remove unused translation imports

Step 4: Delete hook files (if zero consumers remain)

rg "useXxxTranslation" src/ --files-with-matches
# If zero results, delete the hook definition file

Step 5: Extract and verify

yarn lingui extract        # Update PO catalogs
yarn typecheck             # Zero type errors
yarn lint                  # Zero lint errors
yarn test                  # Tests pass

Migration Pattern Reference

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' })

Critical Rules

  1. Module-scope strings MUST use msg, NOT t. t evaluates once at import time and produces stale translations on locale switch. msg creates a deferred descriptor resolved at render time.

  2. @lingui imports go in the third-party section (alongside React, lodash, etc.), NOT where the old hook import was.

  3. Variables in tagged templates must be string | number, not undefined. Add ?? '' fallback.

  4. Plural branches use # for the count, never ${count}.

  5. plural() is standalone — do NOT nest inside t ``.

  6. Do NOT interpolate hardcoded English words into t templates. Enumerate full sentences per type instead.

  7. Look up the EXACT English text from the JSON namespace file. Do not guess or abbreviate.

  8. Do NOT use t in React dependency arrays — it's a macro, not a hook.

  9. Run lingui extract before committing.

  10. Skip internal admin paths — anything gated behind IsInternalAdminReader does not need localization.


Internal Admin Paths to Skip

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/**

PR Description Template

## 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)

Verification Checklist (Per PR)

  • Zero imports of old hooks (useXxxTranslation) remain in target area
  • Zero react-i18next imports remain in target area
  • No module-scope t calls — all module-scope strings use msg
  • No ${count} in plural branches (use #)
  • No undefined variables in tagged templates
  • No hardcoded English interpolated into t templates
  • yarn lingui extract runs without errors
  • Test files updated: no translation mocks, assertions use English text
  • @lingui imports are in the third-party section
  • Internal admin paths not touched

Summary Table

# 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).


Post-Commit Review Agent

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.

When to Run

  • After every commit within a migration PR (not just at the end)
  • Especially after batch-converting files where it's easy to miss one

Review Agent Prompt Template

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.

Collateral Damage Verification Script

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.

Lessons Learned (From Prior Migration PRs)

These are real bugs found in production migration PRs. Every one of them will recur if you don't check for them.

1. Module-Scope t Freezes Locale (Found in PR 2)

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.

2. Hardcoded English Interpolated into t (Found in PR 2)

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

3. Collateral Deletion of Non-i18n Code (Found in PR 2)

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.

4. Stale Source Branch Collateral Damage (Found in PR 6)

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 of resetEditState({ preserveEditSessions: true }) — wrong call signature
  • Removed useIsOutputStreaming hook — 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-outlinedlucide-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.

5. i18n._() Spread Crashes Extractor (Found in PR 2a)

// BAD — SpreadElement crashes babel-plugin-extract-messages
i18n._({ ...descriptor, values: { name } })

// GOOD — pass values as second argument
i18n._(descriptor, { name })

6. msg() with ICU Placeholders Must Use Object Form (Found in PR 2a)

// 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}' })

7. Curly Apostrophes in English Text (Found in PR 2)

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.

8. Never Mock @lingui/core/macro (Found in PR 2)

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.

9. Message Descriptor .id vs .message (Found in PR 2)

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.

10. Multi-line t({ id: }) Missed by Single-line Grep (Found in PR 2a)

// 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.


PO Catalog Conflict Resolution

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

Edge Cases to Defer (Do NOT Try to Fix)

  • Files that build i18next keys dynamically (string concatenation for keys) — flag for manual review
  • dev-auth-inspector.tsx — internal tool, no i18n needed
  • guest-legal-interstitial.tsx — uses <Trans> with dynamic i18nKey; needs special handling
  • Any file where TFunction type from i18next is passed as a function parameter — needs refactoring, not just macro replacement

Key External References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment