diff --git a/.eslintrc.json b/.eslintrc.json index 420e330c024..0c959230cb3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -52,17 +52,6 @@ ], "simple-import-sort/exports": "error", "no-unused-vars": "off", - "no-restricted-imports": [ - "warn", - { - "paths": [ - { - "name": "@/hooks/useTranslation", - "message": "Deprecated (#18742): bind namespaces directly with useTranslations from next-intl (client) or getTranslations from next-intl/server (server)." - } - ] - } - ], "unused-imports/no-unused-vars": [ "error", { diff --git a/AGENTS.md b/AGENTS.md index 9ff803c47cc..e28946d935c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -166,7 +166,7 @@ For pipeline mechanics, recovery, manifests, ETHGlossary integration, and the `i 3. **Follow import order** - ESLint will enforce, but be proactive 4. **Use TypeScript strictly** - No `any` types, prefer `unknown` 5. **Test in Storybook** - Create stories for new components (filename pattern: `.stories.tsx`) -6. **Consider i18n** - All user-facing text should be translatable. Server components: `getTranslations` and `getLocale` from `next-intl/server`. Client components: `useTranslations` from `next-intl`, one namespace-bound function per namespace - to access a second namespace, bind another function (e.g. `const tCommon = useTranslations("common")`) rather than reaching across namespaces. The legacy `@/hooks/useTranslation` wrapper (`namespace:key` syntax) is deprecated for new code +6. **Consider i18n** - All user-facing text should be translatable. Server components: `getTranslations` and `getLocale` from `next-intl/server`. Client components: `useTranslations` from `next-intl`, one namespace-bound function per namespace - to access a second namespace, bind another function (e.g. `const tCommon = useTranslations("common")`) rather than reaching across namespaces 7. **Mobile-first** - Design for mobile, enhance for desktop 8. **Accessibility** - Use Radix primitives, semantic HTML 9. **Use locale-aware formatting wrappers** - Use `numberFormat()` from `src/lib/utils/numbers.ts` instead of `new Intl.NumberFormat()`, and `dateTimeFormat()` from `src/lib/utils/date.ts` instead of `new Intl.DateTimeFormat()` / `.toLocaleDateString()` / `.toLocaleTimeString()`. Both enforce correct numbering systems and calendar for Urdu and Arabic locales. diff --git a/app/[locale]/roadmap/_components/ReleaseCarousel.tsx b/app/[locale]/roadmap/_components/ReleaseCarousel.tsx index 18ec259a069..b9df1beb74c 100644 --- a/app/[locale]/roadmap/_components/ReleaseCarousel.tsx +++ b/app/[locale]/roadmap/_components/ReleaseCarousel.tsx @@ -1,7 +1,7 @@ "use client" import { useCallback, useEffect, useMemo, useState } from "react" -import { useLocale } from "next-intl" +import { useLocale, useTranslations } from "next-intl" import { Image } from "@/components/Image" import { ButtonLink } from "@/components/ui/buttons/Button" @@ -19,11 +19,9 @@ import { dateTimeFormat, formatDate } from "@/lib/utils/date" import { getReleasesData, Release } from "@/data/roadmap/releases" -import { useTranslation } from "@/hooks/useTranslation" - const ReleaseCarousel = () => { const locale = useLocale() - const { t } = useTranslation("page-roadmap") + const t = useTranslations("page-roadmap") const releasesData = useMemo(() => getReleasesData(t), [t]) diff --git a/docs/best-practices.md b/docs/best-practices.md index 047b035de40..3ff8dd6ac03 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -52,7 +52,7 @@ Markdown will be translated as whole pages of content, so no specific action is - _tl;dr Each individual JSON entry should be a complete phrase by itself_ -- This is done using the `Translation` component. However, there is an alternative method for regular JS: using the `t` function from `@/hooks/useTranslation` +- This is done using the `Translation` component. However, there is an alternative method for regular JS: using the `t` function from `next-intl` - **Method one: `` component (preferred if only needed in JSX)** @@ -66,14 +66,19 @@ Markdown will be translated as whole pages of content, so no specific action is - **Method two: `t()`** ```tsx - import { useTranslation } from "@/hooks/useTranslation" + // Client components + import { useTranslations } from "next-intl" - // Utilize anywhere in JS using - const { t } = useTranslation() + // Bind one function per namespace, then access keys within it + const t = useTranslations("common") t("language-json-key") ``` ```tsx + // Server components + import { getTranslations } from "next-intl/server" + + const t = await getTranslations("common") const siteTitle = t("site-title") ``` diff --git a/src/components/FindWalletProductTable/FindWalletLanguageSelectInput.tsx b/src/components/FindWalletProductTable/FindWalletLanguageSelectInput.tsx index cb9378cf246..1eb862ae6b5 100644 --- a/src/components/FindWalletProductTable/FindWalletLanguageSelectInput.tsx +++ b/src/components/FindWalletProductTable/FindWalletLanguageSelectInput.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react" -import { useLocale } from "next-intl" +import { useLocale, useTranslations } from "next-intl" import { FilterInputState, Lang } from "@/lib/types" @@ -16,8 +16,6 @@ import { getLanguageCodeName } from "@/lib/utils/intl" import { trackCustomEvent } from "@/lib/utils/matomo" import { getLanguageCountWalletsData } from "@/lib/utils/wallets" -import { useTranslation } from "@/hooks/useTranslation" - interface FindWalletLanguageSelectInputProps { filterIndex: number itemIndex: number @@ -39,7 +37,7 @@ const FindWalletLanguageSelectInput = ({ const [searchQuery, setSearchQuery] = useState("") const [isSelectOpen, setIsSelectOpen] = useState(false) const searchInputRef = useRef(null) - const { t } = useTranslation("page-wallets-find-wallet") + const t = useTranslations("page-wallets-find-wallet") const languageCountWalletsData = getLanguageCountWalletsData(locale as string) const countSortedLanguagesCount = [...languageCountWalletsData].sort( (a, b) => b.count - a.count diff --git a/src/components/FindWalletProductTable/FindWalletsNoResults.tsx b/src/components/FindWalletProductTable/FindWalletsNoResults.tsx index 1296c1d6e57..65989678414 100644 --- a/src/components/FindWalletProductTable/FindWalletsNoResults.tsx +++ b/src/components/FindWalletProductTable/FindWalletsNoResults.tsx @@ -1,11 +1,11 @@ +import { useTranslations } from "next-intl" + import { trackCustomEvent } from "@/lib/utils/matomo" import { Button } from "../ui/buttons/Button" -import { useTranslation } from "@/hooks/useTranslation" - const FindWalletsNoResults = ({ resetFilters }) => { - const { t } = useTranslation("page-wallets-find-wallet") + const t = useTranslations("page-wallets-find-wallet") // Track empty state trackCustomEvent({ diff --git a/src/components/FindWalletProductTable/PersonaTags.tsx b/src/components/FindWalletProductTable/PersonaTags.tsx index 7619ac50eae..ef267535944 100644 --- a/src/components/FindWalletProductTable/PersonaTags.tsx +++ b/src/components/FindWalletProductTable/PersonaTags.tsx @@ -1,15 +1,14 @@ import { memo } from "react" +import { useTranslations } from "next-intl" import { Tag } from "../ui/tag" -import { useTranslation } from "@/hooks/useTranslation" - type PersonaTagsProps = { walletPersonas: string[] } const PersonaTags = ({ walletPersonas }: PersonaTagsProps) => { - const { t } = useTranslation("page-wallets-find-wallet") + const t = useTranslations("page-wallets-find-wallet") if (walletPersonas.length === 0) return null diff --git a/src/components/FindWalletProductTable/WalletInfo.tsx b/src/components/FindWalletProductTable/WalletInfo.tsx index a75973d8a36..ac9726b3a2c 100644 --- a/src/components/FindWalletProductTable/WalletInfo.tsx +++ b/src/components/FindWalletProductTable/WalletInfo.tsx @@ -1,5 +1,6 @@ import { memo, useMemo } from "react" import { ChevronDown, ChevronUp } from "lucide-react" +import { useTranslations } from "next-intl" import type { ChainName, Wallet } from "@/lib/types" @@ -17,14 +18,12 @@ import { TagsInlineText } from "../ui/tag" import PersonaTags from "./PersonaTags" -import { useTranslation } from "@/hooks/useTranslation" - interface WalletInfoProps { wallet: Wallet } const WalletInfo = ({ wallet }: WalletInfoProps) => { - const { t } = useTranslation("page-wallets-find-wallet") + const t = useTranslations("page-wallets-find-wallet") const walletPersonas = useMemo(() => { return getWalletPersonas(wallet) diff --git a/src/components/FindWalletProductTable/WalletSubComponent.tsx b/src/components/FindWalletProductTable/WalletSubComponent.tsx index 7c0be894f15..0a7c352dca0 100644 --- a/src/components/FindWalletProductTable/WalletSubComponent.tsx +++ b/src/components/FindWalletProductTable/WalletSubComponent.tsx @@ -1,5 +1,5 @@ import { Globe, Info } from "lucide-react" -import { useLocale } from "next-intl" +import { useLocale, useTranslations } from "next-intl" import { FilterOption, Lang, WalletData } from "@/lib/types" @@ -16,8 +16,6 @@ import InlineLink from "@/components/ui/Link" import { cn } from "@/lib/utils/cn" import { getLocaleFormattedDate } from "@/lib/utils/date" -import { useTranslation } from "@/hooks/useTranslation" - const SocialLink = (props) => ( { const locale = useLocale() - const { t } = useTranslation("page-wallets-find-wallet") + const t = useTranslations("page-wallets-find-wallet") const walletFiltersOptions: FilterOption[] = useWalletFilters() const walletFilterDisplayOrder = [ diff --git a/src/components/FindWalletProductTable/hooks/useWalletFilters.tsx b/src/components/FindWalletProductTable/hooks/useWalletFilters.tsx index ff9a9cf5c17..61d34506635 100644 --- a/src/components/FindWalletProductTable/hooks/useWalletFilters.tsx +++ b/src/components/FindWalletProductTable/hooks/useWalletFilters.tsx @@ -1,6 +1,6 @@ import { useRef } from "react" import { Brain, LayersPlus } from "lucide-react" -import { useLocale } from "next-intl" +import { useLocale, useTranslations } from "next-intl" import { FilterOption } from "@/lib/types" @@ -15,11 +15,10 @@ import { trackCustomEvent } from "@/lib/utils/matomo" import { DEFAULT_LOCALE } from "@/lib/constants" -import { useTranslation } from "@/hooks/useTranslation" - export const useWalletFilters = (): FilterOption[] => { const locale = useLocale() - const { t } = useTranslation("page-wallets-find-wallet") + const t = useTranslations("page-wallets-find-wallet") + const tTable = useTranslations("table") const prevNetworkArray = useRef([]) return [ { @@ -31,7 +30,7 @@ export const useWalletFilters = (): FilterOption[] => { filterLabel: t("page-find-wallet-mobile"), description: "", inputState: false, - optionsLegend: t("table:table-mobile-platforms") as string, + optionsLegend: tTable("table-mobile-platforms") as string, input: (filterIndex, itemIndex, inputState, updateFilterState) => { return ( { filterLabel: t("page-find-wallet-desktop"), description: "", inputState: false, - optionsLegend: t("table:table-desktop-platforms") as string, + optionsLegend: tTable("table-desktop-platforms") as string, input: (filterIndex, itemIndex, inputState, updateFilterState) => { return ( { filterLabel: t("page-find-wallet-browser"), description: "", inputState: false, - optionsLegend: t("table:table-browser-engines") as string, + optionsLegend: tTable("table-browser-engines") as string, input: (filterIndex, itemIndex, inputState, updateFilterState) => { return ( { - const { t } = useTranslation("page-wallets-find-wallet") + const t = useTranslations("page-wallets-find-wallet") const personas: WalletPersonas[] = [ { title: t("page-find-wallet-new-to-crypto-title"), diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx index c32dfe2237a..f97bb776d44 100644 --- a/src/components/FindWalletProductTable/index.tsx +++ b/src/components/FindWalletProductTable/index.tsx @@ -1,4 +1,5 @@ "use client" +import { useTranslations } from "next-intl" import type { WalletRow } from "@/lib/types" @@ -13,10 +14,8 @@ import FindWalletsNoResults from "./FindWalletsNoResults" import List from "./List" import WalletSubComponent from "./WalletSubComponent" -import { useTranslation } from "@/hooks/useTranslation" - const FindWalletProductTable = ({ wallets }: { wallets: WalletRow[] }) => { - const { t } = useTranslation("page-wallets-find-wallet") + const t = useTranslations("page-wallets-find-wallet") const walletPersonas = useWalletPersonaPresets() const walletFilterOptions = useWalletFilters() diff --git a/src/hooks/useTranslation.ts b/src/hooks/useTranslation.ts deleted file mode 100644 index 46a2ffbbb11..00000000000 --- a/src/hooks/useTranslation.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useTranslations } from "next-intl" - -/** - * Cases to handle: - * - * - using t("key") - * - & useTranslation() => "common.key" - * - & useTranslation("namespace") => "namespace.key" - * - & useTranslation(["namespace1", "namespace2"]) => "namespace1.key" - * - * - using t("namespace:key") - * - & useTranslation("namespace") and t("namespace:key") => "namespace.key" - * - & useTranslation(["namespace1", "namespace2"]) and t("namespace1:key") => "namespace1.key" - * - & useTranslation(["namespace1", "namespace2"]) and t("namespace2:key") => "namespace2.key" - */ - -const DEFAULT_NAMESPACE = "common" - -/** - * @deprecated Use namespace-bound `useTranslations` from "next-intl" directly — one binding per namespace (e.g. `const tCommon = useTranslations("common")` for a second namespace); use `t.raw`/`t.rich` for strings with embedded HTML. Migration: #18742. This file will be deleted once no consumers remain. - */ -export function useTranslation(namespaces?: string[] | string) { - const t = useTranslations() - - const customT = ( - fullKey: string, - values?: Record - ) => { - try { - if (fullKey.includes(":")) { - const [namespace, key] = fullKey.split(":") - - if (values) { - return t(`${namespace}.${key}`, values) - } - - return t.raw(`${namespace}.${key}`) - } - - const namespace = Array.isArray(namespaces) - ? namespaces[0] - : namespaces || DEFAULT_NAMESPACE - - return t.raw(`${namespace}.${fullKey}`) - } catch (error) { - // Suppress errors by default, enable if needed to debug - // console.error(error) - return fullKey - } - } - - // keep the original methods - customT.raw = t.raw - customT.rich = t.rich - customT.markup = t.markup - customT.has = t.has - - return { t: customT } -} - -export default useTranslation