diff --git a/apps/web/src/app/[locale]/(admin)/events/page.tsx b/apps/web/src/app/[locale]/(admin)/events/page.tsx index 901e0cbd0..f63bac478 100644 --- a/apps/web/src/app/[locale]/(admin)/events/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/events/page.tsx @@ -6,7 +6,7 @@ import { PageHeader, PageTitle, } from "@/app/components/page-layout"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; export default async function Page({ params }: { params: Params }) { const { t } = await getTranslation(params.locale); diff --git a/apps/web/src/app/[locale]/(admin)/menu/page.tsx b/apps/web/src/app/[locale]/(admin)/menu/page.tsx index 7623c0f99..5cefabea9 100644 --- a/apps/web/src/app/[locale]/(admin)/menu/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/menu/page.tsx @@ -8,7 +8,7 @@ import { PageHeader, PageTitle, } from "@/app/components/page-layout"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; export default async function Page({ params }: { params: Params }) { const { t } = await getTranslation(params.locale); diff --git a/apps/web/src/app/[locale]/(admin)/page.tsx b/apps/web/src/app/[locale]/(admin)/page.tsx index fd7aab034..f406c6b29 100644 --- a/apps/web/src/app/[locale]/(admin)/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/page.tsx @@ -10,7 +10,7 @@ import { PageIcon, PageTitle, } from "@/app/components/page-layout"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; export default async function Page({ params }: { params: Params }) { const { t } = await getTranslation(params.locale); diff --git a/apps/web/src/app/[locale]/(admin)/polls/page.tsx b/apps/web/src/app/[locale]/(admin)/polls/page.tsx index 497bcd49a..587fd1456 100644 --- a/apps/web/src/app/[locale]/(admin)/polls/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/polls/page.tsx @@ -9,7 +9,7 @@ import { PageIcon, PageTitle, } from "@/app/components/page-layout"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; export default async function Page({ params, diff --git a/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx b/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx index 3e81f2dc2..3efabbaa7 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx @@ -3,8 +3,8 @@ import { notFound } from "next/navigation"; import { BillingPage } from "@/app/[locale]/(admin)/settings/billing/billing-page"; import { Params } from "@/app/[locale]/types"; -import { getTranslation } from "@/app/i18n"; import { env } from "@/env"; +import { getTranslation } from "@/i18n/server"; export default async function Page() { if (env.NEXT_PUBLIC_SELF_HOSTED === "true") { diff --git a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx index 3d621b6ae..2e99fb1d8 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx @@ -6,7 +6,7 @@ import { PageHeader, PageTitle, } from "@/app/components/page-layout"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; import { SettingsMenu } from "./settings-menu"; diff --git a/apps/web/src/app/[locale]/(admin)/settings/preferences/page.tsx b/apps/web/src/app/[locale]/(admin)/settings/preferences/page.tsx index 377df6db4..edd8f174a 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/preferences/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/preferences/page.tsx @@ -1,5 +1,5 @@ import { Params } from "@/app/[locale]/types"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; import { PreferencesPage } from "./preferences-page"; diff --git a/apps/web/src/app/[locale]/(admin)/settings/preferences/preferences-page.tsx b/apps/web/src/app/[locale]/(admin)/settings/preferences/preferences-page.tsx index a6c6cc1b7..3b48cb2cc 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/preferences/preferences-page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/preferences/preferences-page.tsx @@ -1,7 +1,6 @@ "use client"; import Head from "next/head"; -import { useTranslation } from "@/app/i18n/client"; import { DateTimePreferences } from "@/components/settings/date-time-preferences"; import { LanguagePreference } from "@/components/settings/language-preference"; import { @@ -10,6 +9,7 @@ import { SettingsSection, } from "@/components/settings/settings"; import { Trans } from "@/components/trans"; +import { useTranslation } from "@/i18n/client"; export function PreferencesPage() { const { t } = useTranslation(); diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx index 935fdf1e7..04c2ca7e1 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx @@ -15,8 +15,8 @@ import { Input } from "@rallly/ui/input"; import { signOut } from "next-auth/react"; import { useForm } from "react-hook-form"; -import { useTranslation } from "@/app/i18n/client"; import { Trans } from "@/components/trans"; +import { useTranslation } from "@/i18n/client"; import { usePostHog } from "@/utils/posthog"; import { trpc } from "@/utils/trpc/client"; @@ -32,7 +32,7 @@ export function DeleteAccountDialog({ email: "", }, }); - const { t } = useTranslation("app"); + const { t } = useTranslation(); const trpcUtils = trpc.useUtils(); const posthog = usePostHog(); const deleteAccount = trpc.user.delete.useMutation({ diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx index 30a7eaa6c..f80ee04fb 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx @@ -1,5 +1,5 @@ import { Params } from "@/app/[locale]/types"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; import { ProfilePage } from "./profile-page"; diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/profile-picture.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/profile-picture.tsx index 44b34aafe..e12a3b42f 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/profile-picture.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/profile-picture.tsx @@ -4,11 +4,11 @@ import * as Sentry from "@sentry/nextjs"; import React, { useState } from "react"; import { z } from "zod"; -import { useTranslation } from "@/app/i18n/client"; import { OptimizedAvatarImage } from "@/components/optimized-avatar-image"; import { Trans } from "@/components/trans"; import { useUser } from "@/components/user-provider"; import { IfCloudHosted } from "@/contexts/environment"; +import { useTranslation } from "@/i18n/client"; import { usePostHog } from "@/utils/posthog"; import { trpc } from "@/utils/trpc/client"; diff --git a/apps/web/src/app/[locale]/(auth)/login/page.tsx b/apps/web/src/app/[locale]/(auth)/login/page.tsx index 10caefed9..514774f15 100644 --- a/apps/web/src/app/[locale]/(auth)/login/page.tsx +++ b/apps/web/src/app/[locale]/(auth)/login/page.tsx @@ -3,8 +3,8 @@ import { Trans } from "react-i18next/TransWithoutContext"; import { LoginForm } from "@/app/[locale]/(auth)/login/login-form"; import { Params } from "@/app/[locale]/types"; -import { getTranslation } from "@/app/i18n"; import { AuthCard } from "@/components/auth/auth-layout"; +import { getTranslation } from "@/i18n/server"; export default async function LoginPage({ params }: { params: Params }) { const { t } = await getTranslation(params.locale); diff --git a/apps/web/src/app/[locale]/(auth)/register/page.tsx b/apps/web/src/app/[locale]/(auth)/register/page.tsx index ffab0cecd..4c5cec707 100644 --- a/apps/web/src/app/[locale]/(auth)/register/page.tsx +++ b/apps/web/src/app/[locale]/(auth)/register/page.tsx @@ -1,6 +1,6 @@ import { RegisterForm } from "@/app/[locale]/(auth)/register/register-page"; import { Params } from "@/app/[locale]/types"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; export default async function Page() { return ; diff --git a/apps/web/src/app/[locale]/auth/login/page.tsx b/apps/web/src/app/[locale]/auth/login/page.tsx index 4d5a7b7c4..6c68b3d62 100644 --- a/apps/web/src/app/[locale]/auth/login/page.tsx +++ b/apps/web/src/app/[locale]/auth/login/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation"; import { z } from "zod"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; import { LoginPage } from "./login-page"; diff --git a/apps/web/src/app/[locale]/invite/[urlId]/page.tsx b/apps/web/src/app/[locale]/invite/[urlId]/page.tsx index 5799090e3..6a63e0ce6 100644 --- a/apps/web/src/app/[locale]/invite/[urlId]/page.tsx +++ b/apps/web/src/app/[locale]/invite/[urlId]/page.tsx @@ -3,7 +3,7 @@ import { Metadata } from "next"; import { notFound } from "next/navigation"; import { InvitePage } from "@/app/[locale]/invite/[urlId]/invite-page"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; import { absoluteUrl } from "@/utils/absolute-url"; export default async function Page() { diff --git a/apps/web/src/app/[locale]/new/page.tsx b/apps/web/src/app/[locale]/new/page.tsx index c442a58e0..69e3bed21 100644 --- a/apps/web/src/app/[locale]/new/page.tsx +++ b/apps/web/src/app/[locale]/new/page.tsx @@ -3,9 +3,9 @@ import { Trans } from "react-i18next/TransWithoutContext"; import { GroupPollIcon } from "@/app/[locale]/(admin)/app-card"; import { BackButton } from "@/app/[locale]/(admin)/menu/menu-button"; import { Params } from "@/app/[locale]/types"; -import { getTranslation } from "@/app/i18n"; import { CreatePoll } from "@/components/create-poll"; import { UserDropdown } from "@/components/user-dropdown"; +import { getTranslation } from "@/i18n/server"; export default async function Page({ params }: { params: Params }) { const { t } = await getTranslation(params.locale); diff --git a/apps/web/src/app/[locale]/not-found.tsx b/apps/web/src/app/[locale]/not-found.tsx index 5bb139950..b8fc1e183 100644 --- a/apps/web/src/app/[locale]/not-found.tsx +++ b/apps/web/src/app/[locale]/not-found.tsx @@ -2,7 +2,7 @@ import { Button } from "@rallly/ui/button"; import { FileSearchIcon } from "lucide-react"; import Link from "next/link"; -import { getTranslation } from "@/app/i18n"; +import { getTranslation } from "@/i18n/server"; export default async function Page() { // TODO (Luke Vella) [2023-11-03]: not-found doesn't have access to params right now diff --git a/apps/web/src/app/i18n/client.tsx b/apps/web/src/app/i18n/client.tsx deleted file mode 100644 index 14987801d..000000000 --- a/apps/web/src/app/i18n/client.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; -import i18next, { Namespace } from "i18next"; -import ICU from "i18next-icu"; -import resourcesToBackend from "i18next-resources-to-backend"; -import { useParams } from "next/navigation"; -import React from "react"; -import { - I18nextProvider, - initReactI18next, - useTranslation as useTranslationOrg, -} from "react-i18next"; -import { useAsync } from "react-use"; - -import { defaultNS, getOptions } from "./settings"; - -async function initTranslations(lng: string) { - const i18n = i18next - .use(initReactI18next) - .use(ICU) - .use( - resourcesToBackend( - (language: string, namespace: string) => - import(`../../../public/locales/${language}/${namespace}.json`), - ), - ); - await i18n.init(getOptions(lng)); - - return i18n; -} - -export function useTranslation(ns?: Namespace) { - return useTranslationOrg(ns); -} - -export function I18nProvider({ children }: { children: React.ReactNode }) { - const params = useParams<{ locale: string }>(); - const locale = params?.locale ?? defaultNS; - - const res = useAsync(async () => { - return await initTranslations(locale); - }); - - if (!res.value) { - return null; - } - - return ( - - {children} - - ); -} diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx index cc3a96405..bbe6abe43 100644 --- a/apps/web/src/app/providers.tsx +++ b/apps/web/src/app/providers.tsx @@ -6,8 +6,8 @@ import { domMax, LazyMotion } from "framer-motion"; import { SessionProvider } from "next-auth/react"; import { useState } from "react"; -import { I18nProvider } from "@/app/i18n/client"; import { UserProvider } from "@/components/user-provider"; +import { I18nProvider } from "@/i18n/client"; import { AppRouter } from "@/trpc/routers"; import { ConnectedDayjsProvider } from "@/utils/dayjs"; import { trpcConfig } from "@/utils/trpc/config"; diff --git a/apps/web/src/components/event-card.tsx b/apps/web/src/components/event-card.tsx index 785b9f13d..82a8366be 100644 --- a/apps/web/src/components/event-card.tsx +++ b/apps/web/src/components/event-card.tsx @@ -5,13 +5,13 @@ import { Icon } from "@rallly/ui/icon"; import dayjs from "dayjs"; import { DotIcon, MapPinIcon, PauseIcon } from "lucide-react"; -import { useTranslation } from "@/app/i18n/client"; import TruncatedLinkify from "@/components/poll/truncated-linkify"; import VoteIcon from "@/components/poll/vote-icon"; import { PollStatusBadge } from "@/components/poll-status"; import { RandomGradientBar } from "@/components/random-gradient-bar"; import { Trans } from "@/components/trans"; import { usePoll } from "@/contexts/poll"; +import { useTranslation } from "@/i18n/client"; function IconGuide() { return ( diff --git a/apps/web/src/components/poll/desktop-poll.tsx b/apps/web/src/components/poll/desktop-poll.tsx index 5839144ea..360a5fe4e 100644 --- a/apps/web/src/components/poll/desktop-poll.tsx +++ b/apps/web/src/components/poll/desktop-poll.tsx @@ -23,11 +23,11 @@ import { EmptyStateIcon, EmptyStateTitle, } from "@/app/components/empty-state"; -import { useTranslation } from "@/app/i18n/client"; import { TimesShownIn } from "@/components/clock"; import { useVotingForm } from "@/components/poll/voting-form"; import { usePermissions } from "@/contexts/permissions"; import { usePoll } from "@/contexts/poll"; +import { useTranslation } from "@/i18n/client"; import { useParticipants, diff --git a/apps/web/src/components/poll/mobile-poll/poll-option.tsx b/apps/web/src/components/poll/mobile-poll/poll-option.tsx index 2af591962..4ba1f37a1 100644 --- a/apps/web/src/components/poll/mobile-poll/poll-option.tsx +++ b/apps/web/src/components/poll/mobile-poll/poll-option.tsx @@ -7,11 +7,11 @@ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import * as React from "react"; import { useToggle } from "react-use"; -import { useTranslation } from "@/app/i18n/client"; import { OptimizedAvatarImage } from "@/components/optimized-avatar-image"; import { useParticipants } from "@/components/participants-provider"; import { usePoll } from "@/contexts/poll"; import { useRole } from "@/contexts/role"; +import { useTranslation } from "@/i18n/client"; import { ConnectedScoreSummary } from "../score-summary"; import VoteIcon from "../vote-icon"; diff --git a/apps/web/src/components/trans.tsx b/apps/web/src/components/trans.tsx index b0e8886ad..25b5e87fa 100644 --- a/apps/web/src/components/trans.tsx +++ b/apps/web/src/components/trans.tsx @@ -1,6 +1,6 @@ import { Trans as BaseTrans } from "react-i18next"; -import { useTranslation } from "@/app/i18n/client"; +import { useTranslation } from "@/i18n/client"; import { I18nNamespaces } from "../../declarations/i18next"; @@ -11,6 +11,6 @@ export const Trans = (props: { children?: React.ReactNode; components?: Record | React.ReactElement[]; }) => { - const { t } = useTranslation("app"); + const { t } = useTranslation(); return ; }; diff --git a/apps/web/src/components/user-provider.tsx b/apps/web/src/components/user-provider.tsx index a9f24c75a..3f1ed62d3 100644 --- a/apps/web/src/components/user-provider.tsx +++ b/apps/web/src/components/user-provider.tsx @@ -3,11 +3,11 @@ import { Session } from "next-auth"; import { useSession } from "next-auth/react"; import React from "react"; -import { useTranslation } from "@/app/i18n/client"; import { Spinner } from "@/components/spinner"; import { useSubscription } from "@/contexts/plan"; import { PostHogProvider } from "@/contexts/posthog"; import { PreferencesProvider } from "@/contexts/preferences"; +import { useTranslation } from "@/i18n/client"; import { trpc } from "@/utils/trpc/client"; import { useRequiredContext } from "./use-required-context"; diff --git a/apps/web/src/i18n/client.tsx b/apps/web/src/i18n/client.tsx new file mode 100644 index 000000000..5e3da29d8 --- /dev/null +++ b/apps/web/src/i18n/client.tsx @@ -0,0 +1,34 @@ +"use client"; +import { useParams } from "next/navigation"; +import React from "react"; +import { + I18nextProvider, + useTranslation as useTranslationOrg, +} from "react-i18next"; +import useAsync from "react-use/lib/useAsync"; + +import { initI18next } from "./i18n"; +import { defaultNS } from "./settings"; + +export function useTranslation() { + return useTranslationOrg("app"); +} + +export function I18nProvider({ children }: { children: React.ReactNode }) { + const params = useParams<{ locale: string }>(); + const locale = params?.locale ?? defaultNS; + + const res = useAsync(async () => { + return await initI18next(locale, "app"); + }); + + if (!res.value) { + return null; + } + + return ( + + {children} + + ); +} diff --git a/apps/web/src/app/i18n.ts b/apps/web/src/i18n/i18n.ts similarity index 56% rename from apps/web/src/app/i18n.ts rename to apps/web/src/i18n/i18n.ts index 68770b424..37b1681d6 100644 --- a/apps/web/src/app/i18n.ts +++ b/apps/web/src/i18n/i18n.ts @@ -1,13 +1,15 @@ import { createInstance, Namespace } from "i18next"; +import ICU from "i18next-icu"; import resourcesToBackend from "i18next-resources-to-backend"; import { initReactI18next } from "react-i18next/initReactI18next"; -import { defaultNS, getOptions } from "./i18n/settings"; +import { getOptions } from "./settings"; -const initI18next = async (lng: string, ns: Namespace) => { +export const initI18next = async (lng: string, ns: Namespace) => { const i18nInstance = createInstance(); await i18nInstance .use(initReactI18next) + .use(ICU) .use( resourcesToBackend( (language: string, namespace: string) => @@ -17,14 +19,3 @@ const initI18next = async (lng: string, ns: Namespace) => { .init(getOptions(lng, ns)); return i18nInstance; }; - -export async function getTranslation( - locale: string, - ns: Namespace = defaultNS, -) { - const i18nextInstance = await initI18next(locale, ns); - return { - t: i18nextInstance.getFixedT(locale, Array.isArray(ns) ? ns[0] : ns), - i18n: i18nextInstance, - }; -} diff --git a/apps/web/src/i18n/server.ts b/apps/web/src/i18n/server.ts new file mode 100644 index 000000000..ca58e73fb --- /dev/null +++ b/apps/web/src/i18n/server.ts @@ -0,0 +1,16 @@ +import type { Namespace } from "i18next"; + +import { defaultNS } from "@/i18n/settings"; + +import { initI18next } from "./i18n"; + +export async function getTranslation( + locale: string, + ns: Namespace = defaultNS, +) { + const i18nextInstance = await initI18next(locale, ns); + return { + t: i18nextInstance.getFixedT(locale, Array.isArray(ns) ? ns[0] : ns), + i18n: i18nextInstance, + }; +} diff --git a/apps/web/src/app/i18n/settings.ts b/apps/web/src/i18n/settings.ts similarity index 95% rename from apps/web/src/app/i18n/settings.ts rename to apps/web/src/i18n/settings.ts index ec240cf21..ee224312a 100644 --- a/apps/web/src/app/i18n/settings.ts +++ b/apps/web/src/i18n/settings.ts @@ -10,7 +10,6 @@ export function getOptions( ns: string | string[] = defaultNS, ): InitOptions { return { - // debug: true, supportedLngs: languages, fallbackLng, lng, diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index b3d67d0ed..71ef713b9 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -11,9 +11,9 @@ import Head from "next/head"; import { SessionProvider, signIn, useSession } from "next-auth/react"; import React from "react"; -import { I18nProvider } from "@/app/i18n/client"; import Maintenance from "@/components/maintenance"; import { UserProvider } from "@/components/user-provider"; +import { I18nProvider } from "@/i18n/client"; import { ConnectedDayjsProvider } from "@/utils/dayjs"; import { trpc } from "@/utils/trpc/client";