Skip to content

Migrate legacy useTranslation hook call sites to next-intl useTranslations #18742

Description

@myelinated-wackerow

Context

Team decision (July 2026, codified in #18723): client components use namespace-bound useTranslations from next-intl directly, with one binding per namespace (const t = useTranslations("page-x"), plus e.g. const tCommon = useTranslations("common") for a second namespace). The custom @/hooks/useTranslation wrapper — which added namespace:key cross-namespace syntax and raw-message defaults — is now marked @deprecated (see #18723) but still has 103 importing files.

Why migrate (not just deprecate)

The wrapper's semantics diverge from next-intl in ways that hide bugs: its plain-key form returns t.raw() and silently ignores a values argument (see the sr-only quiz bug, #18743), it swallows missing keys, and its .rich/.raw passthroughs are bound to the root namespace (callers pass full dotted keys). Two translation APIs with different behavior is a standing footgun.

Current inventory (as of #18723)

  • 103 files import @/hooks/useTranslation (58 named import, 22 default import, rest mixed)
  • 33 call sites use the namespace:key colon form → each becomes a second namespace-bound function
  • 1 plain-key call passes values that are silently dropped (QuizRadioGroup.tsx:67 — tracked in fix(quiz): sr-only question number announces literal "{{number}}" #18743)
  • 2 files call t.rich through the wrapper with root-dotted keys (src/components/Staking/StakingLaunchpadWidget.tsx, WithdrawalsTabComparison.tsx)

Migration mechanics (not a mechanical sed)

Per file:

  1. const { t } = useTranslation("ns")const t = useTranslations("ns"); colon-form calls → additional namespace bindings.
  2. Embedded-HTML strings: the wrapper returns raw messages by default, so many call sites render Crowdin-era strings containing <b>/<a href> via htmr. Plain next-intl t() breaks on these — use namespace-bound t.raw("key") (or t.rich). Identify by checking the message content, not the call site.
  3. While touching a file, check whether it should be a Server Component using await getTranslations instead (see design-system skill, server-vs-client.md).
  4. Audit for latent t("plain-key", { values }) calls — these were silently non-interpolating and may reveal more hidden bugs like the quiz one.

Completion

  • Zero imports of @/hooks/useTranslation; delete the hook (it is already marked @deprecated pointing here).
  • Optional during transition: no-restricted-imports lint rule on @/hooks/useTranslation to prevent new usage.
  • Suggested batching: by directory (Quiz, Staking, layouts, ...) in small reviewable PRs; low urgency, good first-issue candidates for mechanical batches once the HTML-string pattern is documented in one example PR.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    dev requiredThis requires developer resourcesrefactor ♻️Changes which don't affect functionality

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions