Add country localization
This commit is contained in:
80
src/app/[locale]/layout.tsx
Normal file
80
src/app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
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";
|
||||
|
||||
type LocaleLayoutProps = Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: Promise<{locale: string}>;
|
||||
}>;
|
||||
|
||||
type LocaleMessages = {
|
||||
Meta: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
|
||||
const alternates = Object.fromEntries([
|
||||
...routing.locales.map((locale) => [locale, `/${locale}`]),
|
||||
["x-default", `/${routing.defaultLocale}`],
|
||||
]);
|
||||
|
||||
async function getLocaleMessages(locale: Locale): Promise<LocaleMessages> {
|
||||
return (await import(`../../../messages/${locale}.json`)).default;
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({locale}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: Pick<LocaleLayoutProps, "params">): Promise<Metadata> {
|
||||
const {locale} = await params;
|
||||
|
||||
if (!hasLocale(routing.locales, locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const messages = await getLocaleMessages(locale);
|
||||
|
||||
return {
|
||||
metadataBase: new URL("https://kairas.io"),
|
||||
title: messages.Meta.title,
|
||||
description: messages.Meta.description,
|
||||
alternates: {
|
||||
canonical: `/${locale}`,
|
||||
languages: alternates,
|
||||
},
|
||||
openGraph: {
|
||||
title: messages.Meta.title,
|
||||
description: messages.Meta.description,
|
||||
url: `/${locale}`,
|
||||
siteName: "Kairas",
|
||||
locale: locale.replace("-", "_"),
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: LocaleLayoutProps) {
|
||||
const {locale} = await params;
|
||||
|
||||
if (!hasLocale(routing.locales, locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang={locale.split("-")[0]}>
|
||||
<body>
|
||||
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { animate, createTimeline, stagger } from "animejs";
|
||||
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";
|
||||
|
||||
const services = [
|
||||
{
|
||||
number: "01",
|
||||
title: "Brand-led websites",
|
||||
text: "Identity, UX, interface design, and front-end systems for companies that need their site to carry the weight of the brand.",
|
||||
},
|
||||
{
|
||||
number: "02",
|
||||
title: "Editorial product pages",
|
||||
text: "Launch pages, case studies, and content structures that make complex offers feel considered, useful, and easy to move through.",
|
||||
},
|
||||
{
|
||||
number: "03",
|
||||
title: "Design systems",
|
||||
text: "Reusable components, visual rules, and interaction patterns that help teams ship new pages without losing quality.",
|
||||
},
|
||||
];
|
||||
type Service = {
|
||||
number: string;
|
||||
title: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const work = [
|
||||
["Atelier North", "Architecture portfolio", "2026"],
|
||||
["Vellum Labs", "SaaS website", "2025"],
|
||||
["Morrow House", "Hospitality booking", "2025"],
|
||||
["Plainform", "Brand system", "2024"],
|
||||
];
|
||||
type WorkItem = [string, string, string];
|
||||
type NoteItem = [string, string, string];
|
||||
|
||||
const notes = [
|
||||
["2026.04.18", "Essay", "Designing quieter conversion paths for premium service brands"],
|
||||
["2026.03.02", "Studio", "Kairas opens a focused website sprint for early-stage teams"],
|
||||
["2026.01.14", "Guide", "What belongs above the fold when the work is the proof"],
|
||||
];
|
||||
type ProcessStep = {
|
||||
title: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const rootRef = useRef<HTMLElement>(null);
|
||||
const locale = useLocale() as Locale;
|
||||
const t = useTranslations("Home");
|
||||
|
||||
const heroWords = t.raw("hero.words") as string[];
|
||||
const tickerItems = t.raw("hero.ticker") as string[];
|
||||
const marqueeItems = t.raw("marquee") as string[];
|
||||
const services = t.raw("services") as Service[];
|
||||
const work = t.raw("work.items") as WorkItem[];
|
||||
const processSteps = t.raw("process.steps") as ProcessStep[];
|
||||
const notes = t.raw("notes.items") as NoteItem[];
|
||||
|
||||
useEffect(() => {
|
||||
const root = rootRef.current;
|
||||
@@ -179,7 +175,7 @@ export default function Home() {
|
||||
observer.unobserve(target);
|
||||
});
|
||||
},
|
||||
{ threshold: 0.18 },
|
||||
{threshold: 0.18},
|
||||
);
|
||||
|
||||
root.querySelectorAll("[data-reveal]").forEach((element) => {
|
||||
@@ -207,7 +203,7 @@ export default function Home() {
|
||||
observer.disconnect();
|
||||
animations.forEach((animation) => animation.revert());
|
||||
};
|
||||
}, []);
|
||||
}, [locale]);
|
||||
|
||||
return (
|
||||
<main
|
||||
@@ -221,40 +217,92 @@ export default function Home() {
|
||||
<div className="intro-mask bg-[var(--navy)]" />
|
||||
</div>
|
||||
<header className="fixed left-0 right-0 top-0 z-20 border-b border-[var(--line)] bg-[rgba(238,228,212,0.86)] backdrop-blur">
|
||||
<nav className="mx-auto flex max-w-[1500px] items-center justify-between px-5 py-4 text-[13px] uppercase leading-none tracking-[0.12em] sm:px-8 lg:px-12">
|
||||
<a href="#" className="nav-item text-lg font-semibold tracking-[0.18em]">
|
||||
<nav className="mx-auto flex max-w-[1500px] items-center justify-between gap-4 px-5 py-4 text-[13px] uppercase leading-none tracking-[0.12em] sm:px-8 lg:px-12">
|
||||
<Link href="/" className="nav-item text-lg font-semibold tracking-[0.18em]">
|
||||
Kairas
|
||||
</a>
|
||||
</Link>
|
||||
<div className="hidden items-center gap-8 md:flex">
|
||||
<a className="nav-item" href="#studio">Studio</a>
|
||||
<a className="nav-item" href="#services">Services</a>
|
||||
<a className="nav-item" href="#work">Work</a>
|
||||
<a className="nav-item" href="#notes">Notes</a>
|
||||
<a className="nav-item" href="#studio">
|
||||
{t("nav.studio")}
|
||||
</a>
|
||||
<a className="nav-item" href="#services">
|
||||
{t("nav.services")}
|
||||
</a>
|
||||
<a className="nav-item" href="#work">
|
||||
{t("nav.work")}
|
||||
</a>
|
||||
<a className="nav-item" href="#notes">
|
||||
{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>
|
||||
<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)]"
|
||||
>
|
||||
hello@kairas.io
|
||||
{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">
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<section className="relative z-10 mx-auto grid min-h-screen max-w-[1500px] content-end gap-12 px-5 pb-12 pt-28 sm:px-8 md:pt-36 lg:grid-cols-[1.05fr_0.95fr] lg:px-12">
|
||||
<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">
|
||||
<div className="flex flex-col justify-end gap-8">
|
||||
<div className="hero-kicker flex max-w-sm flex-wrap items-baseline gap-x-3 gap-y-1 text-sm leading-6 text-[var(--navy-soft)]">
|
||||
<span className="uppercase tracking-[0.14em]">Web design firm</span>
|
||||
<span className="uppercase tracking-[0.14em]">
|
||||
{t("hero.kicker")}
|
||||
</span>
|
||||
<a
|
||||
href="https://kairas.io"
|
||||
className="font-semibold italic tracking-normal"
|
||||
>
|
||||
kairas.io
|
||||
{t("hero.siteUrl")}
|
||||
</a>
|
||||
</div>
|
||||
<h1 className="max-w-5xl overflow-hidden text-7xl font-semibold leading-[0.86] tracking-normal sm:text-8xl md:text-9xl lg:text-[10rem] xl:text-[12rem]">
|
||||
<span className="hero-word motion-hidden">Websites</span>
|
||||
<span className="hero-word motion-hidden">with quiet</span>
|
||||
<span className="hero-word motion-hidden">force.</span>
|
||||
{heroWords.map((word) => (
|
||||
<span key={word} className="hero-word motion-hidden">
|
||||
{word}
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -262,18 +310,20 @@ export default function Home() {
|
||||
<div className="hero-card motion-hidden relative min-h-[420px] overflow-hidden border border-[var(--line)] bg-[var(--paper)] p-4 shadow-[0_18px_60px_rgba(8,27,51,0.08)]">
|
||||
<div className="grid h-full min-h-[388px] grid-rows-[auto_1fr_auto] border border-[var(--line)] bg-[var(--beige)]">
|
||||
<div className="flex items-center justify-between border-b border-[var(--line)] px-4 py-3 text-xs uppercase tracking-[0.16em]">
|
||||
<span>Selected interface</span>
|
||||
<span>{t("hero.interfaceLabel")}</span>
|
||||
<span>01 / 04</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[1fr_0.72fr] gap-3 p-4">
|
||||
<div className="float-piece flex flex-col justify-between bg-[var(--navy)] p-5 text-[var(--paper)]">
|
||||
<span className="text-xs uppercase tracking-[0.18em] text-[#d9ccb8]">
|
||||
Brand system
|
||||
{t("hero.brandLabel")}
|
||||
</span>
|
||||
<div>
|
||||
<p className="spin-mark inline-block text-5xl font-semibold leading-none">KA</p>
|
||||
<p className="spin-mark inline-block text-5xl font-semibold leading-none">
|
||||
KA
|
||||
</p>
|
||||
<p className="mt-3 max-w-[14rem] text-sm leading-6 text-[#d9ccb8]">
|
||||
Layouts tuned for calm reading and decisive action.
|
||||
{t("hero.interfaceCopy")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,33 +340,27 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t border-[var(--line)] px-4 py-3 text-xs uppercase tracking-[0.16em]">
|
||||
<span className="ticker-item">Strategy</span>
|
||||
<span className="ticker-item">Design</span>
|
||||
<span className="ticker-item">Build</span>
|
||||
{tickerItems.map((item) => (
|
||||
<span key={item} className="ticker-item">
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="hero-copy motion-hidden max-w-xl text-xl leading-9 text-[var(--navy-soft)]">
|
||||
Kairas designs and builds refined websites for founders, studios,
|
||||
and service brands that care about taste, clarity, and commercial
|
||||
performance.
|
||||
{t("hero.copy")}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="marquee relative z-10 border-y border-[var(--line)] bg-[var(--navy)] py-5 text-[var(--paper)]">
|
||||
<div className="marquee-track text-4xl italic leading-none sm:text-6xl">
|
||||
{Array.from({ length: 2 }).map((_, group) => (
|
||||
{Array.from({length: 2}).map((_, group) => (
|
||||
<div key={group} className="flex shrink-0 items-center gap-8 pr-8">
|
||||
<span>Strategy</span>
|
||||
<span>/</span>
|
||||
<span>Design</span>
|
||||
<span>/</span>
|
||||
<span>Development</span>
|
||||
<span>/</span>
|
||||
<span>Systems</span>
|
||||
<span>/</span>
|
||||
<span>Launch</span>
|
||||
{marqueeItems.map((item) => (
|
||||
<span key={`${group}-${item}`}>{item}</span>
|
||||
))}
|
||||
<span>/</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -329,24 +373,29 @@ export default function Home() {
|
||||
className="motion-hidden relative z-10 border-y border-[var(--line)] bg-[var(--paper)]"
|
||||
>
|
||||
<div className="mx-auto grid max-w-[1500px] gap-10 px-5 py-20 sm:px-8 lg:grid-cols-[0.7fr_1.3fr] lg:px-12 lg:py-28">
|
||||
<p className="text-sm uppercase tracking-[0.16em]">About us</p>
|
||||
<p className="text-sm uppercase tracking-[0.16em]">
|
||||
{t("studio.eyebrow")}
|
||||
</p>
|
||||
<div>
|
||||
<h2 className="max-w-4xl text-4xl font-semibold leading-tight sm:text-6xl">
|
||||
We shape digital homes for brands that need to feel exact,
|
||||
intentional, and easy to trust.
|
||||
{t("studio.title")}
|
||||
</h2>
|
||||
<p className="mt-8 max-w-2xl text-lg leading-8 text-[var(--ink-muted)]">
|
||||
Our work sits between design studio and front-end craft. We use
|
||||
strong typography, generous pacing, and disciplined systems to
|
||||
make every page feel composed without becoming static.
|
||||
{t("studio.copy")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="services" data-reveal className="motion-hidden relative z-10 mx-auto max-w-[1500px] px-5 py-20 sm:px-8 lg:px-12 lg:py-28">
|
||||
<section
|
||||
id="services"
|
||||
data-reveal
|
||||
className="motion-hidden relative z-10 mx-auto max-w-[1500px] px-5 py-20 sm:px-8 lg:px-12 lg:py-28"
|
||||
>
|
||||
<div className="mb-12 flex items-end justify-between border-b border-[var(--line)] pb-6">
|
||||
<h2 className="text-5xl font-semibold sm:text-7xl">Services</h2>
|
||||
<h2 className="text-5xl font-semibold sm:text-7xl">
|
||||
{t("servicesTitle")}
|
||||
</h2>
|
||||
<span className="text-sm uppercase tracking-[0.16em]">03</span>
|
||||
</div>
|
||||
<div className="grid gap-0 border-t border-[var(--line)]">
|
||||
@@ -355,7 +404,9 @@ export default function Home() {
|
||||
key={service.title}
|
||||
className="grid gap-6 border-b border-[var(--line)] py-9 md:grid-cols-[0.22fr_0.78fr] lg:grid-cols-[0.18fr_0.38fr_0.44fr]"
|
||||
>
|
||||
<span className="text-sm tracking-[0.16em]">{service.number}</span>
|
||||
<span className="text-sm tracking-[0.16em]">
|
||||
{service.number}
|
||||
</span>
|
||||
<h3 className="text-3xl font-semibold">{service.title}</h3>
|
||||
<p className="max-w-2xl text-lg leading-8 text-[var(--ink-muted)]">
|
||||
{service.text}
|
||||
@@ -366,14 +417,18 @@ export default function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="work" data-reveal className="motion-hidden relative z-10 bg-[var(--navy)] text-[var(--paper)]">
|
||||
<section
|
||||
id="work"
|
||||
data-reveal
|
||||
className="motion-hidden relative z-10 bg-[var(--navy)] text-[var(--paper)]"
|
||||
>
|
||||
<div className="mx-auto grid max-w-[1500px] gap-12 px-5 py-20 sm:px-8 lg:grid-cols-[0.42fr_0.58fr] lg:px-12 lg:py-28">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[0.16em] text-[#d9ccb8]">
|
||||
Selected work
|
||||
{t("work.eyebrow")}
|
||||
</p>
|
||||
<h2 className="mt-6 max-w-lg text-5xl font-semibold leading-none sm:text-7xl">
|
||||
Places where the brand can breathe.
|
||||
{t("work.title")}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid content-start border-t border-[rgba(247,241,231,0.24)]">
|
||||
@@ -396,34 +451,40 @@ export default function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section data-reveal className="motion-hidden relative z-10 mx-auto grid max-w-[1500px] gap-10 px-5 py-20 sm:px-8 lg:grid-cols-[0.35fr_0.65fr] lg:px-12 lg:py-28">
|
||||
<h2 className="text-5xl font-semibold sm:text-7xl">Process</h2>
|
||||
<section
|
||||
data-reveal
|
||||
className="motion-hidden relative z-10 mx-auto grid max-w-[1500px] gap-10 px-5 py-20 sm:px-8 lg:grid-cols-[0.35fr_0.65fr] lg:px-12 lg:py-28"
|
||||
>
|
||||
<h2 className="text-5xl font-semibold sm:text-7xl">
|
||||
{t("process.title")}
|
||||
</h2>
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{["Read", "Compose", "Ship"].map((step, index) => (
|
||||
<div key={step} className="border-t border-[var(--line)] pt-5">
|
||||
{processSteps.map((step, index) => (
|
||||
<div key={step.title} className="border-t border-[var(--line)] pt-5">
|
||||
<span className="text-sm tracking-[0.16em]">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<h3 className="mt-8 text-3xl font-semibold">{step}</h3>
|
||||
<h3 className="mt-8 text-3xl font-semibold">{step.title}</h3>
|
||||
<p className="mt-4 leading-7 text-[var(--ink-muted)]">
|
||||
{index === 0 &&
|
||||
"We clarify the offer, audience, proof, and moments where a visitor needs confidence."}
|
||||
{index === 1 &&
|
||||
"We design the system: type, pacing, components, motion notes, and content hierarchy."}
|
||||
{index === 2 &&
|
||||
"We build responsive pages with practical handoff, analytics, and room to grow."}
|
||||
{step.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="notes" data-reveal className="motion-hidden relative z-10 border-y border-[var(--line)] bg-[var(--paper)]">
|
||||
<section
|
||||
id="notes"
|
||||
data-reveal
|
||||
className="motion-hidden relative z-10 border-y border-[var(--line)] bg-[var(--paper)]"
|
||||
>
|
||||
<div className="mx-auto grid max-w-[1500px] gap-10 px-5 py-20 sm:px-8 lg:grid-cols-[0.35fr_0.65fr] lg:px-12 lg:py-28">
|
||||
<div>
|
||||
<h2 className="text-5xl font-semibold sm:text-7xl">Notes</h2>
|
||||
<h2 className="text-5xl font-semibold sm:text-7xl">
|
||||
{t("notes.title")}
|
||||
</h2>
|
||||
<p className="mt-5 text-sm uppercase tracking-[0.16em] text-[var(--ink-muted)]">
|
||||
Studio journal
|
||||
{t("notes.eyebrow")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-t border-[var(--line)]">
|
||||
@@ -450,16 +511,18 @@ export default function Home() {
|
||||
className="motion-hidden relative z-10 mx-auto grid max-w-[1500px] gap-12 px-5 py-16 sm:px-8 lg:grid-cols-[1fr_auto] lg:px-12"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[0.16em]">Kairas</p>
|
||||
<p className="text-sm uppercase tracking-[0.16em]">
|
||||
{t("footer.brand")}
|
||||
</p>
|
||||
<h2 className="mt-5 max-w-4xl text-5xl font-semibold leading-none sm:text-8xl">
|
||||
Build the site your brand has been waiting for.
|
||||
{t("footer.headline")}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid content-between gap-10 text-sm uppercase tracking-[0.14em] lg:text-right">
|
||||
<a href="https://kairas.io">kairas.io</a>
|
||||
<a href="mailto:hello@kairas.io">hello@kairas.io</a>
|
||||
<p>Strategy / Design / Development</p>
|
||||
<p>© 2026 Kairas</p>
|
||||
<a href="https://kairas.io">{t("footer.siteUrl")}</a>
|
||||
<a href="mailto:hello@kairas.io">{t("footer.email")}</a>
|
||||
<p>{t("footer.services")}</p>
|
||||
<p>{t("footer.copyright")}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://kairas.io"),
|
||||
title: "Kairas | Web design studio",
|
||||
description:
|
||||
"Kairas is a web design firm crafting refined websites, brand systems, and digital products.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
6
src/i18n/navigation.ts
Normal file
6
src/i18n/navigation.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import {createNavigation} from "next-intl/navigation";
|
||||
import {routing} from "./routing";
|
||||
|
||||
export const {Link, redirect, usePathname, useRouter, getPathname} =
|
||||
createNavigation(routing);
|
||||
|
||||
16
src/i18n/request.ts
Normal file
16
src/i18n/request.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {getRequestConfig} from "next-intl/server";
|
||||
import {hasLocale} from "next-intl";
|
||||
import {routing} from "./routing";
|
||||
|
||||
export default getRequestConfig(async ({requestLocale}) => {
|
||||
const requested = await requestLocale;
|
||||
const locale = hasLocale(routing.locales, requested)
|
||||
? requested
|
||||
: routing.defaultLocale;
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../../messages/${locale}.json`)).default,
|
||||
};
|
||||
});
|
||||
|
||||
11
src/i18n/routing.ts
Normal file
11
src/i18n/routing.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {defineRouting} from "next-intl/routing";
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ["en-ie", "en-gb", "en-us"],
|
||||
defaultLocale: "en-ie",
|
||||
localePrefix: "always",
|
||||
localeDetection: true,
|
||||
});
|
||||
|
||||
export type Locale = (typeof routing.locales)[number];
|
||||
|
||||
Reference in New Issue
Block a user