Add language dropdown localization

This commit is contained in:
Codex
2026-05-05 00:19:17 +01:00
parent d5c5f4fc17
commit f6ace49f2f
17 changed files with 552 additions and 261 deletions

View File

@@ -2,7 +2,7 @@ import type {Metadata} from "next";
import {NextIntlClientProvider, hasLocale} from "next-intl";
import {notFound} from "next/navigation";
import "../globals.css";
import {routing, type Locale} from "@/i18n/routing";
import {localeDirections, routing, type Locale} from "@/i18n/routing";
type LocaleLayoutProps = Readonly<{
children: React.ReactNode;
@@ -70,11 +70,10 @@ export default async function LocaleLayout({
}
return (
<html lang={locale.split("-")[0]}>
<html lang={locale} dir={localeDirections[locale]}>
<body>
<NextIntlClientProvider>{children}</NextIntlClientProvider>
</body>
</html>
);
}

View File

@@ -3,8 +3,8 @@
import {useEffect, useRef} from "react";
import {animate, createTimeline, stagger} from "animejs";
import {useLocale, useTranslations} from "next-intl";
import {Link} from "@/i18n/navigation";
import {routing, type Locale} from "@/i18n/routing";
import {Link, useRouter} from "@/i18n/navigation";
import {localeNames, routing, type Locale} from "@/i18n/routing";
type Service = {
number: string;
@@ -23,6 +23,7 @@ type ProcessStep = {
export default function Home() {
const rootRef = useRef<HTMLElement>(null);
const locale = useLocale() as Locale;
const router = useRouter();
const t = useTranslations("Home");
const heroWords = t.raw("hero.words") as string[];
@@ -235,25 +236,25 @@ export default function Home() {
{t("nav.notes")}
</a>
</div>
<div className="nav-item hidden items-center gap-2 lg:flex">
<span className="sr-only">{t("localeSwitcher.label")}</span>
{routing.locales.map((option) => (
<Link
key={option}
href="/"
locale={option}
aria-current={option === locale ? "page" : undefined}
style={option === locale ? {color: "var(--paper)"} : undefined}
className={`border px-3 py-2 transition ${
option === locale
? "border-[var(--navy)] bg-[var(--navy)] text-[var(--paper)]"
: "border-[var(--line)] hover:border-[var(--navy)]"
}`}
>
{option.split("-")[1].toUpperCase()}
</Link>
))}
</div>
<label className="nav-item hidden items-center gap-3 lg:flex">
<span className="text-[var(--ink-muted)]">
{t("localeSwitcher.label")}
</span>
<select
value={locale}
aria-label={t("localeSwitcher.label")}
onChange={(event) => {
router.replace("/", {locale: event.target.value as Locale});
}}
className="min-w-40 border border-[var(--line)] bg-transparent px-3 py-2 text-[var(--navy)] outline-none transition hover:border-[var(--navy)] focus:border-[var(--navy)]"
>
{routing.locales.map((option) => (
<option key={option} value={option}>
{localeNames[option]}
</option>
))}
</select>
</label>
<a
href="mailto:hello@kairas.io"
className="nav-item border border-[var(--navy)] px-4 py-3 transition hover:bg-[var(--navy)] hover:text-[var(--paper)]"
@@ -261,27 +262,25 @@ export default function Home() {
{t("nav.contact")}
</a>
</nav>
<div className="flex items-center justify-center gap-2 border-t border-[var(--line)] px-5 py-2 text-[11px] uppercase leading-none tracking-[0.12em] lg:hidden">
<label className="flex items-center justify-center gap-3 border-t border-[var(--line)] px-5 py-2 text-[11px] uppercase leading-none tracking-[0.12em] lg:hidden">
<span className="text-[var(--ink-muted)]">
{t("localeSwitcher.label")}
</span>
{routing.locales.map((option) => (
<Link
key={option}
href="/"
locale={option}
aria-current={option === locale ? "page" : undefined}
style={option === locale ? {color: "var(--paper)"} : undefined}
className={`border px-3 py-2 transition ${
option === locale
? "border-[var(--navy)] bg-[var(--navy)] text-[var(--paper)]"
: "border-[var(--line)] hover:border-[var(--navy)]"
}`}
>
{option.split("-")[1].toUpperCase()}
</Link>
))}
</div>
<select
value={locale}
aria-label={t("localeSwitcher.label")}
onChange={(event) => {
router.replace("/", {locale: event.target.value as Locale});
}}
className="min-w-36 border border-[var(--line)] bg-transparent px-3 py-2 text-[var(--navy)] outline-none"
>
{routing.locales.map((option) => (
<option key={option} value={option}>
{localeNames[option]}
</option>
))}
</select>
</label>
</header>
<section className="relative z-10 mx-auto grid min-h-screen max-w-[1500px] content-end gap-12 px-5 pb-12 pt-40 sm:px-8 md:pt-36 lg:grid-cols-[1.05fr_0.95fr] lg:px-12">

View File

@@ -2,15 +2,43 @@ import {getRequestConfig} from "next-intl/server";
import {hasLocale} from "next-intl";
import {routing} from "./routing";
type Messages = Record<string, unknown>;
function mergeMessages(base: Messages, override: Messages): Messages {
const merged: Messages = {...base};
for (const [key, value] of Object.entries(override)) {
const baseValue = merged[key];
if (
value &&
baseValue &&
typeof value === "object" &&
typeof baseValue === "object" &&
!Array.isArray(value) &&
!Array.isArray(baseValue)
) {
merged[key] = mergeMessages(baseValue as Messages, value as Messages);
} else {
merged[key] = value;
}
}
return merged;
}
export default getRequestConfig(async ({requestLocale}) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
const defaultMessages = (await import("../../messages/en.json")).default;
const localeMessages =
locale === routing.defaultLocale
? defaultMessages
: (await import(`../../messages/${locale}.json`)).default;
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
messages: mergeMessages(defaultMessages, localeMessages),
};
});

View File

@@ -1,11 +1,36 @@
import {defineRouting} from "next-intl/routing";
export const routing = defineRouting({
locales: ["en-ie", "en-gb", "en-us"],
defaultLocale: "en-ie",
locales: ["en", "zh", "hi", "es", "fr", "ar", "bn", "pt", "ru", "ur"],
defaultLocale: "en",
localePrefix: "always",
localeDetection: true,
});
export type Locale = (typeof routing.locales)[number];
export const localeNames: Record<Locale, string> = {
en: "English",
zh: "中文",
hi: "हिन्दी",
es: "Español",
fr: "Français",
ar: "العربية",
bn: "বাংলা",
pt: "Português",
ru: "Русский",
ur: "اردو",
};
export const localeDirections: Record<Locale, "ltr" | "rtl"> = {
en: "ltr",
zh: "ltr",
hi: "ltr",
es: "ltr",
fr: "ltr",
ar: "rtl",
bn: "ltr",
pt: "ltr",
ru: "ltr",
ur: "rtl",
};