feat(i18n): add language switcher
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
"use server"
|
||||
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
import {
|
||||
isLocale,
|
||||
LOCALE_COOKIE_MAX_AGE_SECONDS,
|
||||
LOCALE_COOKIE_NAME,
|
||||
type Locale,
|
||||
} from "@/i18n/locales"
|
||||
|
||||
export type SetLocaleActionResult =
|
||||
| { success: true; locale: Locale }
|
||||
| { success: false; error: "UNSUPPORTED_LOCALE" }
|
||||
|
||||
export async function setLocaleAction(
|
||||
requestedLocale: string,
|
||||
): Promise<SetLocaleActionResult> {
|
||||
if (!isLocale(requestedLocale)) {
|
||||
return { success: false, error: "UNSUPPORTED_LOCALE" }
|
||||
}
|
||||
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set(LOCALE_COOKIE_NAME, requestedLocale, {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
maxAge: LOCALE_COOKIE_MAX_AGE_SECONDS,
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
})
|
||||
|
||||
return { success: true, locale: requestedLocale }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { LanguageSwitcher } from "@/components/i18n/language-switcher"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { getI18n } from "@/i18n/server"
|
||||
import { auth } from "@/lib/auth"
|
||||
@@ -11,12 +12,18 @@ export default async function LoginPage() {
|
||||
|
||||
if (session) redirect("/")
|
||||
|
||||
const { dictionary } = await getI18n()
|
||||
const { dictionary, locale } = await getI18n()
|
||||
const copy = dictionary.login
|
||||
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="w-full max-w-sm space-y-3">
|
||||
<div className="flex justify-end">
|
||||
<LanguageSwitcher
|
||||
activeLocale={locale}
|
||||
copy={dictionary.common.languageSwitcher}
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useTransition } from "react"
|
||||
|
||||
import { setLocaleAction } from "@/actions/i18n.actions"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { isLocale, type Locale, SUPPORTED_LOCALES } from "@/i18n/locales"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type LanguageSwitcherCopy = {
|
||||
label: string
|
||||
options: Record<Locale, string>
|
||||
}
|
||||
|
||||
type LanguageSwitcherProps = {
|
||||
activeLocale: Locale
|
||||
copy: LanguageSwitcherCopy
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function LanguageSwitcher({
|
||||
activeLocale,
|
||||
copy,
|
||||
className,
|
||||
}: LanguageSwitcherProps) {
|
||||
const router = useRouter()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const activeLabel = copy.options[activeLocale]
|
||||
const accessibleName = `${copy.label}: ${activeLabel}`
|
||||
|
||||
function handleLocaleChange(nextLocale: string) {
|
||||
if (isPending || nextLocale === activeLocale || !isLocale(nextLocale)) {
|
||||
return
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await setLocaleAction(nextLocale)
|
||||
|
||||
if (result.success) {
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={accessibleName}
|
||||
className={cn("min-w-12", className)}
|
||||
disabled={isPending}
|
||||
size="xs"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{activeLocale.toUpperCase()}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{copy.label}</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup
|
||||
onValueChange={handleLocaleChange}
|
||||
value={activeLocale}
|
||||
>
|
||||
{SUPPORTED_LOCALES.map((locale) => (
|
||||
<DropdownMenuRadioItem
|
||||
disabled={isPending}
|
||||
key={locale}
|
||||
value={locale}
|
||||
>
|
||||
{copy.options[locale]}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { LanguageSwitcher } from "@/components/i18n/language-switcher"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { getI18n } from "@/i18n/server"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { SIGN_IN_URL } from "@/lib/constants"
|
||||
|
||||
@@ -20,11 +22,17 @@ export default async function Navbar() {
|
||||
|
||||
if (!session) redirect(SIGN_IN_URL)
|
||||
|
||||
const { dictionary, locale } = await getI18n()
|
||||
|
||||
return (
|
||||
<nav className="flex items-center justify-between border-b p-4">
|
||||
<SidebarTrigger />
|
||||
<div className="flex items-center gap-4">
|
||||
<AddMenu />
|
||||
<LanguageSwitcher
|
||||
activeLocale={locale}
|
||||
copy={dictionary.common.languageSwitcher}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
export const en = {
|
||||
common: {
|
||||
languageSwitcher: {
|
||||
label: "Language",
|
||||
options: {
|
||||
en: "English",
|
||||
es: "Spanish",
|
||||
},
|
||||
},
|
||||
},
|
||||
login: {
|
||||
title: "Sign In",
|
||||
usernameLabel: "Username",
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import type { Dictionary } from "./en"
|
||||
|
||||
export const es = {
|
||||
common: {
|
||||
languageSwitcher: {
|
||||
label: "Idioma",
|
||||
options: {
|
||||
en: "Inglés",
|
||||
es: "Español",
|
||||
},
|
||||
},
|
||||
},
|
||||
login: {
|
||||
title: "Iniciar sesión",
|
||||
usernameLabel: "Usuario",
|
||||
|
||||
@@ -5,6 +5,7 @@ export type Locale = (typeof SUPPORTED_LOCALES)[number]
|
||||
export const FALLBACK_LOCALE: Locale = "en"
|
||||
export const DEFAULT_LOCALE_ENV_VAR = "STOCK_MANAGER_DEFAULT_LOCALE"
|
||||
export const LOCALE_COOKIE_NAME = "stock-manager-locale"
|
||||
export const LOCALE_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365
|
||||
|
||||
export function isLocale(value: unknown): value is Locale {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user