Add country localization

This commit is contained in:
Codex
2026-05-04 23:33:29 +01:00
parent 102967242e
commit d5c5f4fc17
13 changed files with 1297 additions and 123 deletions

101
messages/en-gb.json Normal file
View File

@@ -0,0 +1,101 @@
{
"Meta": {
"title": "Kairas | Web design studio for UK brands",
"description": "Kairas is a web design firm crafting refined websites, brand systems, and digital products for UK teams."
},
"Home": {
"nav": {
"studio": "Studio",
"services": "Services",
"work": "Work",
"notes": "Notes",
"contact": "hello@kairas.io"
},
"localeSwitcher": {
"label": "Country",
"options": {
"en-ie": "Ireland",
"en-gb": "United Kingdom",
"en-us": "United States"
}
},
"hero": {
"kicker": "Web design firm for UK brands",
"siteUrl": "kairas.io",
"words": ["Websites", "with quiet", "force."],
"interfaceLabel": "Selected interface",
"brandLabel": "Brand system",
"interfaceCopy": "Layouts tuned for calm reading and decisive action.",
"ticker": ["Strategy", "Design", "Build"],
"copy": "Kairas designs and builds refined websites for UK founders, studios, and service brands that care about taste, clarity, and commercial performance."
},
"marquee": ["Strategy", "Design", "Development", "Systems", "Launch"],
"studio": {
"eyebrow": "About us",
"title": "We shape digital homes for UK brands that need to feel exact, intentional, and easy to trust.",
"copy": "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."
},
"servicesTitle": "Services",
"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."
}
],
"work": {
"eyebrow": "Selected work",
"title": "Places where the brand can breathe.",
"items": [
["Atelier North", "Architecture portfolio", "2026"],
["Vellum Labs", "SaaS website", "2025"],
["Morrow House", "Hospitality booking", "2025"],
["Plainform", "Brand system", "2024"]
]
},
"process": {
"title": "Process",
"steps": [
{
"title": "Read",
"text": "We clarify the offer, audience, proof, and moments where a visitor needs confidence."
},
{
"title": "Compose",
"text": "We design the system: type, pacing, components, motion notes, and content hierarchy."
},
{
"title": "Ship",
"text": "We build responsive pages with practical handoff, analytics, and room to grow."
}
]
},
"notes": {
"title": "Notes",
"eyebrow": "Studio journal",
"items": [
["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"]
]
},
"footer": {
"brand": "Kairas",
"headline": "Build the site your brand has been waiting for.",
"siteUrl": "kairas.io",
"email": "hello@kairas.io",
"services": "Strategy / Design / Development",
"copyright": "© 2026 Kairas"
}
}
}

101
messages/en-ie.json Normal file
View File

@@ -0,0 +1,101 @@
{
"Meta": {
"title": "Kairas | Web design studio in Ireland",
"description": "Kairas is an Ireland-based web design firm crafting refined websites, brand systems, and digital products."
},
"Home": {
"nav": {
"studio": "Studio",
"services": "Services",
"work": "Work",
"notes": "Notes",
"contact": "hello@kairas.io"
},
"localeSwitcher": {
"label": "Country",
"options": {
"en-ie": "Ireland",
"en-gb": "United Kingdom",
"en-us": "United States"
}
},
"hero": {
"kicker": "Web design firm in Ireland",
"siteUrl": "kairas.io",
"words": ["Websites", "with quiet", "force."],
"interfaceLabel": "Selected interface",
"brandLabel": "Brand system",
"interfaceCopy": "Layouts tuned for calm reading and decisive action.",
"ticker": ["Strategy", "Design", "Build"],
"copy": "Kairas designs and builds refined websites for founders, studios, and service brands across Ireland that care about taste, clarity, and commercial performance."
},
"marquee": ["Strategy", "Design", "Development", "Systems", "Launch"],
"studio": {
"eyebrow": "About us",
"title": "We shape digital homes for Irish brands that need to feel exact, intentional, and easy to trust.",
"copy": "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."
},
"servicesTitle": "Services",
"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."
}
],
"work": {
"eyebrow": "Selected work",
"title": "Places where the brand can breathe.",
"items": [
["Atelier North", "Architecture portfolio", "2026"],
["Vellum Labs", "SaaS website", "2025"],
["Morrow House", "Hospitality booking", "2025"],
["Plainform", "Brand system", "2024"]
]
},
"process": {
"title": "Process",
"steps": [
{
"title": "Read",
"text": "We clarify the offer, audience, proof, and moments where a visitor needs confidence."
},
{
"title": "Compose",
"text": "We design the system: type, pacing, components, motion notes, and content hierarchy."
},
{
"title": "Ship",
"text": "We build responsive pages with practical handoff, analytics, and room to grow."
}
]
},
"notes": {
"title": "Notes",
"eyebrow": "Studio journal",
"items": [
["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"]
]
},
"footer": {
"brand": "Kairas",
"headline": "Build the site your brand has been waiting for.",
"siteUrl": "kairas.io",
"email": "hello@kairas.io",
"services": "Strategy / Design / Development",
"copyright": "© 2026 Kairas"
}
}
}

101
messages/en-us.json Normal file
View File

@@ -0,0 +1,101 @@
{
"Meta": {
"title": "Kairas | Web design studio for US teams",
"description": "Kairas is a web design firm crafting refined websites, brand systems, and digital products for US founders and service brands."
},
"Home": {
"nav": {
"studio": "Studio",
"services": "Services",
"work": "Work",
"notes": "Notes",
"contact": "hello@kairas.io"
},
"localeSwitcher": {
"label": "Country",
"options": {
"en-ie": "Ireland",
"en-gb": "United Kingdom",
"en-us": "United States"
}
},
"hero": {
"kicker": "Web design firm for US teams",
"siteUrl": "kairas.io",
"words": ["Websites", "with quiet", "force."],
"interfaceLabel": "Selected interface",
"brandLabel": "Brand system",
"interfaceCopy": "Layouts tuned for calm reading and decisive action.",
"ticker": ["Strategy", "Design", "Build"],
"copy": "Kairas designs and builds refined websites for US founders, studios, and service brands that care about taste, clarity, and commercial performance."
},
"marquee": ["Strategy", "Design", "Development", "Systems", "Launch"],
"studio": {
"eyebrow": "About us",
"title": "We shape digital homes for US teams that need to feel exact, intentional, and easy to trust.",
"copy": "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."
},
"servicesTitle": "Services",
"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."
}
],
"work": {
"eyebrow": "Selected work",
"title": "Places where the brand can breathe.",
"items": [
["Atelier North", "Architecture portfolio", "2026"],
["Vellum Labs", "SaaS website", "2025"],
["Morrow House", "Hospitality booking", "2025"],
["Plainform", "Brand system", "2024"]
]
},
"process": {
"title": "Process",
"steps": [
{
"title": "Read",
"text": "We clarify the offer, audience, proof, and moments where a visitor needs confidence."
},
{
"title": "Compose",
"text": "We design the system: type, pacing, components, motion notes, and content hierarchy."
},
{
"title": "Ship",
"text": "We build responsive pages with practical handoff, analytics, and room to grow."
}
]
},
"notes": {
"title": "Notes",
"eyebrow": "Studio journal",
"items": [
["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"]
]
},
"footer": {
"brand": "Kairas",
"headline": "Build the site your brand has been waiting for.",
"siteUrl": "kairas.io",
"email": "hello@kairas.io",
"services": "Strategy / Design / Development",
"copyright": "© 2026 Kairas"
}
}
}

9
middleware.ts Normal file
View File

@@ -0,0 +1,9 @@
import createMiddleware from "next-intl/middleware";
import {routing} from "./src/i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

View File

@@ -1,5 +1,8 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const nextConfig: NextConfig = {}; const nextConfig: NextConfig = {};
export default nextConfig; const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
export default withNextIntl(nextConfig);

709
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"animejs": "^4.4.1", "animejs": "^4.4.1",
"next": "latest", "next": "latest",
"next-intl": "^4.11.0",
"react": "latest", "react": "latest",
"react-dom": "latest" "react-dom": "latest"
}, },
@@ -467,6 +468,36 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@formatjs/fast-memoize": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.3.tgz",
"integrity": "sha512-Ocd1vPuD68rW6BJDuAOtnnc1GPeVepY5kZXML1psGVFQ+1Q8CfkftT3Tnam+Mxx97Pz08jIEDCotl/GV+Naccg==",
"license": "MIT"
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "3.5.6",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.6.tgz",
"integrity": "sha512-04ZjRIeQCnR/h32wBP9/S7rkyy1hLAs2fXJcNwc7hseJd//K9TMBqK0ukb4dXqnALKQ9m5ruZeOD2qqEkK9ixg==",
"license": "MIT",
"dependencies": {
"@formatjs/icu-skeleton-parser": "2.1.6"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.6.tgz",
"integrity": "sha512-9f1VQ2kaaLHK0WPU1OrAmiNKCKJwyoDmwNzQXbUa6XtFBOgHZ4YZURE8sSedHmMr0kvpB75OtplB0hMYkfdwfg==",
"license": "MIT"
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.5.tgz",
"integrity": "sha512-TEW/NR367c3PcQ2AXfkNig9jC740+qbkM0LgKl7UCE7Xtv7C5Uk1mvlu86MjQZBmscUai8HSWjcEETpwaVvJ6A==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "3.1.3"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -1254,6 +1285,313 @@
"node": ">=12.4.0" "node": ">=12.4.0"
} }
}, },
"node_modules/@parcel/watcher": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.6",
"@parcel/watcher-darwin-arm64": "2.5.6",
"@parcel/watcher-darwin-x64": "2.5.6",
"@parcel/watcher-freebsd-x64": "2.5.6",
"@parcel/watcher-linux-arm-glibc": "2.5.6",
"@parcel/watcher-linux-arm-musl": "2.5.6",
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
"@parcel/watcher-linux-arm64-musl": "2.5.6",
"@parcel/watcher-linux-x64-glibc": "2.5.6",
"@parcel/watcher-linux-x64-musl": "2.5.6",
"@parcel/watcher-win32-arm64": "2.5.6",
"@parcel/watcher-win32-ia32": "2.5.6",
"@parcel/watcher-win32-x64": "2.5.6"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1261,6 +1599,210 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@schummar/icu-type-parser": {
"version": "1.21.5",
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT"
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz",
"integrity": "sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz",
"integrity": "sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz",
"integrity": "sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz",
"integrity": "sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz",
"integrity": "sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-ppc64-gnu": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz",
"integrity": "sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-s390x-gnu": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz",
"integrity": "sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==",
"cpu": [
"s390x"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz",
"integrity": "sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz",
"integrity": "sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz",
"integrity": "sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz",
"integrity": "sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz",
"integrity": "sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1270,6 +1812,15 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@swc/types": {
"version": "0.1.26",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz",
"integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz",
@@ -2825,7 +3376,6 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -3926,6 +4476,21 @@
"hermes-estree": "0.25.1" "hermes-estree": "0.25.1"
} }
}, },
"node_modules/icu-minify": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.11.0.tgz",
"integrity": "sha512-XRvblCwLqWXio5ZLcmDqXvJv7alSACK6UjXuuMOdQWB//d25AQX6xlVlI1FEbc3Q6iPLXXo6HaVLn8LcAFhn1Q==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^3.4.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3978,6 +4543,16 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/intl-messageformat": {
"version": "11.2.3",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.3.tgz",
"integrity": "sha512-kZthTU+3WLcoWoRg5j6LOkN1TeUBtmkX0OIwSAbcHVIfQAEbGVdmANM8u6GL3eUDOqLwheYoXMUshAh1UdeXlQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/fast-memoize": "3.1.3",
"@formatjs/icu-messageformat-parser": "3.5.6"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -4140,7 +4715,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -4186,7 +4760,6 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
@@ -4984,6 +5557,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/next": { "node_modules/next": {
"version": "16.2.4", "version": "16.2.4",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz",
@@ -5037,6 +5619,94 @@
} }
} }
}, },
"node_modules/next-intl": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.11.0.tgz",
"integrity": "sha512-Chp8rgEVUYOX/bCtYy+PXH6lDX3X+GPT9sR9HScHroL283em/4urP9btfdHEMEHJJXdq2W/5wDaDDtWONPdNSA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "^0.8.1",
"@parcel/watcher": "^2.4.1",
"@swc/core": "^1.15.2",
"icu-minify": "^4.11.0",
"negotiator": "^1.0.0",
"next-intl-swc-plugin-extractor": "^4.11.0",
"po-parser": "^2.1.1",
"use-intl": "^4.11.0"
},
"peerDependencies": {
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/next-intl-swc-plugin-extractor": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.11.0.tgz",
"integrity": "sha512-WUGBSxGNd8eQ0rAsJHFmRw2H7+SZAXQIY/HAnYM57JaUsj5D2vx4KOz4zFtXlyKDtsw9awHfgWVvBae2/RDF9A==",
"license": "MIT"
},
"node_modules/next-intl/node_modules/@swc/core": {
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz",
"integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.26"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.33",
"@swc/core-darwin-x64": "1.15.33",
"@swc/core-linux-arm-gnueabihf": "1.15.33",
"@swc/core-linux-arm64-gnu": "1.15.33",
"@swc/core-linux-arm64-musl": "1.15.33",
"@swc/core-linux-ppc64-gnu": "1.15.33",
"@swc/core-linux-s390x-gnu": "1.15.33",
"@swc/core-linux-x64-gnu": "1.15.33",
"@swc/core-linux-x64-musl": "1.15.33",
"@swc/core-win32-arm64-msvc": "1.15.33",
"@swc/core-win32-ia32-msvc": "1.15.33",
"@swc/core-win32-x64-msvc": "1.15.33"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/next-intl/node_modules/@swc/helpers": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",
"integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -5065,6 +5735,12 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-exports-info": { "node_modules/node-exports-info": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -5341,6 +6017,12 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/po-parser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
"integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==",
"license": "MIT"
},
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -6434,6 +7116,27 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-intl": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.11.0.tgz",
"integrity": "sha512-7ILhTLuo3fnSKhoTGDk5X9591pjtWr6qB4inrlvGkN9OEyKhoiG73GZFoLSs68wz3BsSGtoWa62iWvrYEYU+iA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "^3.1.0",
"@schummar/icu-type-parser": "1.21.5",
"icu-minify": "^4.11.0",
"intl-messageformat": "^11.1.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"animejs": "^4.4.1", "animejs": "^4.4.1",
"next": "latest", "next": "latest",
"next-intl": "^4.11.0",
"react": "latest", "react": "latest",
"react-dom": "latest" "react-dom": "latest"
}, },

View 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>
);
}

View File

@@ -2,40 +2,36 @@
import {useEffect, useRef} from "react"; import {useEffect, useRef} from "react";
import {animate, createTimeline, stagger} from "animejs"; 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 = [ type Service = {
{ number: string;
number: "01", title: string;
title: "Brand-led websites", text: string;
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.",
},
];
const work = [ type WorkItem = [string, string, string];
["Atelier North", "Architecture portfolio", "2026"], type NoteItem = [string, string, string];
["Vellum Labs", "SaaS website", "2025"],
["Morrow House", "Hospitality booking", "2025"],
["Plainform", "Brand system", "2024"],
];
const notes = [ type ProcessStep = {
["2026.04.18", "Essay", "Designing quieter conversion paths for premium service brands"], title: string;
["2026.03.02", "Studio", "Kairas opens a focused website sprint for early-stage teams"], text: string;
["2026.01.14", "Guide", "What belongs above the fold when the work is the proof"], };
];
export default function Home() { export default function Home() {
const rootRef = useRef<HTMLElement>(null); 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(() => { useEffect(() => {
const root = rootRef.current; const root = rootRef.current;
@@ -207,7 +203,7 @@ export default function Home() {
observer.disconnect(); observer.disconnect();
animations.forEach((animation) => animation.revert()); animations.forEach((animation) => animation.revert());
}; };
}, []); }, [locale]);
return ( return (
<main <main
@@ -221,40 +217,92 @@ export default function Home() {
<div className="intro-mask bg-[var(--navy)]" /> <div className="intro-mask bg-[var(--navy)]" />
</div> </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"> <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"> <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">
<a href="#" className="nav-item text-lg font-semibold tracking-[0.18em]"> <Link href="/" className="nav-item text-lg font-semibold tracking-[0.18em]">
Kairas Kairas
</a> </Link>
<div className="hidden items-center gap-8 md:flex"> <div className="hidden items-center gap-8 md:flex">
<a className="nav-item" href="#studio">Studio</a> <a className="nav-item" href="#studio">
<a className="nav-item" href="#services">Services</a> {t("nav.studio")}
<a className="nav-item" href="#work">Work</a> </a>
<a className="nav-item" href="#notes">Notes</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> </div>
<a <a
href="mailto:hello@kairas.io" 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)]" 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> </a>
</nav> </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> </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="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)]"> <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 <a
href="https://kairas.io" href="https://kairas.io"
className="font-semibold italic tracking-normal" className="font-semibold italic tracking-normal"
> >
kairas.io {t("hero.siteUrl")}
</a> </a>
</div> </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]"> <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> {heroWords.map((word) => (
<span className="hero-word motion-hidden">with quiet</span> <span key={word} className="hero-word motion-hidden">
<span className="hero-word motion-hidden">force.</span> {word}
</span>
))}
</h1> </h1>
</div> </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="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="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]"> <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> <span>01 / 04</span>
</div> </div>
<div className="grid grid-cols-[1fr_0.72fr] gap-3 p-4"> <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)]"> <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]"> <span className="text-xs uppercase tracking-[0.18em] text-[#d9ccb8]">
Brand system {t("hero.brandLabel")}
</span> </span>
<div> <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]"> <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> </p>
</div> </div>
</div> </div>
@@ -290,16 +340,16 @@ export default function Home() {
</div> </div>
</div> </div>
<div className="flex items-center justify-between border-t border-[var(--line)] px-4 py-3 text-xs uppercase tracking-[0.16em]"> <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> {tickerItems.map((item) => (
<span className="ticker-item">Design</span> <span key={item} className="ticker-item">
<span className="ticker-item">Build</span> {item}
</span>
))}
</div> </div>
</div> </div>
</div> </div>
<p className="hero-copy motion-hidden max-w-xl text-xl leading-9 text-[var(--navy-soft)]"> <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, {t("hero.copy")}
and service brands that care about taste, clarity, and commercial
performance.
</p> </p>
</div> </div>
</section> </section>
@@ -308,15 +358,9 @@ export default function Home() {
<div className="marquee-track text-4xl italic leading-none sm:text-6xl"> <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"> <div key={group} className="flex shrink-0 items-center gap-8 pr-8">
<span>Strategy</span> {marqueeItems.map((item) => (
<span>/</span> <span key={`${group}-${item}`}>{item}</span>
<span>Design</span> ))}
<span>/</span>
<span>Development</span>
<span>/</span>
<span>Systems</span>
<span>/</span>
<span>Launch</span>
<span>/</span> <span>/</span>
</div> </div>
))} ))}
@@ -329,24 +373,29 @@ export default function Home() {
className="motion-hidden relative z-10 border-y border-[var(--line)] bg-[var(--paper)]" 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"> <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> <div>
<h2 className="max-w-4xl text-4xl font-semibold leading-tight sm:text-6xl"> <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, {t("studio.title")}
intentional, and easy to trust.
</h2> </h2>
<p className="mt-8 max-w-2xl text-lg leading-8 text-[var(--ink-muted)]"> <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 {t("studio.copy")}
strong typography, generous pacing, and disciplined systems to
make every page feel composed without becoming static.
</p> </p>
</div> </div>
</div> </div>
</section> </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"> <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> <span className="text-sm uppercase tracking-[0.16em]">03</span>
</div> </div>
<div className="grid gap-0 border-t border-[var(--line)]"> <div className="grid gap-0 border-t border-[var(--line)]">
@@ -355,7 +404,9 @@ export default function Home() {
key={service.title} 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]" 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> <h3 className="text-3xl font-semibold">{service.title}</h3>
<p className="max-w-2xl text-lg leading-8 text-[var(--ink-muted)]"> <p className="max-w-2xl text-lg leading-8 text-[var(--ink-muted)]">
{service.text} {service.text}
@@ -366,14 +417,18 @@ export default function Home() {
</div> </div>
</section> </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 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> <div>
<p className="text-sm uppercase tracking-[0.16em] text-[#d9ccb8]"> <p className="text-sm uppercase tracking-[0.16em] text-[#d9ccb8]">
Selected work {t("work.eyebrow")}
</p> </p>
<h2 className="mt-6 max-w-lg text-5xl font-semibold leading-none sm:text-7xl"> <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> </h2>
</div> </div>
<div className="grid content-start border-t border-[rgba(247,241,231,0.24)]"> <div className="grid content-start border-t border-[rgba(247,241,231,0.24)]">
@@ -396,34 +451,40 @@ export default function Home() {
</div> </div>
</section> </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"> <section
<h2 className="text-5xl font-semibold sm:text-7xl">Process</h2> 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"> <div className="grid gap-8 md:grid-cols-3">
{["Read", "Compose", "Ship"].map((step, index) => ( {processSteps.map((step, index) => (
<div key={step} className="border-t border-[var(--line)] pt-5"> <div key={step.title} className="border-t border-[var(--line)] pt-5">
<span className="text-sm tracking-[0.16em]"> <span className="text-sm tracking-[0.16em]">
{String(index + 1).padStart(2, "0")} {String(index + 1).padStart(2, "0")}
</span> </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)]"> <p className="mt-4 leading-7 text-[var(--ink-muted)]">
{index === 0 && {step.text}
"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."}
</p> </p>
</div> </div>
))} ))}
</div> </div>
</section> </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 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> <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)]"> <p className="mt-5 text-sm uppercase tracking-[0.16em] text-[var(--ink-muted)]">
Studio journal {t("notes.eyebrow")}
</p> </p>
</div> </div>
<div className="border-t border-[var(--line)]"> <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" 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> <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"> <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> </h2>
</div> </div>
<div className="grid content-between gap-10 text-sm uppercase tracking-[0.14em] lg:text-right"> <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="https://kairas.io">{t("footer.siteUrl")}</a>
<a href="mailto:hello@kairas.io">hello@kairas.io</a> <a href="mailto:hello@kairas.io">{t("footer.email")}</a>
<p>Strategy / Design / Development</p> <p>{t("footer.services")}</p>
<p>© 2026 Kairas</p> <p>{t("footer.copyright")}</p>
</div> </div>
</footer> </footer>
</main> </main>

View File

@@ -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
View 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
View 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
View 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];