Skip to content

refactor(i18n): settle all 24 divergences; write the overrides down - #594

Merged
mrviduus merged 1 commit into
mainfrom
chore/locale-divergences
Sep 9, 2026
Merged

refactor(i18n): settle all 24 divergences; write the overrides down#594
mrviduus merged 1 commit into
mainfrom
chore/locale-divergences

Conversation

@mrviduus

@mrviduus mrviduus commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Slice 4 of five. Shared is the source and web is an overlay, so a string can only exist twice if somebody chose that. Twenty-four keys still did. Each was decided on its own evidence rather than by a rule.

Fourteen were dead in shared, not divergent

Website SEO and brand copy for pages the app does not have — authors.seoDesc, books.seoDesc, contact.seoDesc, contact.seoTitle, footer.description, nav.brandTitle, nav.aboutTextStack, three bookDetail.faq* — plus home.hero.* and home.seo.content, which mobile never referenced because it has no home screen (Library is the front door), and vocabulary.deleteConfirm.

Deleted from shared; web keeps them as its own.

This is also a correction to something I said earlier in this work: mobile was not showing an abandoned hero. It was carrying a string nothing rendered.

One key was serving two different destinations

tutor.signIn.cta is the button on the signed-out tutor screen.

destination label
web /books "Browse books"
mobile sign-in "Sign in"

Each label matches its own handler — so this was never a mislabelled button, it was one name for two actions. Web's is now tutor.signIn.ctaBrowse and shared's cta says "Sign in". Neither is an override any more.

One was real drift

reader.ask.indexFailed: "Preparation failed." on web, "Could not index this book." on mobile. Same state, two messages, and the mobile one tells the reader what happened. Web now inherits it — a production copy change, visible as a fixture line.

Eight are deliberate and now written down

overrides.test.ts asserts set equality against the register — so removing an override, or one quietly becoming a duplicate again, fails too. The reasons live in the test because JSON has no comments and a reason nothing enforces is not a reason.

  • contact.responseBody — brand: the website is "TextStack Reader", the app is "TextStack"
  • librarian.openBook — different jobs: web interpolates a title, mobile uses the string as a bare a11y label
  • library.actions.addToCollectionEmpty — different affordance in view
  • library.sort.added / .progress / .recent — short labels for a phone
  • library.collections.new — mobile bakes a + into the copy (flagged as a smell)
  • reader.vocab.tapAgainToStudy — a mechanism difference, {{n}} vs {n}; to be retired by giving the shared t() a vars argument, not by editing the string

Verification

The fixture diffs are the review, and they are small: web has two changed values and one added key; mobile has fourteen removals and not one changed string.

🤖 Generated with Claude Code

https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E

Slice 4 of five. Shared is the source and web is an overlay, so a string can only
exist twice if somebody chose that. Twenty-four keys still did. Each was decided
on its own evidence rather than by a rule.

**Fourteen were dead in shared, not divergent.** Website SEO and brand copy for
pages the app does not have — `authors.seoDesc`, `books.seoDesc`, `contact.seoDesc`,
`contact.seoTitle`, `footer.description`, `nav.brandTitle`, `nav.aboutTextStack`,
three `bookDetail.faq*` — plus `home.hero.*` and `home.seo.content`, which mobile
never referenced because it has no home screen (Library is the front door), and
`vocabulary.deleteConfirm`. Deleted from shared; web keeps them as its own. This is
also the correction to what I said earlier: mobile was not showing an abandoned
hero, it was carrying a string nothing rendered.

**One key was serving two different destinations.** `tutor.signIn.cta` is the
button on the signed-out tutor screen. Web's goes to `/books` and says "Browse
books"; mobile's goes to sign-in and says "Sign in". Each label matches its own
handler — so this was never a mislabelled button, it was one name for two actions.
Web's is now `tutor.signIn.ctaBrowse` and shared's `cta` says "Sign in". Neither is
an override any more.

**One was real drift.** `reader.ask.indexFailed`: "Preparation failed." on web,
"Could not index this book." on mobile. Same state, two messages, and the mobile
one tells the reader what happened. Web now inherits it — a production copy change,
visible as a fixture line.

**Eight are deliberate and now written down** in `overrides.test.ts`, which asserts
**set equality** against the register — so removing an override, or one quietly
becoming a duplicate again, fails too. The reasons live in the test because JSON has
no comments and a reason nothing enforces is not a reason. They are: brand
(`contact.responseBody`), different jobs (`librarian.openBook` — web interpolates a
title, mobile uses the string as a bare a11y label), different affordance in view
(`library.actions.addToCollectionEmpty`), short phone labels (three
`library.sort.*`), a "+" baked into mobile copy (`library.collections.new`, flagged
as a smell), and one mechanism difference (`reader.vocab.tapAgainToStudy`, `{{n}}`
vs `{n}` — to be retired by giving the shared t() a vars argument, not by editing
the string).

The fixture diffs are the review, and they are small: web has two changed values
and one added key; **mobile has fourteen removals and not one changed string.**

786 web + 449 shared + 385 mobile tests green; both typechecks clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
@mrviduus
mrviduus merged commit 9bbeca7 into main Sep 9, 2026
10 checks passed
@mrviduus
mrviduus deleted the chore/locale-divergences branch September 9, 2026 04:10
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