Skip to content

Instantly share code, notes, and snippets.

@TosinAF
Created April 1, 2026 20:23
Show Gist options
  • Select an option

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

Select an option

Save TosinAF/5995e9094f78120ea6371a3a82526174 to your computer and use it in GitHub Desktop.
i18n Call-Site Migration Plan — Agent-Ready PR-by-PR Guide

i18n Call-Site Migration Plan — Agent-Ready PR-by-PR Guide

Date: 2026-03-30 Status: Ready for execution Prerequisite PRs (all merged): #39975 (infra), #40117 (PO fill), #40174 (fixups)


How to Use This Plan

Each PR section below is self-contained. Hand an agent a single PR section and it has everything it needs:

  1. The exact file list to migrate
  2. The transformation pattern
  3. Which JSON locale files to look up English strings in
  4. Branch/PR creation commands
  5. Verification checklist

Execution order matters — PRs must be created and merged in sequence (PR 2 first, then PR 3 based on PR 2's branch, etc.) because each PR adds msgids to the POT file incrementally.


Global Transformation Rules

Every file follows the same mechanical pattern. There are no exceptions unless explicitly noted.

Import Replacement

# OLD (one of these patterns)
from utils.i18n import translate_msg_for_user
from utils.i18n import translate_msg_for_user, translate_msg_pre_auth

# NEW — import what you need:
from utils.i18n import get_user_locale, t              # most files (user-context)
from utils.i18n import t, t_mark                      # files with extractable constants
from utils.i18n import t, get_user_locale             # files where user can be None
from utils.i18n import t, get_request_locale          # pre-auth files (no user available)
from utils.i18n import t, get_user_locale, t_mark     # combination as needed

Call Replacement: translate_msg_for_user

locale is always required. Choose the right accessor based on what you have:

# CASE 1: auth_user already in scope (most common)
# OLD
translate_msg_for_user(auth_user, 'some_key', ns='errors')
translate_msg_for_user(auth_user, 'some_key', ns='errors', var_name=var_value)
# NEW — use get_user_locale() for None-safety and consistency
t('English string here', locale=get_user_locale(auth_user))
t('English string with {var_name}', locale=get_user_locale(auth_user), var_name=var_value)

# CASE 2: user might be None (Optional[AuthUser])
# OLD
translate_msg_for_user(user, 'some_key', ns='errors')  # user: AuthUser | None
# NEW — use get_user_locale() which defaults to en-US when None
t('English string here', locale=get_user_locale(user))

# CASE 3: user accessed via request context (no auth_user variable in scope)
# OLD
translate_msg_for_user(get_request_ctx().current_user, 'some_key', ns='errors')
# NEW — inline get_user_locale() with the accessor
t('English string here', locale=get_user_locale(ctx.current_user))
# If ctx isn't assigned yet, use the full accessor:
t('English string here', locale=get_user_locale(get_request_ctx().current_user))

Call Replacement: translate_msg_pre_auth

Used in pre-authentication contexts where there's no user. Use get_request_locale() which parses the Accept-Language header automatically:

# OLD
translate_msg_pre_auth('some_key', accept_language=request.headers.get('Accept-Language'), ns='errors')

# NEW — get_request_locale() reads Accept-Language from the Flask request, falls back to en-US
t('English string here', locale=get_request_locale())

CASE 4: Defensive request_ctx — request context or user might be None

Some files (mainly routes/word_add_in.py, drafting/suggest.py) defensively check whether get_request_ctx() returns None — typically inside error/exception handlers where request context might not be fully established.

# OLD — defensive pattern (request_ctx might be None)
request_ctx = get_request_ctx()
auth_user = request_ctx.current_user if request_ctx else None
error_msg = translate_msg_for_user(auth_user, 'errors.unable_to_parse_request', ns='word')

# NEW — use get_request_locale() since if there's no user, Accept-Language is the best signal
# (better than get_user_locale(None) which just hardcodes en-US)
error_msg = t('Unable to parse request', locale=get_request_locale())

# The 2 lines resolving request_ctx -> auth_user can often be removed entirely
# if auth_user was ONLY used for locale resolution. If auth_user is used for
# other purposes in the same scope, keep it but don't use it for locale.

Why get_request_locale() over get_user_locale(auth_user)?

  • get_user_locale(None) returns hardcoded en-US — ignores the client's language
  • get_request_locale() reads Accept-Language from the Flask request, falling back to en-US
  • In defensive contexts where user might be None, the request header is a better signal

Callsite Brevity Rules

Use whatever user variable is already in scope. Do NOT introduce throwaway variables just to shorten a single call.

# GOOD — auth_user already exists in this scope
auth_user = get_request_ctx().current_user  # already there for other reasons
t('msg', locale=get_user_locale(auth_user))

# GOOD — only ctx is in scope, inline it
ctx = get_request_ctx()
t('msg', locale=get_user_locale(ctx.current_user))

# BAD — don't add a variable just for one t() call
auth_user = ctx.current_user  # ← pointless if only used on the next line
t('msg', locale=get_user_locale(auth_user))

Multiple t() calls in the same function: If a handler has 3+ t() calls and no existing auth_user variable, it's fine to add auth_user = ctx.current_user once at the top of the function (not right above a single call). Use judgement — the goal is short call sites without throwaway one-off variables.

Locale Resolution Summary

Scenario locale= argument Import needed
Have AuthUser (non-None or maybe-None) get_user_locale(auth_user) t, get_user_locale
User might be None get_user_locale(user) t, get_user_locale
Pre-auth (no user) get_request_locale() t, get_request_locale
Defensive request_ctx might be None get_request_locale() t, get_request_locale
Background job with user_id get_locale_for_user_id(session, user_id) t, get_locale_for_user_id

How to Find the English String

Each translate_msg_for_user(user, 'some_key', ns='namespace') maps to a value in locales/en-US/<namespace>.json.

  • translate_msg_for_user(user, 'invalid_request', ns='errors') -> look up "invalid_request" in locales/en-US/errors.json
  • translate_msg_for_user(user, 'folder_not_found', ns='vault') -> look up "folder_not_found" in locales/en-US/vault.json

JSON interpolation {{variable}} becomes {variable} in t():

{"invalid_format": "Invalid format: {{resource_id}}"}

becomes:

t('Invalid format: {resource_id}', locale=get_user_locale(auth_user), resource_id=req.resource_id)

Available JSON Locale Files

locales/en-US/assistant.json
locales/en-US/auth.json
locales/en-US/collab_requests.json
locales/en-US/common.json
locales/en-US/custom_projects.json
locales/en-US/doc_qa.json
locales/en-US/errors.json
locales/en-US/export.json
locales/en-US/external_connections.json
locales/en-US/extraction.json
locales/en-US/groups.json
locales/en-US/integration.json
locales/en-US/internal_admin.json
locales/en-US/knowledge_sources.json
locales/en-US/library.json
locales/en-US/notifications.json
locales/en-US/outlook.json
locales/en-US/playbooks.json
locales/en-US/practice_areas.json
locales/en-US/resource_sharing.json
locales/en-US/scim.json
locales/en-US/settings.json
locales/en-US/spaces.json
locales/en-US/user_features.json
locales/en-US/vault.json
locales/en-US/word.json
locales/en-US/workflow.json
locales/en-US/workspace_features.json

Critical Safety Rules (ALL PRs except PR 9)

  1. Do NOT run make i18n-update — it will wipe pre-filled PO translations
  2. DO run make i18n-extract after source changes (updates POT only)
  3. PO files must have ZERO diff — only locales/messages.pot should change
  4. Do NOT remove or modify JSON locale files — they stay until full migration complete
  5. Do NOT modify utils/i18n.py (except PR 3 which includes it in the file list)

Tests

When migrating a source file, also check for and update its corresponding test file. Tests may mock translate_msg_for_user — update to mock t instead. Assertions may check for synthetic keys — change to check for English strings.

Test files live at tests/unit/<module>/ mirroring the source structure.

Lessons from Part 1 Reviews — Watch Out For These

These issues came up during PR #40888 (routes migration) and apply to all subsequent PRs.

1. Test mocks WILL break — search proactively

Removing translate_msg_for_user from a module's imports causes any test that patches <module>.translate_msg_for_user to fail with AttributeError. This broke CI in Part 1.

Action for every PR: After migrating a source file, run:

rg 'translate_msg_for_user|translate_msg_pre_auth' tests/unit/<module>/ tests/integration/<module>/

Update every mock/patch found:

  • @patch('<module>.translate_msg_for_user')@patch('<module>.t')
  • Assertions checking ns='scim' or JSON keys like 'some_key' → check English source string as first positional arg
  • Assertions checking call_args[0][1] (old: 2nd positional = key) → call_args[0][0] (new: 1st positional = English string)
  • Variable names mock_translatemock_t (cosmetic but keeps things consistent)

2. t_mark() required for extractable constants

Any dict, list, or module-level variable that holds English strings destined for t() at runtime must wrap values in t_mark(). Without it, pybabel extract won't see the strings and they'll be missing from the POT file.

Patterns to watch for:

# BAD — invisible to PO extractor
error_map = {SomeError: 'Error message here'}

# GOOD — t_mark marks for extraction without translating
error_map = {SomeError: t_mark('Error message here')}

After migrating, search each file for dicts/constants whose values feed into t() downstream.

3. Wrapper functions hide strings from PO extractor

Custom wrappers like _translate_error_for_user(msg) that internally call t() will not be seen by pybabel extract since only t, t_mark, t_context, t_plural are in babel's KEYWORDS list.

Action: Inline all wrapper calls to direct t() calls. In Part 1 this affected routes/workflow_builder_routes.py (68 calls, 25 unique strings).

Search pattern:

rg 'def _.*translat' <files>  # find translation wrapper functions

4. Merge conflicts will add new translate_msg_for_user calls

When rebasing/merging main, new code from other teams may introduce translate_msg_for_user calls in files where we've already removed the import. This causes F821 Undefined name lint errors.

Action: After any merge/rebase, run:

rg 'translate_msg_for_user|translate_msg_pre_auth' <migrated-files>

Migrate any new calls that snuck in from main.

5. Dynamic-key calls must stay as translate_msg_for_user

Some call sites use runtime-computed keys (from exception objects, model attributes, etc.) that can't be statically extracted. These must remain as translate_msg_for_user until the source module providing the key is itself migrated.

How to identify: The second argument to translate_msg_for_user is a variable, not a string literal:

# DYNAMIC — leave as-is
translate_msg_for_user(auth_user, error.i18n_key, ns=error.i18n_ns)
translate_msg_for_user(auth_user, deprecation_message_key, ns='assistant')

Files with dual imports (t + translate_msg_for_user) are expected for this reason.

6. Design decision: locale=get_user_locale(auth_user) as standard pattern

PR review feedback (David Ma): Suggested ergonomic improvements to avoid repeating auth_user.app_language at every call site. Considered user= param on t(), but rejected due to coupling and pre-auth/background-job edge cases.

Decision (agreed with David): Use get_user_locale(auth_user) as the standard locale resolver everywhere.

# STANDARD PATTERN — use this for all call sites with a user
t('Request body is required', locale=get_user_locale(auth_user))

# Pre-auth (no user available)
t('Request body is required', locale=get_request_locale())

# Background jobs (only have user_id)
t('Request body is required', locale=get_locale_for_user_id(session, user_id))

Why get_user_locale(auth_user) over auth_user.app_language:

  • None-safe: get_user_locale(None) returns en-US instead of AttributeError
  • Consistent: Same wrapper used whether user is guaranteed or optional
  • Uniform call sites: Every t() call uses a resolver function, never raw attribute access

For agents migrating all PRs: Always use get_user_locale(auth_user), never auth_user.app_language directly. This means every file with a user-context t() call needs from utils.i18n import get_user_locale.

7. Linter enforcement must be updated before or alongside migration

PR review feedback (David Ma): The existing i18n linter rules may flag the new t() pattern or fail to catch regressions. Verify that:

  • Any custom lint rules that enforce translate_msg_for_user usage are updated to recognize t() / t_mark() / get_user_locale() / get_request_locale()
  • The linter doesn't block PRs that remove translate_msg_for_user imports
  • New lint rules catch bare auth_user.app_language without t() (if applicable)

Action: Check lint/ directory for i18n-related rules before each PR. If linter changes are needed, include them in the same PR or a preceding prep PR.


PR 2: Routes (Top-Level)

Status: Branch exists as tosinaf/i18n-codemod-2-routes, PR #40179 (DRAFT) Action: This PR already has the migrations done. It needs to be rebased onto main (since infra/fill/fixups merged) and force-pushed. Then verify it passes CI.

Base branch: main Branch: tosinaf/i18n-po-callsites-migration-part1

Files (48 source + 1 POT = 49)

routes/action_controls.py
routes/assistant.py
routes/client_admin.py
routes/client_admin_scim.py
routes/clients.py
routes/collab_requests.py
routes/company_profile.py
routes/comparison.py
routes/contracts.py
routes/dashboard.py
routes/diligence.py
routes/event.py
routes/external_connections.py
routes/file.py
routes/general_doc_processing_endpoints.py
routes/general_infra_endpoints.py
routes/general_platform_endpoints.py
routes/groups.py
routes/helpers.py
routes/history.py
routes/integration.py
routes/internal_admin.py
routes/knowledge_sources.py
routes/library.py
routes/library_v2_routes.py
routes/ogc_review.py
routes/outlook_add_in.py
routes/playbook.py
routes/research.py
routes/resource_cloning.py
routes/resource_sharing_routes.py
routes/settings.py
routes/sharing.py
routes/spaces.py
routes/storage_probe.py
routes/transcribe.py
routes/transcripts.py
routes/user_favorites_routes.py
routes/user_features.py
routes/user_profiles.py
routes/vault.py
routes/word_add_in.py
routes/workflow_builder_routes.py
routes/workflows.py
routes/workspace_features.py
routes/workspaces.py
routes/writing_styles.py
locales/messages.pot

JSON namespaces used

errors, word, playbooks, vault, integration, groups, spaces, resource_sharing, settings, external_connections, library, scim, common, user_features, outlook, collab_requests, workspace_features, knowledge_sources

Special cases

  • routes/client_admin.py and routes/client_admin_scim.py need t_mark for exception raises
  • routes/assistant.py has deprecation_message_key field — see PR 2 plan doc for details
  • routes/history.py has significant refactoring of helper functions

Verification

make i18n-extract && make i18n-compile && make i18n-check && make lint-f
rg 'translate_msg_for_user|translate_msg_pre_auth' routes/*.py  # should return NOTHING
git diff -- 'locales/*/LC_MESSAGES/messages.po'                  # should be empty
uv run pytest tests/unit/routes/ -x --timeout=60

Reference

See be-po-strings/i18n-codemod-pr2-routes.md for the full detailed plan.


PR 3: Root-Level Modules

Base branch: tosinaf/i18n-po-callsites-migration-part1 New branch: tosinaf/i18n-po-callsites-migration-part2

Files (43 source + 1 POT = 44)

api/rpc_utils.py
api/utils.py
audio/audio_stream_handler.py
collaboration/errors.py
container.py
contract_review/playbook.py
contracts/extraction.py
contracts/filter.py
doc_qa/eu_lite/multi_doc.py
doc_qa/eu_lite/single_doc.py
doc_utils/pdfkit_document.py
document_comparison/comparison.py
document_service/service.py
drafting/suggest.py
drafting/word_contextual_prompts_service.py
ethical_walls/_implementation/ethical_wall_service.py
extraction/ask.py
hosted_mcp/tools.py
intercept/single_class.py
job_queue/jobs/diligence_async_report_job.py
legacy_drafting/draft.py
main.py
model_gateway/app.py
open_ended/ask.py
redlines_qa/issues_list.py
redlines_qa/qa.py
redlines_qa/streaming_handler.py
resource_cloning/knowledge_base_cloner.py
resource_cloning/playbook_cloner.py
resource_cloning/workflow_cloner.py
rich_prompt/completion.py
sec_edgar/answer.py
sec_edgar/company_profile/report.py
sec_edgar/edgar_retrieval.py
sec_edgar/query_decomp.py
services/infra_manager/secret_runner/secret_runner_app.py
transcripts/ask.py
utils/i18n.py
utils/socket_utils.py
utils/transcription.py
utils/validation.py
utils/word_add_in.py
web_browsing/scraping.py
locales/messages.pot

JSON namespaces used

errors, common, assistant, auth, doc_qa, extraction, word, vault, playbooks, integration, knowledge_sources, custom_projects

Special cases

  • utils/i18n.py — this file DEFINES translate_msg_for_user / translate_msg_pre_auth. It still has internal usages that need migrating. Do NOT remove the function definitions themselves (they are kept as shims until Phase 4). Only migrate internal usages.
  • collaboration/errors.py — may use t_mark for exception messages
  • hosted_mcp/tools.py — has 19 translate calls, significant volume
  • audio/audio_stream_handler.py — has 19 translate calls, significant volume
  • model_gateway/app.py — has 13 translate calls
  • ethical_walls/_implementation/ethical_wall_service.py — has 14 translate calls

Verification

make i18n-extract && make i18n-compile && make i18n-check && make lint-f
rg 'translate_msg_for_user|translate_msg_pre_auth' api/ audio/ collaboration/ container.py contract_review/ contracts/ doc_qa/ doc_utils/ document_comparison/ document_service/ drafting/ ethical_walls/ extraction/ hosted_mcp/ intercept/ job_queue/ legacy_drafting/ main.py model_gateway/ open_ended/ redlines_qa/ resource_cloning/ rich_prompt/ sec_edgar/ services/infra_manager/ transcripts/ utils/socket_utils.py utils/transcription.py utils/validation.py utils/word_add_in.py web_browsing/
git diff -- 'locales/*/LC_MESSAGES/messages.po'  # should be empty
uv run pytest tests/unit/utils/test_i18n.py tests/unit/hosted_mcp/ tests/unit/ethical_walls/ -x --timeout=60

PR creation

git checkout tosinaf/i18n-po-callsites-migration-part1 && git pull
git checkout -b tosinaf/i18n-po-callsites-migration-part2
# ... make changes ...
gh pr create --draft --base tosinaf/i18n-po-callsites-migration-part1 \
  --title "refactor(i18n): migrate root modules to PO source-as-ID" \
  --body "## Summary
- Migrate ~43 root-level source files from translate_msg_for_user()/translate_msg_pre_auth() to t()/t_mark()
- Covers: api/, audio/, contracts/, doc_qa/, drafting/, ethical_walls/, extraction/, hosted_mcp/, resource_cloning/, sec_edgar/, etc.
- Part of i18n PO migration chain: #39975 -> #40117 -> #40174 -> #40179 -> **this PR**

## Translation safety
- PO files are NOT modified (no make i18n-update)
- Pre-filled translations preserved
- Only POT file changes

## Test plan
- [ ] make i18n-check passes
- [ ] make lint-f passes
- [ ] Unit tests pass for migrated modules
- [ ] PO files have zero diff

Generated with [Claude Code](https://claude.com/claude-code)"

PR 4: Workflow Blocks

Base branch: tosinaf/i18n-po-callsites-migration-part2 New branch: tosinaf/i18n-po-callsites-migration-part3

Files (40 source + 1 POT = 41)

assistant/workflows/blocks/answer_bulk.py
assistant/workflows/blocks/antitrust_filings.py
assistant/workflows/blocks/antitrust_simple_file_names.py
assistant/workflows/blocks/audio_transcript_file_extraction.py
assistant/workflows/blocks/audio_transcription.py
assistant/workflows/blocks/audio_transcription_file_type_branch.py
assistant/workflows/blocks/classification.py
assistant/workflows/blocks/coding_agent.py
assistant/workflows/blocks/conditional/conditional_value.py
assistant/workflows/blocks/deep_research.py
assistant/workflows/blocks/diligence_request_list.py
assistant/workflows/blocks/document_drafting.py
assistant/workflows/blocks/docx_apply_edits.py
assistant/workflows/blocks/draft_from_template_edits.py
assistant/workflows/blocks/draft_from_template_plan.py
assistant/workflows/blocks/edgar.py
assistant/workflows/blocks/extract_questions.py
assistant/workflows/blocks/extraction.py
assistant/workflows/blocks/file_metadata.py
assistant/workflows/blocks/fill_a_template.py
assistant/workflows/blocks/lance_upload.py
assistant/workflows/blocks/lexis_motion_to_dismiss.py
assistant/workflows/blocks/lexis_mtd_agents.py
assistant/workflows/blocks/loan_review.py
assistant/workflows/blocks/mixins.py
assistant/workflows/blocks/nudge_improved_questions.py
assistant/workflows/blocks/playbooks/playbook_rule_extraction.py
assistant/workflows/blocks/playbooks/playbook_rule_parser.py
assistant/workflows/blocks/playbooks/playbook_upload.py
assistant/workflows/blocks/playbooks/playbook_workflow_utils.py
assistant/workflows/blocks/proofread.py
assistant/workflows/blocks/redlines.py
assistant/workflows/blocks/report_gen.py
assistant/workflows/blocks/review_table_generation.py
assistant/workflows/blocks/rich_prompt.py
assistant/workflows/blocks/transcript_summarization.py
assistant/workflows/blocks/transcripts_litigation.py
assistant/workflows/blocks/translation.py
assistant/workflows/blocks/user_prompt.py
assistant/workflows/blocks/word_add_in/word_add_in_chat.py
locales/messages.pot

JSON namespaces used

errors, common, workflow, assistant, vault, playbooks, word, extraction

Special cases

  • blocks/document_drafting.py — may have changed since plan was written (rebase overlap)
  • blocks/loan_review.py — rebase overlap
  • blocks/review_table_generation.py — rebase overlap
  • blocks/word_add_in/word_add_in_chat.py — rebase overlap
  • Some blocks use t_mark() for loading state strings that are displayed later — check each file

Verification

make i18n-extract && make i18n-compile && make i18n-check && make lint-f
rg 'translate_msg_for_user|translate_msg_pre_auth' assistant/workflows/blocks/
git diff -- 'locales/*/LC_MESSAGES/messages.po'  # should be empty
uv run pytest tests/unit/assistant/workflows/blocks/ -x --timeout=60

PR creation

git checkout tosinaf/i18n-po-callsites-migration-part2 && git pull
git checkout -b tosinaf/i18n-po-callsites-migration-part3
# ... make changes ...
gh pr create --draft --base tosinaf/i18n-po-callsites-migration-part2 \
  --title "refactor(i18n): migrate workflow blocks to PO source-as-ID" \
  --body "## Summary
- Migrate 40 workflow block files from translate_msg_for_user() to t()/t_mark()
- All files under assistant/workflows/blocks/
- Part of i18n PO migration chain

## Translation safety
- PO files are NOT modified
- Only POT file changes

## Test plan
- [ ] make i18n-check passes
- [ ] make lint-f passes
- [ ] Workflow block tests pass
- [ ] PO files have zero diff

Generated with [Claude Code](https://claude.com/claude-code)"

PR 5: DB Services + Retrieval

Base branch: tosinaf/i18n-po-callsites-migration-part3 New branch: tosinaf/i18n-po-callsites-migration-part4

Files (46 source + 1 POT = 47)

DB services (23)

db/services/admin_service/authz.py
db/services/admin_service/customization.py
db/services/admin_service/pwc.py
db/services/admin_service/style_template.py
db/services/collab_requests_service.py
db/services/dms_integrations_service.py
db/services/dms_one_way_sync_orchestration_service.py
db/services/external_connections_service.py
db/services/file_upload_service.py
db/services/file_upload_service_utils.py
db/services/generic_resource_sharing_constants.py
db/services/generic_resource_sharing_service.py
db/services/groups/groups_service.py
db/services/playbooks_service.py
db/services/spaces_service.py
db/services/user_workspace_service.py
db/services/vault_retention_notification_service.py
db/services/workflow_auth_decorators.py
db/services/workflow_builder/workflow_builder_read_only_service.py
db/services/workflow_builder/workflow_builder_write_service.py
db/services/workspace_feature_service.py
db/storage_managers/workflow_builder_definition_metadata_storage_manager.py
db/storage_managers/workflow_builder_definition_version_storage_manager.py

Retrieval (23)

retrieval/knowledge_source_info.py
retrieval/knowledge_sources/australia_breach_reporting_knowledge_source.py
retrieval/knowledge_sources/base_library_knowledge_source.py
retrieval/knowledge_sources/caselaw_knowledge_source.py
retrieval/knowledge_sources/cuatrecasas_knowledge_source.py
retrieval/knowledge_sources/document_knowledge_source.py
retrieval/knowledge_sources/edgar_knowledge_source.py
retrieval/knowledge_sources/eurlex_knowledge_source.py
retrieval/knowledge_sources/firm_knowledge_source.py
retrieval/knowledge_sources/from_counsel_knowledge_source.py
retrieval/knowledge_sources/harvey_guide_knowledge_source.py
retrieval/knowledge_sources/lefebvre_knowledge_source.py
retrieval/knowledge_sources/lexis_protege_knowledge_source.py
retrieval/knowledge_sources/library_knowledge_source_config.py
retrieval/knowledge_sources/memo_knowledge_source.py
retrieval/knowledge_sources/playbook_knowledge_source.py
retrieval/knowledge_sources/rettsdata_knowledge_source.py
retrieval/knowledge_sources/review_table_knowledge_source.py
retrieval/knowledge_sources/scc_online_knowledge_source.py
retrieval/knowledge_sources/scotus_knowledge_source.py
retrieval/knowledge_sources/tax_knowledge_source.py
retrieval/knowledge_sources/thinking_states/utils.py
retrieval/knowledge_sources/web_knowledge_source.py

JSON namespaces used

errors, groups, spaces, vault, settings, integration, external_connections, resource_sharing, knowledge_sources, common, playbooks, workspace_features, collab_requests, scim, library, word, user_features, workflow

Special cases

  • db/services/admin_service/authz.py — 64 translate calls, biggest file in this batch
  • db/services/file_upload_service.py — 37 translate calls
  • db/services/generic_resource_sharing_service.py — 34 translate calls, rebase overlap
  • db/services/spaces_service.py — rebase overlap
  • retrieval/knowledge_source_info.py — rebase overlap
  • lexis_protege_knowledge_source.py — 11 translate calls
  • Knowledge source files often have t_mark() for display names/descriptions that need to be extractable

Verification

make i18n-extract && make i18n-compile && make i18n-check && make lint-f
rg 'translate_msg_for_user|translate_msg_pre_auth' db/services/ db/storage_managers/ retrieval/knowledge_sources/ retrieval/knowledge_source_info.py
git diff -- 'locales/*/LC_MESSAGES/messages.po'  # should be empty
uv run pytest tests/unit/db/services/ tests/unit/retrieval/knowledge_sources/ -x --timeout=60

PR creation

git checkout tosinaf/i18n-po-callsites-migration-part3 && git pull
git checkout -b tosinaf/i18n-po-callsites-migration-part4
# ... make changes ...
gh pr create --draft --base tosinaf/i18n-po-callsites-migration-part3 \
  --title "refactor(i18n): migrate db services + retrieval to PO source-as-ID" \
  --body "## Summary
- Migrate 46 files: 23 db services/storage managers + 23 retrieval knowledge sources
- Part of i18n PO migration chain

## Translation safety
- PO files are NOT modified
- Only POT file changes

## Test plan
- [ ] make i18n-check passes
- [ ] make lint-f passes
- [ ] DB service and retrieval tests pass
- [ ] PO files have zero diff

Generated with [Claude Code](https://claude.com/claude-code)"

PR 6: Assistant Core + Custom Projects

Base branch: tosinaf/i18n-po-callsites-migration-part4 New branch: tosinaf/i18n-po-callsites-migration-part5

Files (49 source + 1 POT = 50)

Assistant core (34)

assistant/agent/agent.py
assistant/agent/agent_sdk/consumers/assistant_message.py
assistant/agent/agent_sdk/consumers/assistant_reasoning.py
assistant/agent/agent_sdk/consumers/citation_consumer.py
assistant/agent/agent_sdk/model_provider.py
assistant/agent/agent_sdk/tools/convert_to_playbook_tool.py
assistant/agent/agent_sdk/tools/docx_drafting.py
assistant/agent/agent_sdk/tools/file_task_tool.py
assistant/agent/agent_sdk/tools/multi_doc_subagent_editing/tool.py
assistant/agent/agent_sdk/tools/playbook_reconciliation.py
assistant/agent/agent_sdk/tools/playbook_rule_review.py
assistant/agent/agent_sdk/tools/review_rule_tool.py
assistant/agent/agent_sdk/tools/tools.py
assistant/agent/agent_sdk/tools/web_search_utils.py
assistant/agent/runners/assistant.py
assistant/agent/runners/deep_research.py
assistant/agent/runners/docx_drafting.py
assistant/agent/runners/playbook_orchestrator.py
assistant/agent/runners/playbook_rule_review.py
assistant/agent/runners/post_hook_util.py
assistant/agent/tools/utils.py
assistant/agent_reconnect_stream_handler.py
assistant/agent_request_handler.py
assistant/agent_stream_handler.py
assistant/agent_sync_client.py
assistant/ask.py
assistant/assistant_db.py
assistant/base_stream_handler.py
assistant/chat_stream_handler.py
assistant/draft_stream_handler.py
assistant/magic_prompt.py
assistant/open_ended.py
assistant/perms.py
assistant/request_router/import_draft_route.py

Custom projects (15)

custom_projects/antitrust_filings/ask.py
custom_projects/blackstone_comments_memo/compare_comment_memos.py
custom_projects/blackstone_comments_memo/document_allocation.py
custom_projects/blackstone_comments_memo/workflow_block_blackstone_comments_memo.py
custom_projects/chiomenti/workflow_block_chiomenti_placeholder_extract.py
custom_projects/chiomenti/workflow_block_chiomenti_template_preload.py
custom_projects/deal_screen_memo/workflow_block_draft_deal_screen_memo.py
custom_projects/icertis/extract.py
custom_projects/icertis/review.py
custom_projects/s1_risk_factors/workflow_block_s1_company_info.py
custom_projects/s1_risk_factors/workflow_block_s1_precedent_finder.py
custom_projects/s1_risk_factors/workflow_block_s1_risk_factor_draft.py
custom_projects/s1_risk_factors/workflow_block_s1_risk_factor_matrix.py
custom_projects/s1_risk_factors/workflow_block_s1_risk_factor_redline.py
custom_projects/s1_risk_factors/workflow_block_s1_template_apply.py

JSON namespaces used

errors, assistant, common, word, playbooks, vault, custom_projects, extraction

Special cases

  • assistant/agent/agent_sdk/tools/docx_drafting.py — 22 translate calls
  • assistant/agent/agent_sdk/tools/convert_to_playbook_tool.py — 10 translate calls
  • assistant/agent/agent_sdk/tools/playbook_rule_review.py — 11 translate calls
  • s1_risk_factors/workflow_block_s1_risk_factor_matrix.py — 7 translate calls
  • Files under assistant/agent/agent_sdk/consumers/ — new since original plan, verify they still have translate calls at migration time

Verification

make i18n-extract && make i18n-compile && make i18n-check && make lint-f
rg 'translate_msg_for_user|translate_msg_pre_auth' assistant/ --glob '!assistant/workflows/blocks/*' --glob '!assistant/workflows/*.py' custom_projects/
git diff -- 'locales/*/LC_MESSAGES/messages.po'  # should be empty
uv run pytest tests/unit/assistant/ tests/unit/custom_projects/ -x --timeout=60

PR creation

git checkout tosinaf/i18n-po-callsites-migration-part4 && git pull
git checkout -b tosinaf/i18n-po-callsites-migration-part5
# ... make changes ...
gh pr create --draft --base tosinaf/i18n-po-callsites-migration-part4 \
  --title "refactor(i18n): migrate assistant + custom projects to PO source-as-ID" \
  --body "## Summary
- Migrate 49 files: 34 assistant core + 15 custom project files
- Part of i18n PO migration chain

## Translation safety
- PO files are NOT modified
- Only POT file changes

## Test plan
- [ ] make i18n-check passes
- [ ] make lint-f passes
- [ ] Assistant and custom project tests pass
- [ ] PO files have zero diff

Generated with [Claude Code](https://claude.com/claude-code)"

PR 7: Integrations + Framework + Diligence + DB Models

Base branch: tosinaf/i18n-po-callsites-migration-part5 New branch: tosinaf/i18n-po-callsites-migration-part6

Files (44 source + 1 POT = 45)

Integrations (16)

integrations/abstract_classes/non_oauth_integration_base.py
integrations/abstract_classes/oauth_integration_base.py
integrations/box.py
integrations/epona.py
integrations/google_drive.py
integrations/imanage.py
integrations/microsoft.py
integrations/netdocs.py
integrations/outlook.py
integrations/sendgrid/email_harvey_activities.py
integrations/sendgrid/parser.py
integrations/sharepoint.py
integrations/token_utils.py
integrations/utils.py
integrations/verification.py
ai_modules/integrations/workflow_adapter.py

Framework (3)

framework/hy_service.py
framework/validation.py
framework/verification.py

Diligence (12)

diligence/competitors.py
diligence/diligence_request_list/drl.py
diligence/diligence_request_list/drl_handler.py
diligence/followups_streaming_handler.py
diligence/prompts/tax.py
diligence/report.py
diligence/sections.py
diligence/streaming_handler.py
diligence/task.py
diligence/transcripts/interview.py
diligence/transcripts/transcribe.py
diligence/transcripts_streaming_handler.py

DB models + storage + misc (13)

ai_modules/modules/docx_edit/module.py
custom_projects/scotus/citation.py
custom_projects/s1_risk_factors/workflow_block_s1_template_extract.py
db/db.py
db/export/export.py
db/models/event.py
db/models/event_share_user.py
db/models/event_share_workspace.py
db/models/user_workspace.py
db/storage_managers/user_profiles_storage_manager.py
assistant/utils/ks_utils.py
assistant/utils/stream.py
assistant/workflow_stream_handler.py

JSON namespaces used

errors, integration, common, auth, settings, vault, assistant, word, notifications, custom_projects

Special cases

  • framework/verification.py — 79 translate calls, largest file in the entire migration
  • integrations/imanage.py — 47 translate calls
  • integrations/google_drive.py — 25 translate calls
  • integrations/box.py — 22 translate calls
  • integrations/microsoft.py — 21 translate calls
  • diligence/transcripts/interview.py — 7 translate calls
  • integrations/sendgrid/email_harvey_activities.py — new since original plan

Verification

make i18n-extract && make i18n-compile && make i18n-check && make lint-f
rg 'translate_msg_for_user|translate_msg_pre_auth' integrations/ framework/ diligence/ db/db.py db/export/ db/models/ db/storage_managers/ ai_modules/
git diff -- 'locales/*/LC_MESSAGES/messages.po'  # should be empty
uv run pytest tests/unit/integrations/ tests/unit/framework/ tests/unit/diligence/ tests/unit/db/models/ -x --timeout=60

PR creation

git checkout tosinaf/i18n-po-callsites-migration-part5 && git pull
git checkout -b tosinaf/i18n-po-callsites-migration-part6
# ... make changes ...
gh pr create --draft --base tosinaf/i18n-po-callsites-migration-part5 \
  --title "refactor(i18n): migrate integrations + framework + diligence to PO source-as-ID" \
  --body "## Summary
- Migrate 44 files: integrations, framework, diligence, db models/storage
- Includes highest-volume files: framework/verification.py (79 calls), integrations/imanage.py (47 calls)
- Part of i18n PO migration chain

## Translation safety
- PO files are NOT modified
- Only POT file changes

## Test plan
- [ ] make i18n-check passes
- [ ] make lint-f passes
- [ ] Integration, framework, diligence tests pass
- [ ] PO files have zero diff

Generated with [Claude Code](https://claude.com/claude-code)"

PR 8: Nested Routes + Functional Services + Workflow Utils + Translation

Base branch: tosinaf/i18n-po-callsites-migration-part6 New branch: tosinaf/i18n-po-callsites-migration-part7

Files (49 source + 1 POT = 50)

Nested routes — API/RPC/MCP (18)

routes/api/assistant/assistant_api.py
routes/api/base/base_api.py
routes/api/base/token_mgmt_api.py
routes/api/icertis/icertis_api.py
routes/api/integrations/integrations_api.py
routes/api/ks/ks_documents_api.py
routes/api/singapore_cjts/singapore_cjts.py
routes/api/vault/vault_api.py
routes/api/workflow/workflow_api.py
routes/mcp/servers.py
routes/rpc/diligence.py
routes/rpc/doc_processing/client.py
routes/rpc/doc_processing/service.py
routes/rpc/doc_processing/utils/blob_utils.py
routes/rpc/doc_processing/utils/gotenberg_utils.py
routes/rpc/doc_processing/utils/plaintext_fallback.py
routes/rpc/doc_processing/utils/service_utils.py
routes/rpc/doc_processing/utils/standard_extractor.py

Vault routes (10)

routes/vault_routes/files.py
routes/vault_routes/history.py
routes/vault_routes/magic_prompt.py
routes/vault_routes/projects.py
routes/vault_routes/review_assignments.py
routes/vault_routes/review_events.py
routes/vault_routes/review_queries.py
routes/vault_routes/sharing.py
routes/vault_routes/upload.py
routes/vault_routes/workflows.py

Functional services (7)

functional_services/file_service.py
functional_services/folder_service.py
functional_services/review_service.py
functional_services/review_workflow_service.py
functional_services/vault_pagination_service.py
functional_services/vault_to_knowledge_base_conversion_service.py
functional_services/vault_utils.py

Workflow utils (8)

assistant/workflows/admin_file_utils.py
assistant/workflows/block_helpers.py
assistant/workflows/knowledge_source_utils.py
assistant/workflows/loading_states.py
assistant/workflows/sanitization.py
assistant/workflows/workflow_agent_service.py
assistant/workflows/workflow_execution_engine.py
assistant/workflows/workflow_service.py

Translation (7)

translation/streaming_handler.py
translation/transform_document.py
translation/transform_docx.py
translation/transform_json.py
translation/transform_pdf.py
translation/transform_utils.py
translation/translate.py

JSON namespaces used

errors, vault, common, assistant, word, playbooks, integration, settings, auth, workflow, scim

Special cases

  • routes/api/vault/vault_api.py — 65 translate calls, huge file
  • routes/vault_routes/review_events.py — 58 translate calls
  • functional_services/review_service.py — 59 translate calls
  • routes/vault_routes/files.py — 47 translate calls
  • routes/vault_routes/history.py — 44 translate calls
  • functional_services/review_workflow_service.py — 35 translate calls
  • translation/translate.py — 16 translate calls
  • assistant/workflows/workflow_execution_engine.py — 15 translate calls
  • functional_services/vault_utils.py — rebase overlap
  • assistant/workflows/workflow_service.py — rebase overlap
  • translation/transform_utils.py — rebase overlap
  • routes/api/workflow/workflow_api.py — new since original plan

Verification

make i18n-extract && make i18n-compile && make i18n-check && make lint-f
rg 'translate_msg_for_user|translate_msg_pre_auth' routes/api/ routes/mcp/ routes/rpc/ routes/vault_routes/ functional_services/ assistant/workflows/ translation/
git diff -- 'locales/*/LC_MESSAGES/messages.po'  # should be empty
uv run pytest tests/unit/functional_services/ tests/unit/translation/ tests/unit/assistant/workflows/ -x --timeout=60

PR creation

git checkout tosinaf/i18n-po-callsites-migration-part6 && git pull
git checkout -b tosinaf/i18n-po-callsites-migration-part7
# ... make changes ...
gh pr create --draft --base tosinaf/i18n-po-callsites-migration-part6 \
  --title "refactor(i18n): migrate nested routes + services + translation to PO source-as-ID" \
  --body "## Summary
- Migrate 49 files: nested routes (API/RPC/MCP/vault_routes), functional services, workflow utils, translation
- Includes high-volume files: vault_api.py (65), review_service.py (59), review_events.py (58)
- Part of i18n PO migration chain

## Translation safety
- PO files are NOT modified
- Only POT file changes

## Test plan
- [ ] make i18n-check passes
- [ ] make lint-f passes
- [ ] Functional service, translation, workflow tests pass
- [ ] PO files have zero diff

Generated with [Claude Code](https://claude.com/claude-code)"

PR 9: Vault + Sharing + Auth + Final PO Sync

Base branch: tosinaf/i18n-po-callsites-migration-part7 New branch: tosinaf/i18n-po-callsites-migration-part8

THIS IS THE LAST CODEMOD PR. Special rules apply — see below.

Files (27 source + 1 POT + 9 PO = 37)

Auth (3)

auth/authentication.py
auth/permission_validation.py
auth/user_workspace_service.py

Citation (3)

citation/citation_scout_inline.py
citation/citation_scout_progress.py
citation/legacy_citation_scout.py

Resource sharing (7)

resource_sharing/resource_share_validations.py
resource_sharing/resource_type_share_operations.py
resource_sharing/resource_type_share_operations_event.py
resource_sharing/resource_type_share_operations_playbook.py
resource_sharing/resource_type_share_operations_review_table.py
resource_sharing/resource_type_share_operations_vault.py
resource_sharing/resource_type_share_operations_workflow.py

SCIM + root routes (3)

routes/root_routes/unlock_password_file_helper.py
routes/scim/scim_blueprint.py
routes/scim/scim_errors.py

Vault (10)

vault/config.py
vault/export_service.py
vault/magic_prompt.py
vault/review_v2.py
vault/review_v2_modules/answer_generation.py
vault/review_v2_modules/context_preparation.py
vault/utils.py
vault/validation.py
vault/vault_db.py
vault/vault_workflows.py

Assistant overflow (1)

assistant/workflow_draft_stream_handler.py

JSON namespaces used

errors, auth, vault, resource_sharing, scim, common, assistant, word, export

Special cases

  • auth/authentication.py — 19 translate calls
  • auth/permission_validation.py — 14 translate calls
  • vault/export_service.py — 17 translate calls
  • vault/vault_db.py — 12 translate calls, rebase overlap
  • vault/utils.py — 11 translate calls
  • routes/scim/scim_blueprint.py — 58 translate calls, rebase overlap

SPECIAL: Final PO Sync

This PR is different from PRs 2-8. After migrating all source files:

  1. Uncomment make i18n-update in the Makefile (it was commented out by #40117)
  2. Run the full i18n pipeline:
make i18n-extract       # Update POT with final msgids
make i18n-update        # NOW safe — syncs PO <-> POT, translation count guard catches losses >10
make i18n-compile       # Compile PO -> MO
make i18n-check         # Full verification
  1. The PO files WILL change in this PR (and only this PR) — that is expected
  2. Verify the translation count guard passes (built into make i18n-update)

Verification

# Full final verification
make i18n-extract && make i18n-update && make i18n-compile && make i18n-check && make lint-f
# Verify NO translate_msg calls remain in source (excluding tests, lint validators, and utils/i18n.py shims)
rg 'translate_msg_for_user|translate_msg_pre_auth' --type py \
  --glob '!tests/**' --glob '!lint/**' --glob '!utils/i18n.py'
# ^ Should return NOTHING
uv run pytest tests/unit/auth/ tests/unit/vault/ tests/unit/resource_sharing/ tests/unit/routes/scim/ -x --timeout=60
uv run pytest tests/unit/utils/test_i18n.py -v

PR creation

git checkout tosinaf/i18n-po-callsites-migration-part7 && git pull
git checkout -b tosinaf/i18n-po-callsites-migration-part8
# ... make changes ...
# IMPORTANT: uncomment make i18n-update in Makefile, then run full pipeline
gh pr create --draft --base tosinaf/i18n-po-callsites-migration-part7 \
  --title "refactor(i18n): migrate vault + auth + final PO sync" \
  --body "## Summary
- Migrate final 27 source files: auth, citation, resource_sharing, scim, vault
- **Final PO sync**: uncomments make i18n-update and runs full PO <-> POT synchronization
- Completes the i18n PO migration chain

## Translation safety
- This is the ONLY PR where PO files change
- Translation count guard validates <10 translations lost
- All ~1,778 pre-filled translations should be preserved

## Test plan
- [ ] make i18n-check passes (full pipeline)
- [ ] make lint-f passes
- [ ] Auth, vault, resource_sharing, scim tests pass
- [ ] No translate_msg_for_user/translate_msg_pre_auth calls remain in source (excluding test files, lint validators, utils/i18n.py shims)
- [ ] Translation count guard passes on make i18n-update

Generated with [Claude Code](https://claude.com/claude-code)"

Summary Table

PR Description Source Files POT PO Total Base Branch
2 Routes (top-level) 48 1 0 49 main
3 Root-level modules 43 1 0 44 part1
4 Workflow blocks 40 1 0 41 part2
5 DB services + retrieval 46 1 0 47 part3
6 Assistant + custom projects 49 1 0 50 part4
7 Integ + framework + diligence + db models 44 1 0 45 part5
8 Nested routes + services + translation 49 1 0 50 part6
9 Vault + sharing + auth + final sync 27 1 9 37 part7
Total 346

Agent Handoff Checklist

When handing a PR section to an agent, provide:

  1. This plan file (or the specific PR section)
  2. The "Global Transformation Rules" section (at the top of this doc)
  3. The base branch name (so the agent knows where to branch from)
  4. One instruction: "Migrate all translate_msg_for_user / translate_msg_pre_auth calls in the listed files to t() / t_mark(), following the transformation rules. Look up English strings in the JSON locale files. Run verification steps when done."

The agent does NOT need:

  • The OG branch — transformations are done by reading each file and looking up JSON keys
  • Access to other PR sections — each section is self-contained
  • Knowledge of the overall migration — just the mechanical transformation

Lessons Learned (Part 2 — PR #41109)

Issues caught in code review that caused multiple fix rounds. Add these to the agent handoff checklist for remaining PRs:

1. Always use get_user_locale() — never auth_user.app_language directly

  • Even when auth_user is non-null (guarded by if auth_user:), always use get_user_locale(auth_user) for consistency.
  • When auth_user is AuthUser | None (e.g. telemetry_logger.auth_user), get_user_locale() is required to avoid AttributeError.
  • Ensure get_user_locale is added to the import: from utils.i18n import get_user_locale, t

2. Write literal Unicode characters, not \uXXXX escape sequences

  • When the original JSON locale string contains smart quotes (', ", ") or ellipsis (), write the actual Unicode character in the Python source, not the escape form.
  • \u2019 in Python source is confusing to reviewers — write ' directly.
  • The strings are identical at runtime, but literal characters are much more readable.

3. Don't add unrelated assertions or code changes

  • The migration is mechanical — don't add assert statements, type guards, or other "helpful" changes that weren't in the original code. Keep diffs minimal.

4. Check interpolation variable types — t() is stricter than the old API

  • t() kwargs are typed str | float | boolNone is intentionally excluded because it would render as literal "None" in user-facing strings.
  • The old translate_msg_for_user accepted **kwargs: Any, so pre-existing nullable variables were silently passed through. After migration, pyright catches these.
  • When an interpolation variable is str | None, use value or '' to provide a safe fallback.
  • Example callsites that needed this:
    • audio/audio_stream_handler.py:169self.audio_format typed str | None, fix: format=self.audio_format or ''
    • utils/socket_utils.py:794req_type typed str | None, fix: req_type=req_type or ''

5. PRs should be independent (not stacked)

  • Each migration PR should branch from origin/main, not from the previous part's branch.
  • This avoids carrying unrelated diff from previous parts.

6. Run pyright before pushing

  • make lint-f catches ruff/flake8 but not pyright. Run uv run pyright <changed_files> to catch type errors before CI.

Updated Agent Handoff Checklist Addition

When handing a PR section to an agent, also include:

  • "Always use get_user_locale(user) — never user.app_language directly"
  • "Write literal Unicode characters (e.g. ') not escape sequences (e.g. \u2019)"
  • "Don't add assertions, type guards, or other changes not in the original code"
  • "Check interpolation variable types — t() rejects None (use value or '' for nullable vars)"
  • "Run uv run pyright <files> after migration to catch type errors"
  • "Dynamic keys (enum values, dict values passed as variables to t()) must use t_mark() at definition — Babel can't extract variables"
  • "Delete dead locale key mapping dicts (e.g. _SPOOFED_LOADING_STATE_LOCALE_KEYS) after migrating to t_mark()"
  • "After squash/rebuild, verify diff cleanliness: git diff --name-only, --diff-filter=D, and gh pr diff --name-only must all match expectations"
  • "If Part N changed a function signature, Part N+1 must update all callers and tests for that signature — don't assume a clean rebuild preserves earlier fixes"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment