Skip to content

Localize HIG built-in component strings via HIGProvider messages (#62) - #91

Closed
joeblau wants to merge 1 commit into
mainfrom
claude/hig-62-localize
Closed

joeblau wants to merge 1 commit into
mainfrom
claude/hig-62-localize

Conversation

@joeblau

@joeblau joeblau commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Refs #62

What

  • Contract: HIGMessages in @bloxwap/hig-contracts (root export plus a new ./messages subpath). English defaults are exported one per key, alongside defaultMessages and resolveMessages(). Function keys (pageOf(page,total), dismissNotice(message,dismiss), tradeHeading(asset), required(label), staleValue(value), labelledValue(label,value), …) receive host values, so translations can reorder them.
  • Provider: web and native HIGProvider take messages?: Partial<HIGMessages>. Nested providers inherit outer overrides; missing keys fall back to English. There is also a useMessages() hook.
  • Components: every built-in string now comes from messages: Close, Cancel, Dismiss (visible label and accessible name), the busy suffix, required/stale/label-value composition, landmark names, Pagination, Search, loading/empty/retry states, PriceChart fallback, native Menu/Select/Progress/BackButton, StatusIndicator state names, and recipe copy (TradeEntry, InstallCommand, ActivityList).
  • Per-component overrides (these win over the provider):
    • closeLabel: Dialog, AlertDialog, Popover, native Dialog, GestureSheet.
    • busyLabel: Button. '' hides the suffix but keeps aria-busy / accessibilityState.busy, for hosts that show a spinner.
    • dismissLabel: Toast.
    • label: native BackButton.
  • Accessible names stay in sync with visible text. The busy suffix is part of the button's name, and the Toast dismiss name embeds the visible dismiss label.
  • Bundle budget: each component imports only the English defaults it renders, so Button is 757/850 B gzip and Field 346/400 B gzip (npm run quality). A single defaults object pulled into every bundle measured 1331 B and broke the Button budget.
  • Docs: a key table and usage guidance in docs/foundations/localization.md.

Acceptance criteria

  • No hard-coded English without an override, enforced by a test. tests/localization-literals.test.ts walks the TypeScript AST of every file in packages/{react,native}/src. It fails on:

    • JSX text containing letters;
    • spoken attributes (aria-label, title, label, accessibilityLabel, …);
    • composed templates;
    • literals that flow into JSX children or templates.

    Technical literals (class names, roles, layout values, key names, module ids) are allowed by context. A second test checks that the scanner catches Close, aria-label='Close dialog', busy?'Working':… and `${n} results`. The scanner listed 96 literals before this change and lists 0 after it, including everything newly merged on main (Native fonts package, token font variables, and radius/type/motion/density roles (#64, #65, #66) #75, feat(native): token-role colours, Text font-scale cap/CJK tracking (#67) and Android slider set-progress (#71) #76, fix: shorter web buttons and pill-shaped inputs #80).

  • Translated labels render on web and native, including RTL.

    • tests/localization.dom.test.tsx renders fr-FR, ko-KR and ar-EG (dir=rtl). It checks that Close, Dismiss (visible text and accessible name) and the busy suffix are translated, and that no English remains. It also covers overrides, the empty busy label, nested providers and recipes.
    • tests/native/localization.test.cjs covers the same matrix on React Native. tests/native/gesture-sheet.test.cjs covers the GestureSheet Close label (RTL) and closeLabel.
    • The translations are test-only strings. HIG ships English, and product translations stay with the host (glossary rules apply).
  • Monorepo native app drops its busy-suffix workaround. This PR adds what that needs (<Button busy busyLabel=''> plus the host spinner, or messages.busy). The monorepo change has to consume a candidate built from this merge, and the source PR bloxwap/monorepo#4540 is still open. I'll link the follow-up on Localize HIG built-in component strings (Close, Dismiss, busy suffix) #62.

Proof (local, Linux, Node 22)

  • npm run check → exit 0 (vitest 18 files / 104 tests; jest native 53 tests; lint + drift; typecheck)
  • npm run examples:check && npm run quality && npm run smoke:ssr && npm run smoke:packed && npm run smoke:packed-boundaries && npm run smoke:packed-native → exit 0
  • Visible change: NumberReadout stale text now renders as one staleValue(reading) run, e.g. — (stale). Before, it was a reading followed by a separate (stale) span. tests/data.dom.test.tsx is updated to match.

Migration / rollback

Additive. English output is unchanged apart from the stale-readout DOM merge above. To roll back, revert this commit.

🤖 Generated with Claude Code

#62)

- New typed HIGMessages contract in @bloxwap/hig-contracts (+ ./messages
  subpath) with English defaults exported per key, defaultMessages and
  resolveMessages. Components import only the defaults they render, so
  the Button bundle stays within budget (757/850 B gzip).
- HIGProvider (web and native) takes a partial `messages`; nested
  providers inherit outer overrides, missing keys fall back to English.
- Every built-in string now comes from messages: Close, Cancel,
  Dismiss (+ accessible name), busy suffix, required/stale/labelled
  value composition, landmarks, pagination, search, loading/empty
  states, native Menu/Select/Progress/BackButton, StatusIndicator states
  and recipe copy (TradeEntry, InstallCommand, ActivityList).
- Per-component overrides: closeLabel (Dialog, AlertDialog, Popover,
  native Dialog, GestureSheet), busyLabel (Button; '' hides the suffix
  but keeps busy state), dismissLabel (Toast), label (native BackButton).
- tests/localization-literals.test.ts scans packages/{react,native}/src
  with the TypeScript AST and fails on any new hard-coded user-facing
  literal; web and native tests render fr-FR, ko-KR and ar-EG (RTL).
- Keys documented in docs/foundations/localization.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@joeblau

joeblau commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate: #62 shipped in #82 (872906a) while this was in review. Its acceptance criterion 3 (monorepo busy-suffix workaround) is still blocked on bloxwap/monorepo#4540, as noted on #62.

@joeblau joeblau closed this Oct 15, 2026
@joeblau
joeblau deleted the claude/hig-62-localize branch October 15, 2026 20:38
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