diff --git a/messages/README-LANGUAGES.md b/messages/README-LANGUAGES.md new file mode 100644 index 0000000..ec15c0b --- /dev/null +++ b/messages/README-LANGUAGES.md @@ -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. + + + + + + + diff --git a/messages/de.json b/messages/de.json new file mode 100644 index 0000000..a862be3 --- /dev/null +++ b/messages/de.json @@ -0,0 +1,7 @@ +{ + "navigation": { + "getting-started": "Erste Schritte", + "donate": "Spenden", + "useful-links": "Nützliche Links" + } +} \ No newline at end of file diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..e0a7a18 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,7 @@ +{ + "navigation": { + "getting-started": "Getting Started", + "donate": "Donate", + "useful-links": "Useful Links" + } +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 47cc272..7f48842 100644 --- a/next.config.js +++ b/next.config.js @@ -1,7 +1,10 @@ +const createNextIntlPlugin = require('next-intl/plugin'); const { PHASE_DEVELOPMENT_SERVER } = require('next/constants') - + +const withNextIntl = createNextIntlPlugin(); + /** @type {import('next').NextConfig} */ -module.exports = (phase, { defaultConfig }) => { +const nextConfig = (phase, { defaultConfig }) => { const defaultConfigWWW = { images: { remotePatterns: [ @@ -38,4 +41,5 @@ module.exports = (phase, { defaultConfig }) => { output: 'export', }; }; - + +module.exports = withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index 00fa56f..3c0d111 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "framer-motion": "^11.3.24", "lucide-react": "^0.400.0", "next": "14.2.4", + "next-intl": "^3.18.1", "next-themes": "^0.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -2455,6 +2456,55 @@ "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==", "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": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -8604,6 +8654,18 @@ "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": { "version": "2.2.4", "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", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", - "peer": true, "engines": { "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": { "version": "0.3.0", "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", diff --git a/package.json b/package.json index fa53fd6..4709265 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "framer-motion": "^11.3.24", "lucide-react": "^0.400.0", "next": "14.2.4", + "next-intl": "^3.18.1", "next-themes": "^0.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fd2a59b..e68eabb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,8 @@ import { Inter } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; import StyledComponentsRegistry from "@/lib/styled-components-registry"; +import {NextIntlClientProvider} from 'next-intl'; +import {getLocale, getMessages} from 'next-intl/server'; const inter = Inter({ subsets: ["latin"] }); @@ -12,26 +14,32 @@ export const metadata: Metadata = { keywords: ["Zen", "Browser", "Zen Browser", "Web", "Internet", "Fast"], }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const locale = await getLocale(); + + const messages = await getMessages(); + return ( - + - - {children} - + + + {children} + + ); diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index 92ba5c1..e99420f 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -18,6 +18,7 @@ import { ModeToggle } from "./mode-toggle" import { MobileNav } from "./mobile-nav" import { HeartIcon } from "lucide-react" import { HeartFilledIcon } from "@radix-ui/react-icons" +import { useTranslations } from "next-intl"; 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() { + + const t = useTranslations('navigation'); + return (
@@ -59,7 +63,7 @@ export function Navigation() { - Getting started + {t('getting-started')}
  • @@ -94,7 +98,7 @@ export function Navigation() { - Donate + {t('donate')}
      @@ -114,7 +118,7 @@ export function Navigation() { - Useful Links + {t('useful-links')}
        {components.map((component) => ( diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..a2fcfed --- /dev/null +++ b/src/i18n.ts @@ -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, + }; + } +});