Skip to content

refactor: t() / bilingual() positional → object form - #32

Merged
TrainTravel merged 1 commit into
mainfrom
refactor/t-object-form
May 23, 2026
Merged

refactor: t() / bilingual() positional → object form#32
TrainTravel merged 1 commit into
mainfrom
refactor/t-object-form

Conversation

@TrainTravel

Copy link
Copy Markdown
Owner

Summary

Convert the t() and bilingual() helpers in LanguageContext.tsx from positional (fr, en, es, zhHans?, zhHant?) arguments to a single Translations object argument. All 350 call sites swept programmatically via an AST-based codemod (TypeScript compiler API).

// BEFORE
t('Bonjour', 'Hello', 'Hola', '你好', '你好')
bilingual('Vide-tête', 'Brain Dump', 'Volcado mental')

// AFTER
t({ fr: 'Bonjour', en: 'Hello', es: 'Hola', 'zh-Hans': '你好', 'zh-Hant': '你好' })
bilingual({ fr: 'Vide-tête', en: 'Brain Dump', es: 'Volcado mental' })

Return shapes do NOT change: t() still returns { primary, secondary }, bilingual() still returns "target / primary". No user-visible change. This is pure dev-ergonomics work — pulled out of the original Language Pair PR (#25) for a clean diff per the discussion in memory/project_language_pair.md.

Why now

  • Adding a 6th supported language (Japanese, planned) costs nothing once Translations is an object — just add a new optional key. With the positional form, every call site would have had to grow a 6th arg.
  • The 5% of call sites with template literals or variable refs (t(badge.fr, badge.en, badge.es)) are sed-unsafe but AST-safe — the codemod handles them automatically.
  • Removes a class of silent ordering bugs where swapping two positional args compiles fine but ships broken UI.

Test plan

  • npx tsc --noEmit — clean
  • npx eslint . --ext .ts,.tsx — 11 errors / 27 warnings, identical to origin/main baseline; zero new issues
  • npx vitest run — 155 passed / 156 total (only pre-existing useJournal startFreeWrite fails; confirmed unchanged on main)
  • All 25 LanguageContext tests pass under the new signatures
  • Codemod script kept at scripts/codemod-t-object-form.mjs for future similar refactors
  • CI E2E will run on push — flaky baseline pre-existed
  • Manual: load the app, swap languages, verify chrome strings still render in correct lang

Codemod notes

  • Tooling: TypeScript compiler API (ts.createSourceFile + AST walk + getStart/getEnd text-range edits)
  • Per-file scope detection — only files that destructure t/bilingual from useLanguage() are touched
  • Already-object calls are skipped (idempotent)
  • Source text preserved verbatim per argument (template literals, accented chars, escape sequences, ternaries, variable refs all flow through unchanged)
  • 3 call sites in LanguageContext.test.tsx use result.current.t(...) member-access — the AST walker's identifier matcher doesn't catch these; updated manually

🤖 Generated with Claude Code

Replace the positional-argument signatures with a single object keyed by
language code. Same return shapes — t() still returns {primary, secondary},
bilingual() still returns "{target} / {primary}".

Before:
  t('Continuer', 'Continue', 'Continuar', '继续', '繼續').primary
After:
  t({ fr: 'Continuer', en: 'Continue', es: 'Continuar', 'zh-Hans': '继续', 'zh-Hant': '繼續' }).primary

Why object form:
  - Call sites are self-documenting: the key states which language it is.
  - Order mistakes (es vs zh-Hans) are now impossible.
  - Adding a sixth language no longer ripples through every call site.

Scope:
  - LanguageContext.tsx — new exported Translations type, single-arg signatures
  - LanguageContext.test.tsx — 3 call sites updated; all language tests still pass
  - 350 call sites across 35 files rewritten via AST codemod
  - Codemod kept at scripts/codemod-t-object-form.mjs for future similar refactors
  - CLAUDE.md "Bilingual System" examples updated to object form

Re-swept on top of current main (post PR #28 font picker, #29 Mandarin,
#30 theme metadata, #31 self-compassion). Single break, single PR — per
the explicit decision in the language pair follow-up plan.

Verification:
  - npx tsc --noEmit          → clean
  - eslint                    → 11 errors + 27 warnings (zero new vs main baseline)
  - vitest                    → 155/156 pass; the 1 failure (startFreeWrite) is pre-existing on main

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@TrainTravel
TrainTravel merged commit e09a8e9 into main May 23, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant