Date: 2026-03-30 Status: Ready for execution Prerequisite PRs (all merged): #39975 (infra), #40117 (PO fill), #40174 (fixups)
Each PR section below is self-contained. Hand an agent a single PR section and it has everything it needs:
- The exact file list to migrate
- The transformation pattern
- Which JSON locale files to look up English strings in
- Branch/PR creation commands
- 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.
Every file follows the same mechanical pattern. There are no exceptions unless explicitly noted.
# 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 neededlocale 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))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())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 hardcodeden-US— ignores the client's languageget_request_locale()readsAccept-Languagefrom the Flask request, falling back toen-US- In defensive contexts where user might be None, the request header is a better signal
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.
| 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 |
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"inlocales/en-US/errors.jsontranslate_msg_for_user(user, 'folder_not_found', ns='vault')-> look up"folder_not_found"inlocales/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)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
- Do NOT run
make i18n-update— it will wipe pre-filled PO translations - DO run
make i18n-extractafter source changes (updates POT only) - PO files must have ZERO diff — only
locales/messages.potshould change - Do NOT remove or modify JSON locale files — they stay until full migration complete
- Do NOT modify
utils/i18n.py(except PR 3 which includes it in the file list)
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.
These issues came up during PR #40888 (routes migration) and apply to all subsequent PRs.
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_translate→mock_t(cosmetic but keeps things consistent)
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.
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 functionsWhen 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.
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.
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)returnsen-USinstead ofAttributeError - 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.
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_userusage are updated to recognizet()/t_mark()/get_user_locale()/get_request_locale() - The linter doesn't block PRs that remove
translate_msg_for_userimports - New lint rules catch bare
auth_user.app_languagewithoutt()(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.
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
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
errors, word, playbooks, vault, integration, groups, spaces, resource_sharing, settings, external_connections, library, scim, common, user_features, outlook, collab_requests, workspace_features, knowledge_sources
routes/client_admin.pyandroutes/client_admin_scim.pyneedt_markfor exception raisesroutes/assistant.pyhasdeprecation_message_keyfield — see PR 2 plan doc for detailsroutes/history.pyhas significant refactoring of helper functions
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=60See be-po-strings/i18n-codemod-pr2-routes.md for the full detailed plan.
Base branch: tosinaf/i18n-po-callsites-migration-part1
New branch: tosinaf/i18n-po-callsites-migration-part2
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
errors, common, assistant, auth, doc_qa, extraction, word, vault, playbooks, integration, knowledge_sources, custom_projects
utils/i18n.py— this file DEFINEStranslate_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 uset_markfor exception messageshosted_mcp/tools.py— has 19 translate calls, significant volumeaudio/audio_stream_handler.py— has 19 translate calls, significant volumemodel_gateway/app.py— has 13 translate callsethical_walls/_implementation/ethical_wall_service.py— has 14 translate calls
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=60git 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)"Base branch: tosinaf/i18n-po-callsites-migration-part2
New branch: tosinaf/i18n-po-callsites-migration-part3
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
errors, common, workflow, assistant, vault, playbooks, word, extraction
blocks/document_drafting.py— may have changed since plan was written (rebase overlap)blocks/loan_review.py— rebase overlapblocks/review_table_generation.py— rebase overlapblocks/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
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=60git 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)"Base branch: tosinaf/i18n-po-callsites-migration-part3
New branch: tosinaf/i18n-po-callsites-migration-part4
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/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
errors, groups, spaces, vault, settings, integration, external_connections, resource_sharing, knowledge_sources, common, playbooks, workspace_features, collab_requests, scim, library, word, user_features, workflow
db/services/admin_service/authz.py— 64 translate calls, biggest file in this batchdb/services/file_upload_service.py— 37 translate callsdb/services/generic_resource_sharing_service.py— 34 translate calls, rebase overlapdb/services/spaces_service.py— rebase overlapretrieval/knowledge_source_info.py— rebase overlaplexis_protege_knowledge_source.py— 11 translate calls- Knowledge source files often have
t_mark()for display names/descriptions that need to be extractable
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=60git 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)"Base branch: tosinaf/i18n-po-callsites-migration-part4
New branch: tosinaf/i18n-po-callsites-migration-part5
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/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
errors, assistant, common, word, playbooks, vault, custom_projects, extraction
assistant/agent/agent_sdk/tools/docx_drafting.py— 22 translate callsassistant/agent/agent_sdk/tools/convert_to_playbook_tool.py— 10 translate callsassistant/agent/agent_sdk/tools/playbook_rule_review.py— 11 translate callss1_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
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=60git 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)"Base branch: tosinaf/i18n-po-callsites-migration-part5
New branch: tosinaf/i18n-po-callsites-migration-part6
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/hy_service.py
framework/validation.py
framework/verification.py
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
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
errors, integration, common, auth, settings, vault, assistant, word, notifications, custom_projects
framework/verification.py— 79 translate calls, largest file in the entire migrationintegrations/imanage.py— 47 translate callsintegrations/google_drive.py— 25 translate callsintegrations/box.py— 22 translate callsintegrations/microsoft.py— 21 translate callsdiligence/transcripts/interview.py— 7 translate callsintegrations/sendgrid/email_harvey_activities.py— new since original plan
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=60git 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)"Base branch: tosinaf/i18n-po-callsites-migration-part6
New branch: tosinaf/i18n-po-callsites-migration-part7
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
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/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
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/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
errors, vault, common, assistant, word, playbooks, integration, settings, auth, workflow, scim
routes/api/vault/vault_api.py— 65 translate calls, huge fileroutes/vault_routes/review_events.py— 58 translate callsfunctional_services/review_service.py— 59 translate callsroutes/vault_routes/files.py— 47 translate callsroutes/vault_routes/history.py— 44 translate callsfunctional_services/review_workflow_service.py— 35 translate callstranslation/translate.py— 16 translate callsassistant/workflows/workflow_execution_engine.py— 15 translate callsfunctional_services/vault_utils.py— rebase overlapassistant/workflows/workflow_service.py— rebase overlaptranslation/transform_utils.py— rebase overlaproutes/api/workflow/workflow_api.py— new since original plan
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=60git 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)"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.
auth/authentication.py
auth/permission_validation.py
auth/user_workspace_service.py
citation/citation_scout_inline.py
citation/citation_scout_progress.py
citation/legacy_citation_scout.py
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
routes/root_routes/unlock_password_file_helper.py
routes/scim/scim_blueprint.py
routes/scim/scim_errors.py
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/workflow_draft_stream_handler.py
errors, auth, vault, resource_sharing, scim, common, assistant, word, export
auth/authentication.py— 19 translate callsauth/permission_validation.py— 14 translate callsvault/export_service.py— 17 translate callsvault/vault_db.py— 12 translate calls, rebase overlapvault/utils.py— 11 translate callsroutes/scim/scim_blueprint.py— 58 translate calls, rebase overlap
This PR is different from PRs 2-8. After migrating all source files:
- Uncomment
make i18n-updatein the Makefile (it was commented out by #40117) - 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- The PO files WILL change in this PR (and only this PR) — that is expected
- Verify the translation count guard passes (built into
make i18n-update)
# 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 -vgit 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)"| 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 |
When handing a PR section to an agent, provide:
- This plan file (or the specific PR section)
- The "Global Transformation Rules" section (at the top of this doc)
- The base branch name (so the agent knows where to branch from)
- One instruction: "Migrate all
translate_msg_for_user/translate_msg_pre_authcalls in the listed files tot()/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
Issues caught in code review that caused multiple fix rounds. Add these to the agent handoff checklist for remaining PRs:
- Even when
auth_useris non-null (guarded byif auth_user:), always useget_user_locale(auth_user)for consistency. - When
auth_userisAuthUser | None(e.g.telemetry_logger.auth_user),get_user_locale()is required to avoidAttributeError. - Ensure
get_user_localeis added to the import:from utils.i18n import get_user_locale, t
- When the original JSON locale string contains smart quotes (
',",") or ellipsis (…), write the actual Unicode character in the Python source, not the escape form. \u2019in Python source is confusing to reviewers — write'directly.- The strings are identical at runtime, but literal characters are much more readable.
- The migration is mechanical — don't add
assertstatements, type guards, or other "helpful" changes that weren't in the original code. Keep diffs minimal.
t()kwargs are typedstr | float | bool—Noneis intentionally excluded because it would render as literal"None"in user-facing strings.- The old
translate_msg_for_useraccepted**kwargs: Any, so pre-existing nullable variables were silently passed through. After migration, pyright catches these. - When an interpolation variable is
str | None, usevalue or ''to provide a safe fallback. - Example callsites that needed this:
audio/audio_stream_handler.py:169—self.audio_formattypedstr | None, fix:format=self.audio_format or ''utils/socket_utils.py:794—req_typetypedstr | None, fix:req_type=req_type or ''
- Each migration PR should branch from
origin/main, not from the previous part's branch. - This avoids carrying unrelated diff from previous parts.
make lint-fcatches ruff/flake8 but not pyright. Runuv run pyright <changed_files>to catch type errors before CI.
When handing a PR section to an agent, also include:
- "Always use
get_user_locale(user)— neveruser.app_languagedirectly" - "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()rejectsNone(usevalue 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 uset_mark()at definition — Babel can't extract variables" - "Delete dead locale key mapping dicts (e.g.
_SPOOFED_LOADING_STATE_LOCALE_KEYS) after migrating tot_mark()" - "After squash/rebuild, verify diff cleanliness:
git diff --name-only,--diff-filter=D, andgh pr diff --name-onlymust 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"