Merge pull request #134 from ktz-dev/internationalization
feat: Multi-language support
This commit is contained in:
18
messages/README-LANGUAGES.md
Normal file
18
messages/README-LANGUAGES.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Contributing Translations
|
||||||
|
|
||||||
|
To contribute to the translation of your language you must modify the json in `/messages` that is named corresponding to the ISO Language Code of your given language.
|
||||||
|
|
||||||
|
If you do not see a JSON for your language then add the language.
|
||||||
|
|
||||||
|
## Adding a language
|
||||||
|
|
||||||
|
1. To add a language you must add the language to the `const SUPPORTED_LANGUAGES = ['en', 'de'];` variable in the `./src/i18n.ts` file.
|
||||||
|
2. You must create a new `.json` file in the `./messages` directory
|
||||||
|
3. Copy the contents of the `en.json` file, make your way down the key-value pairs and change **only the values** to the translated equivalent.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
7
messages/de.json
Normal file
7
messages/de.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"navigation": {
|
||||||
|
"getting-started": "Erste Schritte",
|
||||||
|
"donate": "Spenden",
|
||||||
|
"useful-links": "Nützliche Links"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
messages/en.json
Normal file
7
messages/en.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"navigation": {
|
||||||
|
"getting-started": "Getting Started",
|
||||||
|
"donate": "Donate",
|
||||||
|
"useful-links": "Useful Links"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
const createNextIntlPlugin = require('next-intl/plugin');
|
||||||
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
|
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
module.exports = (phase, { defaultConfig }) => {
|
const nextConfig = (phase, { defaultConfig }) => {
|
||||||
const defaultConfigWWW = {
|
const defaultConfigWWW = {
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
@@ -39,3 +42,4 @@ module.exports = (phase, { defaultConfig }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = withNextIntl(nextConfig);
|
||||||
|
|||||||
97
package-lock.json
generated
97
package-lock.json
generated
@@ -31,6 +31,7 @@
|
|||||||
"framer-motion": "^11.3.24",
|
"framer-motion": "^11.3.24",
|
||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"next": "14.2.4",
|
"next": "14.2.4",
|
||||||
|
"next-intl": "^3.18.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -2455,6 +2456,55 @@
|
|||||||
"integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==",
|
"integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@formatjs/ecma402-abstract": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "0.5.4",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/fast-memoize": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||||
|
"version": "2.7.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz",
|
||||||
|
"integrity": "sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "2.0.0",
|
||||||
|
"@formatjs/icu-skeleton-parser": "1.8.2",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "2.0.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
|
||||||
|
"integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@hapi/hoek": {
|
"node_modules/@hapi/hoek": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||||
@@ -8604,6 +8654,18 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/intl-messageformat": {
|
||||||
|
"version": "10.5.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz",
|
||||||
|
"integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "2.0.0",
|
||||||
|
"@formatjs/fast-memoize": "2.2.0",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.7.8",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/invariant": {
|
"node_modules/invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
@@ -11328,7 +11390,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@@ -11390,6 +11451,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-intl": {
|
||||||
|
"version": "3.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.18.1.tgz",
|
||||||
|
"integrity": "sha512-ht8HyroJeiJIte9yhg1f0Nc2rlZmkvSYQ3nhqFVJLzhq7T1Xb8nfjilffrOJc3sA8kEjBOS4bdIrg4YX8REO0Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/amannn"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "^0.5.4",
|
||||||
|
"negotiator": "^0.6.3",
|
||||||
|
"use-intl": "^3.18.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-themes": {
|
"node_modules/next-themes": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
|
||||||
@@ -15068,6 +15150,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-intl": {
|
||||||
|
"version": "3.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.18.1.tgz",
|
||||||
|
"integrity": "sha512-BFNhVnszG1AB04DbNvJ+TLLd1oBDGergAKI8t9xaE4vDJYZaVKQH4zmpdArbegzTu5U9XMen6w14d1P1hBwKOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/fast-memoize": "^2.2.0",
|
||||||
|
"intl-messageformat": "^10.5.14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sidecar": {
|
"node_modules/use-sidecar": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"framer-motion": "^11.3.24",
|
"framer-motion": "^11.3.24",
|
||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"next": "14.2.4",
|
"next": "14.2.4",
|
||||||
|
"next-intl": "^3.18.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Inter } from "next/font/google";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import StyledComponentsRegistry from "@/lib/styled-components-registry";
|
import StyledComponentsRegistry from "@/lib/styled-components-registry";
|
||||||
|
import {NextIntlClientProvider} from 'next-intl';
|
||||||
|
import {getLocale, getMessages} from 'next-intl/server';
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
@@ -12,26 +14,32 @@ export const metadata: Metadata = {
|
|||||||
keywords: ["Zen", "Browser", "Zen Browser", "Web", "Internet", "Fast"],
|
keywords: ["Zen", "Browser", "Zen Browser", "Web", "Internet", "Fast"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const locale = await getLocale();
|
||||||
|
|
||||||
|
const messages = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<link rel="me" href="https://fosstodon.org/@zenbrowser"></link>
|
<link rel="me" href="https://fosstodon.org/@zenbrowser"></link>
|
||||||
<link rel="alternate" type="application/rss+xml" title="Zen Browser Release Notes" href="https://www.zen-browser.app/feed.xml" />
|
<link rel="alternate" type="application/rss+xml" title="Zen Browser Release Notes" href="https://www.zen-browser.app/feed.xml" />
|
||||||
</head>
|
</head>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<ThemeProvider
|
<NextIntlClientProvider messages={messages}>
|
||||||
attribute="class"
|
<ThemeProvider
|
||||||
defaultTheme="dark"
|
attribute="class"
|
||||||
enableSystem
|
defaultTheme="dark"
|
||||||
disableTransitionOnChange
|
enableSystem
|
||||||
>
|
disableTransitionOnChange
|
||||||
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
|
>
|
||||||
</ThemeProvider>
|
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
|
||||||
|
</ThemeProvider>
|
||||||
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { ModeToggle } from "./mode-toggle"
|
|||||||
import { MobileNav } from "./mobile-nav"
|
import { MobileNav } from "./mobile-nav"
|
||||||
import { HeartIcon } from "lucide-react"
|
import { HeartIcon } from "lucide-react"
|
||||||
import { HeartFilledIcon } from "@radix-ui/react-icons"
|
import { HeartFilledIcon } from "@radix-ui/react-icons"
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export const components: { title: string; href: string; description: string }[] = [
|
export const components: { title: string; href: string; description: string }[] = [
|
||||||
{
|
{
|
||||||
@@ -48,6 +49,9 @@ export const components: { title: string; href: string; description: string }[]
|
|||||||
]
|
]
|
||||||
|
|
||||||
export function Navigation() {
|
export function Navigation() {
|
||||||
|
|
||||||
|
const t = useTranslations('navigation');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background z-40 top-0 left-0 w-full flex fixed border-b border-grey p-2 items-center justify-center">
|
<div className="bg-background z-40 top-0 left-0 w-full flex fixed border-b border-grey p-2 items-center justify-center">
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
@@ -59,7 +63,7 @@ export function Navigation() {
|
|||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
|
<NavigationMenuTrigger>{t('getting-started')}</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
|
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
|
||||||
<li className="row-span-3">
|
<li className="row-span-3">
|
||||||
@@ -94,7 +98,7 @@ export function Navigation() {
|
|||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuTrigger>
|
<NavigationMenuTrigger>
|
||||||
<HeartFilledIcon className="text-red-500" />
|
<HeartFilledIcon className="text-red-500" />
|
||||||
<span className="ml-2">Donate</span>
|
<span className="ml-2">{t('donate')}</span>
|
||||||
</NavigationMenuTrigger>
|
</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
||||||
@@ -114,7 +118,7 @@ export function Navigation() {
|
|||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuTrigger>Useful Links</NavigationMenuTrigger>
|
<NavigationMenuTrigger>{t('useful-links')}</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
||||||
{components.map((component) => (
|
{components.map((component) => (
|
||||||
|
|||||||
35
src/i18n.ts
Normal file
35
src/i18n.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { getRequestConfig } from "next-intl/server";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
|
const SUPPORTED_LANGUAGES = ['en', 'de'];
|
||||||
|
|
||||||
|
export default getRequestConfig(async () => {
|
||||||
|
const headersList = headers();
|
||||||
|
|
||||||
|
const acceptLanguage = headersList.get("accept-language") || "en";
|
||||||
|
|
||||||
|
const [primaryLanguage] = acceptLanguage
|
||||||
|
.split(",")
|
||||||
|
.map((lang) => lang.split(";")[0])
|
||||||
|
.map((lang) => lang.toLowerCase());
|
||||||
|
|
||||||
|
const locale = SUPPORTED_LANGUAGES.includes(primaryLanguage) ? primaryLanguage : 'en';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messages = (await import(`../messages/${locale}.json`)).default;
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load messages for locale: ${locale}`, error);
|
||||||
|
|
||||||
|
const fallbackMessages = (await import(`../messages/en.json`)).default;
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale: "en",
|
||||||
|
messages: fallbackMessages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user