diff --git a/app/(auth)/sign-in/collaborator/page.tsx b/app/(auth)/sign-in/collaborator/page.tsx index 753467032..fbb722f52 100644 --- a/app/(auth)/sign-in/collaborator/page.tsx +++ b/app/(auth)/sign-in/collaborator/page.tsx @@ -1,19 +1,21 @@ import { InviteSignIn } from "@/components/invite-sign-in"; import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty"; +import { getTranslations } from "next-intl/server"; export default async function Page({ searchParams, }: { searchParams: Promise<{ token?: string }>; }) { + const t = await getTranslations("InviteSignIn"); const { token } = await searchParams; if (!token?.trim()) { return ( - Invite unavailable - This invitation link is invalid. + {t("unavailableTitle")} + {t("unavailableLinkDesc")} ); diff --git a/app/(main)/loading.tsx b/app/(main)/loading.tsx index f392b2e33..7512fd3b0 100644 --- a/app/(main)/loading.tsx +++ b/app/(main)/loading.tsx @@ -1,5 +1,12 @@ import { Loader } from "@/components/loader"; +import { getTranslations } from "next-intl/server"; -export default function Loading() { - return Loading; +export default async function Loading() { + const t = await getTranslations("Loading"); + + return ( + + {t("text")} + + ); } diff --git a/app/(main)/main-root-layout.tsx b/app/(main)/main-root-layout.tsx index 537dbb76e..802a172fc 100644 --- a/app/(main)/main-root-layout.tsx +++ b/app/(main)/main-root-layout.tsx @@ -1,6 +1,7 @@ import { User } from "@/components/user"; import { AdminButton } from "@/components/admin-button"; import { About } from "@/components/about"; +import { LanguageSwitcher } from "@/components/language-switcher"; export function MainRootLayout({ children, @@ -13,6 +14,7 @@ export function MainRootLayout({
+
diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 71a8da0d7..d3cfcfc41 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -7,6 +7,7 @@ import { RepoSelect } from "@/components/repo/repo-select"; import { RepoTemplates } from "@/components/repo/repo-templates"; import { RepoLatest } from "@/components/repo/repo-latest"; import { DocumentTitle } from "@/components/document-title"; +import { useTranslations } from "next-intl"; import { hasGithubIdentity } from "@/lib/authz-shared"; import { Empty, @@ -22,6 +23,7 @@ export default function Page() { const [defaultAccount, setDefaultAccount] = useState(null); const [hasRecentVisits, setHasRecentVisits] = useState(false); const { user } = useUser(); + const t = useTranslations("HomePage"); const isGithubUser = hasGithubIdentity(user); useEffect(() => { @@ -33,21 +35,21 @@ export default function Page() { return ( - +
{user.accounts.length > 0 ? (
{hasRecentVisits && (

- Recently visited + {t("recent")}

)}

- Open a project + {t("open")}

setDefaultAccount(account)} @@ -56,7 +58,7 @@ export default function Page() { {isGithubUser && (

- Create from a template + {t("template")}

@@ -65,10 +67,9 @@ export default function Page() { ) : isGithubUser ? ( - Install the GitHub App + {t("installTitle")} - Install the GitHub App on at least one account before you can - open or create projects. + {t("installDesc")} @@ -85,18 +86,16 @@ export default function Page() { GitHub - Install GitHub App + {t("installCta")} ) : ( - No repositories yet + {t("noneTitle")} - You need an invitation to a repository before you can - collaborate. Ask a repository owner or organization admin to - invite you. + {t("noneDesc")} diff --git a/app/(main)/settings/page.tsx b/app/(main)/settings/page.tsx index 217047cd3..d62ddb9b5 100644 --- a/app/(main)/settings/page.tsx +++ b/app/(main)/settings/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { headers } from "next/headers"; +import { getTranslations } from "next-intl/server"; import { and, eq } from "drizzle-orm"; import { auth } from "@/lib/auth"; import { db } from "@/db"; @@ -21,6 +22,7 @@ import { ArrowLeft } from "lucide-react"; import { cn } from "@/lib/utils"; export default async function Page() { + const t = await getTranslations("SettingsPage"); const session = await auth.api.getSession({ headers: await headers(), }); @@ -39,7 +41,7 @@ export default async function Page() { return ( - +
- Go home + {t("back")}

- Settings + {t("title")}

@@ -65,9 +67,11 @@ export default async function Page() { - Authentication + + {t("authTitle")} + - Your sign-in methods and linked identity providers. + {t("authDesc")} @@ -84,10 +88,10 @@ export default async function Page() { - Installations + {t("instTitle")} - Manage the accounts the Github application is installed on. + {t("instDesc")} diff --git a/app/error.tsx b/app/error.tsx index 15a01fd71..cb8574136 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { buttonVariants } from "@/components/ui/button"; import { GithubAuthExpired } from "@/components/github-auth-expired"; import { isGithubAuthError } from "@/lib/github-auth"; +import { useTranslations } from "next-intl"; import { Empty, EmptyContent, @@ -20,6 +21,8 @@ export default function Error({ error: Error & { digest?: string }; reset: () => void; }) { + const t = useTranslations("ErrorPage"); + useEffect(() => { console.error(error); }, [error]); @@ -31,7 +34,7 @@ export default function Error({ return ( - Something went wrong + {t("title")} {error.message} @@ -39,13 +42,13 @@ export default function Error({ className={buttonVariants({ variant: "default" })} href="/" > - Go home + {t("goHome")} diff --git a/app/layout.tsx b/app/layout.tsx index a8906bba1..a43a8a373 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,7 @@ import { Toaster } from "@/components/ui/sonner" import { Providers } from "@/components/providers"; +import { NextIntlClientProvider } from "next-intl"; +import { getLocale } from "next-intl/server"; import type { Metadata } from "next"; import { Inter, JetBrains_Mono } from "next/font/google"; import { getBaseUrl } from "@/lib/base-url"; @@ -57,8 +59,10 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const locale = await getLocale(); + return ( - + - - {children} - + + + {children} + + diff --git a/app/not-found.tsx b/app/not-found.tsx index 4cac55f1c..c71842617 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,17 +1,20 @@ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty"; import Link from "next/link"; import { buttonVariants } from "@/components/ui/button"; +import { getTranslations } from "next-intl/server"; + +export default async function NotFound() { + const t = await getTranslations("NotFoundPage"); -export default function NotFound() { return ( - Page not found - The page or resource you requested could not be found. + {t("title")} + {t("desc")} - Go home + {t("goHome")} diff --git a/components/about.tsx b/components/about.tsx index 0bd32756d..927102ba6 100644 --- a/components/about.tsx +++ b/components/about.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { useEffect, useMemo, useState } from "react"; import { ArrowUpRight } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { @@ -31,6 +32,7 @@ const version = const UPDATE_DOCS_URL = "https://pagescms.org/docs"; export function About() { + const t = useTranslations("AboutDialog"); const [open, setOpen] = useState(false); const [latestVersion, setLatestVersion] = useState(null); @@ -84,11 +86,11 @@ export function About() { - About Pages CMS + {t("title")} - About Pages CMS + {t("title")} @@ -106,15 +108,14 @@ export function About() { Pages CMS - - Open source CMS for static sites. Edit directly on GitHub with a - clean interface. + + {t("desc")}
{version} @@ -129,7 +130,7 @@ export function About() { variant="secondary" className="bg-primary/10 font-medium text-primary" > - Update to {latestVersion} + {t("updateTo")} {latestVersion} @@ -138,7 +139,7 @@ export function About() { } /> pagescms.org @@ -146,7 +147,7 @@ export function About() { } /> pagescms.org/docs @@ -154,7 +155,7 @@ export function About() { } /> pagescms/pagescms diff --git a/components/admin-button.tsx b/components/admin-button.tsx index 507e75122..6fe5177a5 100644 --- a/components/admin-button.tsx +++ b/components/admin-button.tsx @@ -2,17 +2,19 @@ import Link from "next/link"; import { Settings } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useUser } from "@/contexts/user-context"; import { Button } from "@/components/ui/button"; export function AdminButton() { const { user } = useUser(); + const t = useTranslations("AdminButton"); if (!user?.isAdmin) return null; return ( diff --git a/components/github-auth-expired.tsx b/components/github-auth-expired.tsx index 86433d1c4..16607e592 100644 --- a/components/github-auth-expired.tsx +++ b/components/github-auth-expired.tsx @@ -6,8 +6,10 @@ import { getSafeRedirect } from "@/lib/auth-redirect"; import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty"; import { Button } from "@/components/ui/button"; import { ArrowLeft, Loader } from "lucide-react"; +import { useTranslations } from "next-intl"; const GithubAuthExpired = () => { + const t = useTranslations("GithubAuthExpired"); const [loading, setLoading] = useState(false); const handleSignInAgain = async () => { @@ -28,11 +30,11 @@ const GithubAuthExpired = () => { return ( - GitHub session expired - Your GitHub session has expired. You'll need to sign in again. + {t("title")} + {t("desc")} diff --git a/components/invite-sign-in.tsx b/components/invite-sign-in.tsx index 1c84e81eb..1a5b42eb7 100644 --- a/components/invite-sign-in.tsx +++ b/components/invite-sign-in.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import { Loader } from "lucide-react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; import { authClient } from "@/lib/auth-client"; import { Button, buttonVariants } from "@/components/ui/button"; import { OtpVerificationForm } from "@/components/otp-verification-form"; @@ -28,6 +29,7 @@ type InviteState = }; export function InviteSignIn({ token }: { token: string }) { + const t = useTranslations("InviteSignIn"); const [state, setState] = useState({ status: "loading" }); const [otp, setOtp] = useState(""); const [pending, setPending] = useState(null); @@ -81,7 +83,7 @@ export function InviteSignIn({ token }: { token: string }) { toast.error(result.error.message); } } catch { - toast.error("Unable to send sign-in code."); + toast.error(t("unableToSendCode")); } finally { setPending(null); } @@ -90,7 +92,7 @@ export function InviteSignIn({ token }: { token: string }) { async function verifyOtp() { if (state.status !== "otp_required") return; if (otp.length !== 6) { - toast.error("Enter the 6-digit code."); + toast.error(t("enterCode")); return; } @@ -121,10 +123,10 @@ export function InviteSignIn({ token }: { token: string }) { return; } - toast.error("Unable to claim this invitation."); + toast.error(t("unableToClaim")); setPending(null); } catch { - toast.error("Unable to verify code."); + toast.error(t("unableToVerify")); setPending(null); } } @@ -143,12 +145,12 @@ export function InviteSignIn({ token }: { token: string }) { return ( - Invite unavailable - This invitation is no longer available. + {t("unavailableTitle")} + {t("unavailableDesc")} - Sign in + {t("signIn")} @@ -159,8 +161,8 @@ export function InviteSignIn({ token }: { token: string }) { return ( - Wrong account - This invitation was sent to another account. + {t("wrongAccountTitle")} + {t("wrongAccountDesc")} - Go home + {t("goHome")} diff --git a/components/language-switcher.tsx b/components/language-switcher.tsx new file mode 100644 index 000000000..796a26b0b --- /dev/null +++ b/components/language-switcher.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Globe } from "lucide-react"; +import { useLocale, useTranslations } from "next-intl"; +import { + LANGUAGE_COOKIE_KEY, + isLanguage, + SUPPORTED_LANGUAGES, +} from "@/i18n/constants"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const languageLabelKey = { + en: "en", + de: "de", + es: "es", + fr: "fr", +} as const; + +export function LanguageSwitcher() { + const router = useRouter(); + const locale = useLocale(); + const t = useTranslations("LanguageSwitcher"); + const language = isLanguage(locale) ? locale : "en"; + + return ( + + ); +} \ No newline at end of file diff --git a/components/otp-verification-form.tsx b/components/otp-verification-form.tsx index 1abdd30db..d7a783776 100644 --- a/components/otp-verification-form.tsx +++ b/components/otp-verification-form.tsx @@ -17,7 +17,11 @@ type OtpVerificationFormProps = { pending: boolean; resendDisabled?: boolean; resendPending?: boolean; + title?: string; + description?: string; submitLabel?: string; + resendLabel?: string; + signInAnotherWayLabel?: string; onChange: (value: string) => void; onResend: () => void; onSignInAnotherWay?: () => void; @@ -31,20 +35,26 @@ export function OtpVerificationForm({ pending, resendDisabled, resendPending, + title = "Verify your login", + description, submitLabel = "Verify code", + resendLabel = "Resend code", + signInAnotherWayLabel = "Sign in another way", onChange, onResend, onSignInAnotherWay, onSubmit, }: OtpVerificationFormProps) { + const resolvedDescription = description || `Enter the 6-digit code sent to ${emailLabel}.`; + return (

- Verify your login + {title}

- Enter the 6-digit code sent to {emailLabel}. + {resolvedDescription}

@@ -79,7 +89,7 @@ export function OtpVerificationForm({ type="button" variant="ghost" > - Resend code + {resendLabel} {resendPending && } {onSignInAnotherWay && ( @@ -90,7 +100,7 @@ export function OtpVerificationForm({ type="button" variant="ghost" > - Sign in another way + {signInAnotherWayLabel} )}
diff --git a/components/repo/repo-sidebar.tsx b/components/repo/repo-sidebar.tsx index 734eae9af..7de874cac 100644 --- a/components/repo/repo-sidebar.tsx +++ b/components/repo/repo-sidebar.tsx @@ -10,6 +10,7 @@ import { } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useConfig } from "@/contexts/config-context"; import { useRepo } from "@/contexts/repo-context"; import { useUser } from "@/contexts/user-context"; @@ -89,6 +90,7 @@ type NavigationNode = { }; function RepoSwitcher() { + const t = useTranslations("RepoSidebar"); const router = useRouter(); const { owner, repo, branches = [] } = useRepo(); const { config } = useConfig(); @@ -195,13 +197,13 @@ function RepoSwitcher() { target="_blank" rel="noreferrer" > - View on GitHub + {t("viewGithub")} - Branches + {t("branches")} - Manage branches + {t("manageBranches")} {recentRepos.length > 0 && ( <> - Recently visited + {t("recent")} {recentRepos.map((visit) => ( - All projects + {t("allProjects")}
- Manage branches + {t("manageBranches")} @@ -260,6 +262,7 @@ function RepoSwitcher() { } export function RepoSidebar() { + const t = useTranslations("RepoSidebar"); const pathname = usePathname(); const { user } = useUser(); const { config } = useConfig(); @@ -314,9 +317,9 @@ export function RepoSidebar() { return media.map((item: any) => ({ type: "media", name: item.name || "default", - label: item.label || item.name || "Media", + label: item.label || item.name || t("media"), })); - }, [config]); + }, [config, t]); const adminItems = useMemo(() => { if (!config) return []; @@ -329,7 +332,7 @@ export function RepoSidebar() { if (canManageRepo && isCacheEnabled(configObject)) { items.push({ key: "admin-cache", - label: "Cache", + label: t("cache"), href: `/${config.owner}/${config.repo}/${encodeURIComponent(config.branch)}/cache`, icon: , }); @@ -338,14 +341,14 @@ export function RepoSidebar() { if (canManageRepo) { items.push({ key: "admin-actions", - label: "Actions", + label: t("actions"), href: `/${config.owner}/${config.repo}/${encodeURIComponent(config.branch)}/actions`, icon: , }); items.push({ key: "admin-collaborators", - label: "Collaborators", + label: t("collaborators"), href: `/${config.owner}/${config.repo}/${encodeURIComponent(config.branch)}/collaborators`, icon: , }); @@ -354,14 +357,14 @@ export function RepoSidebar() { if (canManageRepo && isConfigEnabled(configObject)) { items.push({ key: "admin-configuration", - label: "Configuration", + label: t("configuration"), href: `/${config.owner}/${config.repo}/${encodeURIComponent(config.branch)}/configuration`, icon: , }); } return items; - }, [config, user]); + }, [config, t, user]); const rootActions = useMemo( () => getRootActions(config?.object), [config?.object], @@ -526,26 +529,26 @@ export function RepoSidebar() { ); } - const renderNavigationGroup = (label: string, nodes: NavigationNode[]) => { + const renderNavigationGroup = (key: string, label: string, nodes: NavigationNode[]) => { if (nodes.length === 0) return null; return ( - + {label} - {nodes.map((node) => renderNavigationNode(node, `${label}-${node.name}`))} + {nodes.map((node) => renderNavigationNode(node, `${key}-${node.name}`))} ); }; - const renderFlatGroup = (label: string, items: NavItem[]) => { + const renderFlatGroup = (key: string, label: string, items: NavItem[]) => { if (items.length === 0) return null; return ( - + {label} @@ -570,12 +573,12 @@ export function RepoSidebar() { }; const groups = [ - renderNavigationGroup("Content", contentNavigation), - renderNavigationGroup("Media", mediaNavigation), + renderNavigationGroup("content", t("content"), contentNavigation), + renderNavigationGroup("media", t("media"), mediaNavigation), rootActions.length > 0 && config ? ( - - Actions + + {t("actions")} ) : null, - renderFlatGroup("Admin", adminItems), + renderFlatGroup("admin", t("admin"), adminItems), ].filter(Boolean); return ( diff --git a/components/settings/identities.tsx b/components/settings/identities.tsx index 3a8e88c95..488f5aeb8 100644 --- a/components/settings/identities.tsx +++ b/components/settings/identities.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { signIn } from "@/lib/auth-client"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -35,6 +36,7 @@ export function Identities({ githubManageUrl, }: IdentitiesProps) { const router = useRouter(); + const t = useTranslations("Identities"); const [pendingAction, setPendingAction] = useState< "connect" | "disconnect" | null >(null); @@ -65,17 +67,17 @@ export function Identities({ const payload = await response.json().catch(() => null); if (!response.ok || !payload?.status) { const message = - payload?.message || "Failed to disconnect GitHub account."; + payload?.message || t("disconnectFailed"); throw new Error(message); } - toast.success("GitHub account disconnected."); + toast.success(t("disconnected")); router.refresh(); } catch (error) { const message = error instanceof Error ? error.message - : "Failed to disconnect GitHub account."; + : t("disconnectFailed"); toast.error(message); } finally { setPendingAction(null); @@ -87,7 +89,7 @@ export function Identities({
  • - Email + {t("email")}
    {email}
  • @@ -99,11 +101,11 @@ export function Identities({ )} > - GitHub + {t("github")}
    {githubConnected && (
    - {githubUsername ? `@${githubUsername}` : "Connected"} + {githubUsername ? `@${githubUsername}` : t("connected")}
    )} {!githubConnected ? ( @@ -114,7 +116,7 @@ export function Identities({ onClick={handleConnectGithub} disabled={pendingAction !== null} > - Connect + {t("connect")} {pendingAction === "connect" && ( )} @@ -133,7 +135,7 @@ export function Identities({ ) : ( )} - GitHub actions + {t("actions")} @@ -141,7 +143,7 @@ export function Identities({ <> - Manage on GitHub + {t("manage")} @@ -153,7 +155,7 @@ export function Identities({ onClick={handleDisconnectGithub} disabled={pendingAction !== null} > - Disconnect + {t("disconnect")} diff --git a/components/settings/installations.tsx b/components/settings/installations.tsx index c0d36624c..bb06bdf67 100644 --- a/components/settings/installations.tsx +++ b/components/settings/installations.tsx @@ -1,5 +1,6 @@ "use client"; +import { useTranslations } from "next-intl"; import { useUser } from "@/contexts/user-context"; import { Button } from "@/components/ui/button"; import { getGithubInstallationUrl } from "@/lib/github-app"; @@ -13,12 +14,13 @@ import { ArrowUpRight, Ban, EllipsisVertical } from "lucide-react"; const Installations = () => { const { user } = useUser(); + const t = useTranslations("Installations"); if (!user || !user.accounts) { return (
    - No account with the Github application installed. + {t("empty")}
    ); } @@ -42,7 +44,7 @@ const Installations = () => { @@ -52,7 +54,7 @@ const Installations = () => { target="_blank" rel="noreferrer" > - Manage GitHub App + {t("manage")} diff --git a/components/settings/profile.tsx b/components/settings/profile.tsx index ec6279ce9..55cc0f7f5 100644 --- a/components/settings/profile.tsx +++ b/components/settings/profile.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { toast } from "sonner"; import { getInitialsFromName } from "@/lib/utils/avatar"; import { @@ -30,6 +31,7 @@ type ProfileProps = { export function Profile({ name, email, githubUsername }: ProfileProps) { const router = useRouter(); + const t = useTranslations("ProfileCard"); const [displayName, setDisplayName] = useState(name?.trim() || ""); const [isSaving, setIsSaving] = useState(false); @@ -50,13 +52,16 @@ export function Profile({ name, email, githubUsername }: ProfileProps) { }); const payload = await response.json().catch(() => null); if (!response.ok || !payload?.status) { - throw new Error(payload?.message || "Failed to update profile."); + throw new Error(payload?.message || t("updateFailed")); } - toast.success("Profile updated."); + toast.success(t("updated")); router.refresh(); } catch (error) { - const message = error instanceof Error ? error.message : "Failed to update profile."; + const message = + error instanceof Error + ? error.message + : t("updateFailed"); toast.error(message); } finally { setIsSaving(false); @@ -66,8 +71,8 @@ export function Profile({ name, email, githubUsername }: ProfileProps) { return ( - Profile - Manage the information displayed to other users. + {t("title")} + {t("desc")}
    @@ -123,7 +128,7 @@ export function Profile({ name, email, githubUsername }: ProfileProps) { onClick={() => void handleSave()} disabled={!canSave} > - Save profile + {t("save")} {isSaving && } diff --git a/components/sign-in.tsx b/components/sign-in.tsx index f8ce4924c..425eb807e 100644 --- a/components/sign-in.tsx +++ b/components/sign-in.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { emailOtp, signIn } from "@/lib/auth-client"; import { getAuthCallbackURL, getSafeRedirect } from "@/lib/auth-redirect"; import { Input } from "@/components/ui/input"; @@ -11,6 +12,7 @@ import { toast } from "sonner"; import { Loader } from "lucide-react"; export function SignIn() { + const t = useTranslations("AuthPage"); const [email, setEmail] = useState(""); const [otp, setOtp] = useState(""); const [step, setStep] = useState<"email" | "otp">("email"); @@ -30,9 +32,12 @@ export function SignIn() { : `/sign-in?redirect=${encodeURIComponent(safeRedirect)}`; const getErrorMessage = (value: string) => { + if (value.toLowerCase() === "email_not_found") { + return t("githubEmailMissing"); + } if (value.toLowerCase() !== "unable_to_get_user_info") return value; return [ - "GitHub denied profile access. Re-authorize Pages CMS in GitHub Settings > Applications > Authorized GitHub Apps / Authorized OAuth Apps, then try again.", + t("githubDenied"), "https://github.com/settings/applications", ].join(" "); }; @@ -67,9 +72,9 @@ export function SignIn() { } setSubmittingMethod(null); - toast.error("Could not start GitHub sign-in. Please try again."); + toast.error(t("startGithubFailed")); } catch (error: any) { - toast.error(error?.message || "Could not start GitHub sign-in."); + toast.error(error?.message || t("startGithubFailed")); setSubmittingMethod(null); } }; @@ -77,7 +82,7 @@ export function SignIn() { const handleEmailSignIn = async () => { const normalizedEmail = email.trim().toLowerCase(); if (!normalizedEmail) { - toast.error("Invalid email"); + toast.error(t("invalidEmail")); return; } @@ -96,7 +101,7 @@ export function SignIn() { setEmail(normalizedEmail); setOtp(""); setStep("otp"); - toast.success("We sent you a sign-in code.", { duration: 8000 }); + toast.success(t("codeSent"), { duration: 8000 }); } finally { setSubmittingMethod(null); } @@ -104,7 +109,7 @@ export function SignIn() { const handleOtpSignIn = async () => { if (otp.length !== 6) { - toast.error("Enter the 6-digit code."); + toast.error(t("enterCode")); return; } @@ -133,21 +138,21 @@ export function SignIn() { const legalCopy = (

    - By clicking continue, you agree to our{" "} + {t("legalPrefix")}{" "} - Terms of Service + {t("terms")} {" "} - and{" "} + {t("and")}{" "} - Privacy Policy + {t("privacy")} .

    @@ -165,6 +170,11 @@ export function SignIn() { pending={submittingMethod === "otp"} resendDisabled={submittingMethod === "otp"} resendPending={submittingMethod === "email"} + submitLabel={t("verifyCode")} + title={t("otpTitle")} + description={t("otpDesc").replace("{email}", email)} + resendLabel={t("resendCode")} + signInAnotherWayLabel={t("signInAnotherWay")} onChange={setOtp} onResend={() => void handleEmailSignIn()} onSignInAnotherWay={resetToFullSignIn} @@ -178,7 +188,7 @@ export function SignIn() { ) : (

    - Sign in to Pages CMS + {t("title")}

    - Or + {t("or")}
    setEmail(event.target.value)} />