You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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:
const { t } = useTranslation("ns") → const t = useTranslations("ns"); colon-form calls → additional namespace bindings.
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.
While touching a file, check whether it should be a Server Component using await getTranslations instead (see design-system skill, server-vs-client.md).
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.
Context
Team decision (July 2026, codified in #18723): client components use namespace-bound
useTranslationsfromnext-intldirectly, with one binding per namespace (const t = useTranslations("page-x"), plus e.g.const tCommon = useTranslations("common")for a second namespace). The custom@/hooks/useTranslationwrapper — which addednamespace:keycross-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 avaluesargument (see the sr-only quiz bug, #18743), it swallows missing keys, and its.rich/.rawpassthroughs 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)
@/hooks/useTranslation(58 named import, 22 default import, rest mixed)namespace:keycolon form → each becomes a second namespace-bound functionvaluesthat are silently dropped (QuizRadioGroup.tsx:67— tracked in fix(quiz): sr-only question number announces literal "{{number}}" #18743)t.richthrough the wrapper with root-dotted keys (src/components/Staking/StakingLaunchpadWidget.tsx,WithdrawalsTabComparison.tsx)Migration mechanics (not a mechanical sed)
Per file:
const { t } = useTranslation("ns")→const t = useTranslations("ns"); colon-form calls → additional namespace bindings.<b>/<a href>via htmr. Plain next-intlt()breaks on these — use namespace-boundt.raw("key")(ort.rich). Identify by checking the message content, not the call site.await getTranslationsinstead (see design-system skill, server-vs-client.md).t("plain-key", { values })calls — these were silently non-interpolating and may reveal more hidden bugs like the quiz one.Completion
@/hooks/useTranslation; delete the hook (it is already marked@deprecatedpointing here).no-restricted-importslint rule on@/hooks/useTranslationto prevent new usage.🤖 Generated with Claude Code