From 386958c28b5ee412ef3c02fda413afa11f4db3dc Mon Sep 17 00:00:00 2001 From: Florian Hoss Date: Mon, 15 Jun 2026 18:15:59 +0200 Subject: [PATCH 1/5] chore(i18n): set up next-intl app-router configuration --- app/layout.tsx | 14 +- i18n/constants.ts | 9 + i18n/request.ts | 26 ++ messages/de.json | 107 ++++++ messages/en.json | 107 ++++++ next.config.mjs | 4 +- package-lock.json | 803 +++++++++++++++++++++++++++++++++++++++++----- package.json | 1 + 8 files changed, 986 insertions(+), 85 deletions(-) create mode 100644 i18n/constants.ts create mode 100644 i18n/request.ts create mode 100644 messages/de.json create mode 100644 messages/en.json diff --git a/app/layout.tsx b/app/layout.tsx index a8906bba1..a43a8a373 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,7 @@ import { Toaster } from "@/components/ui/sonner" import { Providers } from "@/components/providers"; +import { NextIntlClientProvider } from "next-intl"; +import { getLocale } from "next-intl/server"; import type { Metadata } from "next"; import { Inter, JetBrains_Mono } from "next/font/google"; import { getBaseUrl } from "@/lib/base-url"; @@ -57,8 +59,10 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const locale = await getLocale(); + return ( - + - - {children} - + + + {children} + + diff --git a/i18n/constants.ts b/i18n/constants.ts new file mode 100644 index 000000000..137f5bbcf --- /dev/null +++ b/i18n/constants.ts @@ -0,0 +1,9 @@ +export const LANGUAGE_COOKIE_KEY = "locale"; + +export const SUPPORTED_LANGUAGES = ["en", "de"] as const; + +export type Language = (typeof SUPPORTED_LANGUAGES)[number]; + +export function isLanguage(value: string): value is Language { + return (SUPPORTED_LANGUAGES as readonly string[]).includes(value); +} diff --git a/i18n/request.ts b/i18n/request.ts new file mode 100644 index 000000000..bf361d19b --- /dev/null +++ b/i18n/request.ts @@ -0,0 +1,26 @@ +import { cookies } from "next/headers"; +import { getRequestConfig } from "next-intl/server"; +import { + isLanguage, + LANGUAGE_COOKIE_KEY, + SUPPORTED_LANGUAGES, + type Language, +} from "./constants"; + +export { + isLanguage, + LANGUAGE_COOKIE_KEY, + SUPPORTED_LANGUAGES, + type Language, +}; + +export default getRequestConfig(async () => { + const store = await cookies(); + const cookieLocale = store.get(LANGUAGE_COOKIE_KEY)?.value; + const locale = cookieLocale && isLanguage(cookieLocale) ? cookieLocale : "en"; + + return { + locale, + messages: (await import(`../messages/${locale}.json`)).default, + }; +}); diff --git a/messages/de.json b/messages/de.json new file mode 100644 index 000000000..33f98c6a1 --- /dev/null +++ b/messages/de.json @@ -0,0 +1,107 @@ +{ + "LanguageSwitcher": { + "label": "Sprache", + "en": "Englisch", + "de": "Deutsch" + }, + "Loading": { + "text": "Lädt..." + }, + "HomePage": { + "title": "Projekte", + "recent": "Zuletzt besucht", + "open": "Projekt öffnen", + "template": "Aus einer Vorlage erstellen", + "installTitle": "GitHub-App installieren", + "installDesc": "Installiere die GitHub-App auf mindestens einem Konto, bevor du Projekte öffnen oder erstellen kannst.", + "installCta": "GitHub-App installieren", + "noneTitle": "Noch keine Repositories", + "noneDesc": "Du brauchst eine Einladung zu einem Repository, bevor du mitarbeiten kannst. Bitte eine Repository-Eigentümerin, einen Repository-Eigentümer oder eine Organisations-Administratorin bzw. einen Organisations-Administrator um eine Einladung." + }, + "SettingsPage": { + "back": "Zur Startseite", + "title": "Einstellungen", + "authTitle": "Authentifizierung", + "authDesc": "Deine Anmeldemethoden und verknüpften Identitätsanbieter.", + "instTitle": "Installationen", + "instDesc": "Verwalte die Konten, auf denen die GitHub-Anwendung installiert ist." + }, + "ProfileCard": { + "title": "Profil", + "desc": "Verwalte die Informationen, die anderen Benutzerinnen und Benutzern angezeigt werden.", + "name": "Name", + "picture": "Bild", + "save": "Profil speichern", + "updated": "Profil aktualisiert.", + "updateFailed": "Profil konnte nicht aktualisiert werden." + }, + "Identities": { + "email": "E-Mail", + "github": "GitHub", + "connected": "Verbunden", + "connect": "Verbinden", + "disconnect": "Trennen", + "actions": "GitHub-Aktionen", + "manage": "Auf GitHub verwalten", + "disconnectFailed": "GitHub-Konto konnte nicht getrennt werden.", + "disconnected": "GitHub-Konto getrennt." + }, + "Installations": { + "empty": "Kein Konto mit installierter GitHub-Anwendung gefunden.", + "actions": "Installationsaktionen", + "manage": "GitHub-App verwalten" + }, + "AboutDialog": { + "title": "Über Pages CMS", + "desc": "Open-Source-CMS für statische Websites. Bearbeite Inhalte direkt auf GitHub mit einer klaren Oberfläche.", + "version": "Version", + "website": "Website", + "docs": "Dokumentation", + "github": "GitHub", + "updateTo": "Aktualisieren auf" + }, + "RepoSidebar": { + "viewGithub": "Auf GitHub ansehen", + "branches": "Branches", + "manageBranches": "Branches verwalten", + "allProjects": "Alle Projekte", + "content": "Inhalt", + "media": "Medien", + "recent": "Zuletzt besucht", + "actions": "Aktionen", + "admin": "Admin", + "cache": "Cache", + "collaborators": "Mitwirkende", + "configuration": "Konfiguration" + }, + "AuthPage": { + "title": "Bei Pages CMS anmelden", + "github": "Mit GitHub anmelden", + "or": "Oder", + "emailPlaceholder": "E-Mail", + "continueEmail": "Mit E-Mail fortfahren", + "invalidEmail": "Ungueltige E-Mail", + "codeSent": "Wir haben dir einen Anmeldecode gesendet.", + "enterCode": "Gib den 6-stelligen Code ein.", + "startGithubFailed": "GitHub-Anmeldung konnte nicht gestartet werden. Bitte versuche es erneut.", + "legalPrefix": "Mit Klick auf Fortfahren stimmst du unseren", + "terms": "Nutzungsbedingungen", + "and": "und", + "privacy": "Datenschutzbestimmungen", + "githubEmailMissing": "GitHub hat fuer dieses Konto keine E-Mail-Adresse geliefert. Starte fuer lokale Entwicklung die App neu und versuche es erneut (E-Mail-Fallback ist aktiviert), oder stelle sicher, dass die App die E-Mail-Berechtigung hat und autorisiere sie in den GitHub-Einstellungen > Applications erneut.", + "githubDenied": "GitHub hat den Profilzugriff verweigert. Autorisiere Pages CMS in den GitHub-Einstellungen > Applications > Authorized GitHub Apps / Authorized OAuth Apps erneut und versuche es dann noch einmal.", + "otpTitle": "Anmeldung bestaetigen", + "otpDesc": "Gib den 6-stelligen Code ein, der an {email} gesendet wurde.", + "verifyCode": "Code bestaetigen", + "resendCode": "Code erneut senden", + "signInAnotherWay": "Anders anmelden" + }, + "UserMenu": { + "theme": "Farbschema", + "light": "Hell", + "dark": "Dunkel", + "system": "System", + "settings": "Einstellungen", + "signOut": "Abmelden" + } +} diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 000000000..ce11f7cf6 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,107 @@ +{ + "LanguageSwitcher": { + "label": "Language", + "en": "English", + "de": "German" + }, + "Loading": { + "text": "Loading..." + }, + "HomePage": { + "title": "Projects", + "recent": "Recently visited", + "open": "Open a project", + "template": "Create from a template", + "installTitle": "Install the GitHub App", + "installDesc": "Install the GitHub App on at least one account before you can open or create projects.", + "installCta": "Install GitHub App", + "noneTitle": "No repositories yet", + "noneDesc": "You need an invitation to a repository before you can collaborate. Ask a repository owner or organization admin to invite you." + }, + "SettingsPage": { + "back": "Go home", + "title": "Settings", + "authTitle": "Authentication", + "authDesc": "Your sign-in methods and linked identity providers.", + "instTitle": "Installations", + "instDesc": "Manage the accounts the GitHub application is installed on." + }, + "ProfileCard": { + "title": "Profile", + "desc": "Manage the information displayed to other users.", + "name": "Name", + "picture": "Picture", + "save": "Save profile", + "updated": "Profile updated.", + "updateFailed": "Failed to update profile." + }, + "Identities": { + "email": "Email", + "github": "GitHub", + "connected": "Connected", + "connect": "Connect", + "disconnect": "Disconnect", + "actions": "GitHub actions", + "manage": "Manage on GitHub", + "disconnectFailed": "Failed to disconnect GitHub account.", + "disconnected": "GitHub account disconnected." + }, + "Installations": { + "empty": "No account with the GitHub application installed.", + "actions": "Installation actions", + "manage": "Manage GitHub App" + }, + "AboutDialog": { + "title": "About Pages CMS", + "desc": "Open source CMS for static sites. Edit directly on GitHub with a clean interface.", + "version": "Version", + "website": "Website", + "docs": "Docs", + "github": "GitHub", + "updateTo": "Update to" + }, + "RepoSidebar": { + "viewGithub": "View on GitHub", + "branches": "Branches", + "manageBranches": "Manage branches", + "allProjects": "All projects", + "content": "Content", + "media": "Media", + "recent": "Recently visited", + "actions": "Actions", + "admin": "Admin", + "cache": "Cache", + "collaborators": "Collaborators", + "configuration": "Configuration" + }, + "AuthPage": { + "title": "Sign in to Pages CMS", + "github": "Sign in with GitHub", + "or": "Or", + "emailPlaceholder": "Email", + "continueEmail": "Continue with email", + "invalidEmail": "Invalid email", + "codeSent": "We sent you a sign-in code.", + "enterCode": "Enter the 6-digit code.", + "startGithubFailed": "Could not start GitHub sign-in. Please try again.", + "legalPrefix": "By clicking continue, you agree to our", + "terms": "Terms of Service", + "and": "and", + "privacy": "Privacy Policy", + "githubEmailMissing": "GitHub did not return an email for this account. For local dev, restart the app and try again (email fallback is enabled), or make sure the app has email permission and re-authorize it in GitHub Settings > Applications.", + "githubDenied": "GitHub denied profile access. Re-authorize Pages CMS in GitHub Settings > Applications > Authorized GitHub Apps / Authorized OAuth Apps, then try again.", + "otpTitle": "Verify your login", + "otpDesc": "Enter the 6-digit code sent to {email}.", + "verifyCode": "Verify code", + "resendCode": "Resend code", + "signInAnotherWay": "Sign in another way" + }, + "UserMenu": { + "theme": "Theme", + "light": "Light", + "dark": "Dark", + "system": "System", + "settings": "Settings", + "signOut": "Sign out" + } +} diff --git a/next.config.mjs b/next.config.mjs index 4678774e6..48fef7a6e 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,6 @@ +import createNextIntlPlugin from 'next-intl/plugin'; /** @type {import('next').NextConfig} */ const nextConfig = {}; -export default nextConfig; +const withNextIntl = createNextIntlPlugin(); +export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index e3321dea6..0eb8304c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "lucide-react": "^0.563.0", "marked": "^17.0.1", "next": "^16.2.0", + "next-intl": "^4.13.0", "next-themes": "^0.4.6", "nodemailer": "^8.0.2", "postgres": "^3.4.7", @@ -1367,7 +1368,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1384,7 +1384,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1401,7 +1400,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1418,7 +1416,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1435,7 +1432,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1452,7 +1448,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1469,7 +1464,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1486,7 +1480,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1503,7 +1496,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1520,7 +1512,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1537,7 +1528,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1554,7 +1544,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1571,7 +1560,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1588,7 +1576,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1605,7 +1592,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1622,7 +1608,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1639,7 +1624,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1656,7 +1640,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1673,7 +1656,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1690,7 +1672,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1707,7 +1688,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1724,7 +1704,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1791,7 +1770,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1808,7 +1786,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1825,7 +1802,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1842,7 +1818,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1859,7 +1834,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1876,7 +1850,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1893,7 +1866,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1910,7 +1882,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1927,7 +1898,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1944,7 +1914,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1961,7 +1930,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1978,7 +1946,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1995,7 +1962,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2012,7 +1978,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2029,7 +1994,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2046,7 +2010,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2063,7 +2026,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2080,7 +2042,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2097,7 +2058,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2114,7 +2074,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2131,7 +2090,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2148,7 +2106,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2165,7 +2122,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2182,7 +2138,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2199,7 +2154,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2216,7 +2170,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2408,6 +2361,36 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.6.tgz", + "integrity": "sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.11.tgz", + "integrity": "sha512-NVsuNsc2dUVG9+4HBJ/srScxtA/18LqGgwtop/tuN/OIBjVl6QA+0KhfZQddDD9sEh2LeVjLFPGVU3ixa3blcA==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.10" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.10.tgz", + "integrity": "sha512-XuSva+8ZGawk8VnD5VD6UeH8KarQ/Z022zgjHDoHmlNiAewstXuuzXc0Hk5pGFSdG+nNw5bfJKXqj1ZXHn9yUA==", + "license": "MIT" + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.10.tgz", + "integrity": "sha512-P/IC3qws3jH+1fEs+o0RIFgXKRaQlFehjS5W0FPAqdo6hgzawLl+eD0q0JjheQ3XtoOe5n8WSYfX06KQZI/QJA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.6" + } + }, "node_modules/@hono/node-server": { "version": "1.19.12", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.12.tgz", @@ -3987,7 +3970,332 @@ "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", "engines": { - "node": ">=14" + "node": ">=14" + } + }, + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "musl" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "musl" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "musl" + ], + "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/@popperjs/core": { @@ -5860,6 +6168,12 @@ "dev": true, "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/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -5905,6 +6219,222 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.41.tgz", + "integrity": "sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.41.tgz", + "integrity": "sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA==", + "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.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.41.tgz", + "integrity": "sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.41.tgz", + "integrity": "sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.41.tgz", + "integrity": "sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.41.tgz", + "integrity": "sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.41.tgz", + "integrity": "sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.41.tgz", + "integrity": "sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.41.tgz", + "integrity": "sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.41.tgz", + "integrity": "sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg==", + "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.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.41.tgz", + "integrity": "sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw==", + "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.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.41.tgz", + "integrity": "sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw==", + "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": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -5914,6 +6444,15 @@ "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": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", @@ -9122,7 +9661,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -10636,7 +11174,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -11152,6 +11689,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/icu-minify": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.13.0.tgz", + "integrity": "sha512-SIFMeUHZJjzS5RvIGvybKvWoHjDm9cGVEs2EpJ8PmywOdJLWyblPm7TdPLLoUtkJtwQD7iGhl2WMptZ+N0on+w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -11221,6 +11773,16 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "11.2.8", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.8.tgz", + "integrity": "sha512-l323RCl3qJDVQ8U9j74ut/hVMdg3VPsOHpVMDvFfz9qiq4dPO5ooVYFNVUzzrpgG39a+RLzcXyJb8VFgIU+tUA==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.6", + "@formatjs/icu-messageformat-parser": "3.5.11" + } + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -11426,7 +11988,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11482,7 +12043,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -12778,7 +13338,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -12837,6 +13396,83 @@ } } }, + "node_modules/next-intl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.13.0.tgz", + "integrity": "sha512-OvNq2v5XLx4EkQOsAhVE9g+6zdb83XHusADCXXtIW4LILYnjEVaeINdr1lkVWKSjzwNUiMSlH5N4K0OQTRiv6A==", + "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.13.0", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.13.0", + "po-parser": "^2.1.1", + "use-intl": "^4.13.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.13.0", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.13.0.tgz", + "integrity": "sha512-6S/fJI0KXvLCL8nhBo9P8eGaJPzmwJBTCzX0NaUIj0VyU8U89d//T+vjMLdNIXl5MlLaYH7B9MbAjb8Mvu+tqQ==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.41.tgz", + "integrity": "sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==", + "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.41", + "@swc/core-darwin-x64": "1.15.41", + "@swc/core-linux-arm-gnueabihf": "1.15.41", + "@swc/core-linux-arm64-gnu": "1.15.41", + "@swc/core-linux-arm64-musl": "1.15.41", + "@swc/core-linux-ppc64-gnu": "1.15.41", + "@swc/core-linux-s390x-gnu": "1.15.41", + "@swc/core-linux-x64-gnu": "1.15.41", + "@swc/core-linux-x64-musl": "1.15.41", + "@swc/core-win32-arm64-msvc": "1.15.41", + "@swc/core-win32-ia32-msvc": "1.15.41", + "@swc/core-win32-x64-msvc": "1.15.41" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -12875,6 +13511,12 @@ "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-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -13445,6 +14087,12 @@ "node": ">=16.20.0" } }, + "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -15494,7 +16142,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15511,7 +16158,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15528,7 +16174,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15545,7 +16190,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15562,7 +16206,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15579,7 +16222,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15596,7 +16238,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15613,7 +16254,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15630,7 +16270,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15647,7 +16286,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15664,7 +16302,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15681,7 +16318,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15698,7 +16334,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15715,7 +16350,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15732,7 +16366,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15749,7 +16382,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15766,7 +16398,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15783,7 +16414,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15800,7 +16430,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15817,7 +16446,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15834,7 +16462,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15851,7 +16478,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15868,7 +16494,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15885,7 +16510,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15902,7 +16526,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15919,7 +16542,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16346,6 +16968,27 @@ "react": "*" } }, + "node_modules/use-intl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.13.0.tgz", + "integrity": "sha512-fAFDrWaASxlhXOipcOyb5VDD+YONqj6+8O8EcG/J7RBoOUF3A8YahRWLN+mBxYMrlMQB8N6Voqk5X+YC+HSL0A==", + "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.13.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/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", diff --git a/package.json b/package.json index fddc9d377..f48387442 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "lucide-react": "^0.563.0", "marked": "^17.0.1", "next": "^16.2.0", + "next-intl": "^4.13.0", "next-themes": "^0.4.6", "nodemailer": "^8.0.2", "postgres": "^3.4.7", From 4cec6a883e2e6454ed30e0c8252646aa83ea9f49 Mon Sep 17 00:00:00 2001 From: Florian Hoss Date: Mon, 15 Jun 2026 18:16:25 +0200 Subject: [PATCH 2/5] feat(i18n): add header language switcher --- app/(main)/main-root-layout.tsx | 2 ++ components/language-switcher.tsx | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 components/language-switcher.tsx diff --git a/app/(main)/main-root-layout.tsx b/app/(main)/main-root-layout.tsx index 537dbb76e..802a172fc 100644 --- a/app/(main)/main-root-layout.tsx +++ b/app/(main)/main-root-layout.tsx @@ -1,6 +1,7 @@ import { User } from "@/components/user"; import { AdminButton } from "@/components/admin-button"; import { About } from "@/components/about"; +import { LanguageSwitcher } from "@/components/language-switcher"; export function MainRootLayout({ children, @@ -13,6 +14,7 @@ export function MainRootLayout({
+
diff --git a/components/language-switcher.tsx b/components/language-switcher.tsx new file mode 100644 index 000000000..5c468858e --- /dev/null +++ b/components/language-switcher.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Globe } from "lucide-react"; +import { useLocale, useTranslations } from "next-intl"; +import { + LANGUAGE_COOKIE_KEY, + isLanguage, + SUPPORTED_LANGUAGES, +} from "@/i18n/constants"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const languageLabelKey = { + en: "en", + de: "de", +} as const; + +export function LanguageSwitcher() { + const router = useRouter(); + const locale = useLocale(); + const t = useTranslations("LanguageSwitcher"); + const language = isLanguage(locale) ? locale : "en"; + + return ( + + ); +} \ No newline at end of file From 9ccf54664cbd247569848a31a9a4350c350e20c2 Mon Sep 17 00:00:00 2001 From: Florian Hoss Date: Mon, 15 Jun 2026 18:16:32 +0200 Subject: [PATCH 3/5] feat(i18n): translate main, settings, auth, and sidebar UI --- app/(main)/loading.tsx | 11 ++++-- app/(main)/page.tsx | 23 ++++++------- app/(main)/settings/page.tsx | 18 ++++++---- components/about.tsx | 21 ++++++------ components/otp-verification-form.tsx | 18 +++++++--- components/repo/repo-sidebar.tsx | 49 ++++++++++++++------------- components/settings/identities.tsx | 22 ++++++------ components/settings/installations.tsx | 8 +++-- components/settings/profile.tsx | 21 +++++++----- components/sign-in.tsx | 40 ++++++++++++++-------- components/user.tsx | 15 ++++---- 11 files changed, 145 insertions(+), 101 deletions(-) diff --git a/app/(main)/loading.tsx b/app/(main)/loading.tsx index f392b2e33..7512fd3b0 100644 --- a/app/(main)/loading.tsx +++ b/app/(main)/loading.tsx @@ -1,5 +1,12 @@ import { Loader } from "@/components/loader"; +import { getTranslations } from "next-intl/server"; -export default function Loading() { - return Loading; +export default async function Loading() { + const t = await getTranslations("Loading"); + + return ( + + {t("text")} + + ); } diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 71a8da0d7..d3cfcfc41 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -7,6 +7,7 @@ import { RepoSelect } from "@/components/repo/repo-select"; import { RepoTemplates } from "@/components/repo/repo-templates"; import { RepoLatest } from "@/components/repo/repo-latest"; import { DocumentTitle } from "@/components/document-title"; +import { useTranslations } from "next-intl"; import { hasGithubIdentity } from "@/lib/authz-shared"; import { Empty, @@ -22,6 +23,7 @@ export default function Page() { const [defaultAccount, setDefaultAccount] = useState(null); const [hasRecentVisits, setHasRecentVisits] = useState(false); const { user } = useUser(); + const t = useTranslations("HomePage"); const isGithubUser = hasGithubIdentity(user); useEffect(() => { @@ -33,21 +35,21 @@ export default function Page() { return ( - +
{user.accounts.length > 0 ? (
{hasRecentVisits && (

- Recently visited + {t("recent")}

)}

- Open a project + {t("open")}

setDefaultAccount(account)} @@ -56,7 +58,7 @@ export default function Page() { {isGithubUser && (

- Create from a template + {t("template")}

@@ -65,10 +67,9 @@ export default function Page() { ) : isGithubUser ? ( - Install the GitHub App + {t("installTitle")} - Install the GitHub App on at least one account before you can - open or create projects. + {t("installDesc")} @@ -85,18 +86,16 @@ export default function Page() { GitHub - Install GitHub App + {t("installCta")} ) : ( - No repositories yet + {t("noneTitle")} - You need an invitation to a repository before you can - collaborate. Ask a repository owner or organization admin to - invite you. + {t("noneDesc")} diff --git a/app/(main)/settings/page.tsx b/app/(main)/settings/page.tsx index 217047cd3..d62ddb9b5 100644 --- a/app/(main)/settings/page.tsx +++ b/app/(main)/settings/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { headers } from "next/headers"; +import { getTranslations } from "next-intl/server"; import { and, eq } from "drizzle-orm"; import { auth } from "@/lib/auth"; import { db } from "@/db"; @@ -21,6 +22,7 @@ import { ArrowLeft } from "lucide-react"; import { cn } from "@/lib/utils"; export default async function Page() { + const t = await getTranslations("SettingsPage"); const session = await auth.api.getSession({ headers: await headers(), }); @@ -39,7 +41,7 @@ export default async function Page() { return ( - +
- Go home + {t("back")}

- Settings + {t("title")}

@@ -65,9 +67,11 @@ export default async function Page() { - Authentication + + {t("authTitle")} + - Your sign-in methods and linked identity providers. + {t("authDesc")} @@ -84,10 +88,10 @@ export default async function Page() { - Installations + {t("instTitle")} - Manage the accounts the Github application is installed on. + {t("instDesc")} diff --git a/components/about.tsx b/components/about.tsx index 0bd32756d..927102ba6 100644 --- a/components/about.tsx +++ b/components/about.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { useEffect, useMemo, useState } from "react"; import { ArrowUpRight } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { @@ -31,6 +32,7 @@ const version = const UPDATE_DOCS_URL = "https://pagescms.org/docs"; export function About() { + const t = useTranslations("AboutDialog"); const [open, setOpen] = useState(false); const [latestVersion, setLatestVersion] = useState(null); @@ -84,11 +86,11 @@ export function About() { - About Pages CMS + {t("title")} - About Pages CMS + {t("title")} @@ -106,15 +108,14 @@ export function About() { Pages CMS - - Open source CMS for static sites. Edit directly on GitHub with a - clean interface. + + {t("desc")}
{version} @@ -129,7 +130,7 @@ export function About() { variant="secondary" className="bg-primary/10 font-medium text-primary" > - Update to {latestVersion} + {t("updateTo")} {latestVersion} @@ -138,7 +139,7 @@ export function About() { } /> pagescms.org @@ -146,7 +147,7 @@ export function About() { } /> pagescms.org/docs @@ -154,7 +155,7 @@ export function About() { } /> pagescms/pagescms diff --git a/components/otp-verification-form.tsx b/components/otp-verification-form.tsx index 1abdd30db..d7a783776 100644 --- a/components/otp-verification-form.tsx +++ b/components/otp-verification-form.tsx @@ -17,7 +17,11 @@ type OtpVerificationFormProps = { pending: boolean; resendDisabled?: boolean; resendPending?: boolean; + title?: string; + description?: string; submitLabel?: string; + resendLabel?: string; + signInAnotherWayLabel?: string; onChange: (value: string) => void; onResend: () => void; onSignInAnotherWay?: () => void; @@ -31,20 +35,26 @@ export function OtpVerificationForm({ pending, resendDisabled, resendPending, + title = "Verify your login", + description, submitLabel = "Verify code", + resendLabel = "Resend code", + signInAnotherWayLabel = "Sign in another way", onChange, onResend, onSignInAnotherWay, onSubmit, }: OtpVerificationFormProps) { + const resolvedDescription = description || `Enter the 6-digit code sent to ${emailLabel}.`; + return (

- Verify your login + {title}

- Enter the 6-digit code sent to {emailLabel}. + {resolvedDescription}

@@ -79,7 +89,7 @@ export function OtpVerificationForm({ type="button" variant="ghost" > - Resend code + {resendLabel} {resendPending && } {onSignInAnotherWay && ( @@ -90,7 +100,7 @@ export function OtpVerificationForm({ type="button" variant="ghost" > - Sign in another way + {signInAnotherWayLabel} )}
diff --git a/components/repo/repo-sidebar.tsx b/components/repo/repo-sidebar.tsx index 734eae9af..7de874cac 100644 --- a/components/repo/repo-sidebar.tsx +++ b/components/repo/repo-sidebar.tsx @@ -10,6 +10,7 @@ import { } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useConfig } from "@/contexts/config-context"; import { useRepo } from "@/contexts/repo-context"; import { useUser } from "@/contexts/user-context"; @@ -89,6 +90,7 @@ type NavigationNode = { }; function RepoSwitcher() { + const t = useTranslations("RepoSidebar"); const router = useRouter(); const { owner, repo, branches = [] } = useRepo(); const { config } = useConfig(); @@ -195,13 +197,13 @@ function RepoSwitcher() { target="_blank" rel="noreferrer" > - View on GitHub + {t("viewGithub")} - Branches + {t("branches")} - Manage branches + {t("manageBranches")} {recentRepos.length > 0 && ( <> - Recently visited + {t("recent")} {recentRepos.map((visit) => ( - All projects + {t("allProjects")}
- Manage branches + {t("manageBranches")} @@ -260,6 +262,7 @@ function RepoSwitcher() { } export function RepoSidebar() { + const t = useTranslations("RepoSidebar"); const pathname = usePathname(); const { user } = useUser(); const { config } = useConfig(); @@ -314,9 +317,9 @@ export function RepoSidebar() { return media.map((item: any) => ({ type: "media", name: item.name || "default", - label: item.label || item.name || "Media", + label: item.label || item.name || t("media"), })); - }, [config]); + }, [config, t]); const adminItems = useMemo(() => { if (!config) return []; @@ -329,7 +332,7 @@ export function RepoSidebar() { if (canManageRepo && isCacheEnabled(configObject)) { items.push({ key: "admin-cache", - label: "Cache", + label: t("cache"), href: `/${config.owner}/${config.repo}/${encodeURIComponent(config.branch)}/cache`, icon: , }); @@ -338,14 +341,14 @@ export function RepoSidebar() { if (canManageRepo) { items.push({ key: "admin-actions", - label: "Actions", + label: t("actions"), href: `/${config.owner}/${config.repo}/${encodeURIComponent(config.branch)}/actions`, icon: , }); items.push({ key: "admin-collaborators", - label: "Collaborators", + label: t("collaborators"), href: `/${config.owner}/${config.repo}/${encodeURIComponent(config.branch)}/collaborators`, icon: , }); @@ -354,14 +357,14 @@ export function RepoSidebar() { if (canManageRepo && isConfigEnabled(configObject)) { items.push({ key: "admin-configuration", - label: "Configuration", + label: t("configuration"), href: `/${config.owner}/${config.repo}/${encodeURIComponent(config.branch)}/configuration`, icon: , }); } return items; - }, [config, user]); + }, [config, t, user]); const rootActions = useMemo( () => getRootActions(config?.object), [config?.object], @@ -526,26 +529,26 @@ export function RepoSidebar() { ); } - const renderNavigationGroup = (label: string, nodes: NavigationNode[]) => { + const renderNavigationGroup = (key: string, label: string, nodes: NavigationNode[]) => { if (nodes.length === 0) return null; return ( - + {label} - {nodes.map((node) => renderNavigationNode(node, `${label}-${node.name}`))} + {nodes.map((node) => renderNavigationNode(node, `${key}-${node.name}`))} ); }; - const renderFlatGroup = (label: string, items: NavItem[]) => { + const renderFlatGroup = (key: string, label: string, items: NavItem[]) => { if (items.length === 0) return null; return ( - + {label} @@ -570,12 +573,12 @@ export function RepoSidebar() { }; const groups = [ - renderNavigationGroup("Content", contentNavigation), - renderNavigationGroup("Media", mediaNavigation), + renderNavigationGroup("content", t("content"), contentNavigation), + renderNavigationGroup("media", t("media"), mediaNavigation), rootActions.length > 0 && config ? ( - - Actions + + {t("actions")} ) : null, - renderFlatGroup("Admin", adminItems), + renderFlatGroup("admin", t("admin"), adminItems), ].filter(Boolean); return ( diff --git a/components/settings/identities.tsx b/components/settings/identities.tsx index 3a8e88c95..488f5aeb8 100644 --- a/components/settings/identities.tsx +++ b/components/settings/identities.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { signIn } from "@/lib/auth-client"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -35,6 +36,7 @@ export function Identities({ githubManageUrl, }: IdentitiesProps) { const router = useRouter(); + const t = useTranslations("Identities"); const [pendingAction, setPendingAction] = useState< "connect" | "disconnect" | null >(null); @@ -65,17 +67,17 @@ export function Identities({ const payload = await response.json().catch(() => null); if (!response.ok || !payload?.status) { const message = - payload?.message || "Failed to disconnect GitHub account."; + payload?.message || t("disconnectFailed"); throw new Error(message); } - toast.success("GitHub account disconnected."); + toast.success(t("disconnected")); router.refresh(); } catch (error) { const message = error instanceof Error ? error.message - : "Failed to disconnect GitHub account."; + : t("disconnectFailed"); toast.error(message); } finally { setPendingAction(null); @@ -87,7 +89,7 @@ export function Identities({
  • - Email + {t("email")}
    {email}
  • @@ -99,11 +101,11 @@ export function Identities({ )} > - GitHub + {t("github")}
    {githubConnected && (
    - {githubUsername ? `@${githubUsername}` : "Connected"} + {githubUsername ? `@${githubUsername}` : t("connected")}
    )} {!githubConnected ? ( @@ -114,7 +116,7 @@ export function Identities({ onClick={handleConnectGithub} disabled={pendingAction !== null} > - Connect + {t("connect")} {pendingAction === "connect" && ( )} @@ -133,7 +135,7 @@ export function Identities({ ) : ( )} - GitHub actions + {t("actions")} @@ -141,7 +143,7 @@ export function Identities({ <> - Manage on GitHub + {t("manage")} @@ -153,7 +155,7 @@ export function Identities({ onClick={handleDisconnectGithub} disabled={pendingAction !== null} > - Disconnect + {t("disconnect")} diff --git a/components/settings/installations.tsx b/components/settings/installations.tsx index c0d36624c..bb06bdf67 100644 --- a/components/settings/installations.tsx +++ b/components/settings/installations.tsx @@ -1,5 +1,6 @@ "use client"; +import { useTranslations } from "next-intl"; import { useUser } from "@/contexts/user-context"; import { Button } from "@/components/ui/button"; import { getGithubInstallationUrl } from "@/lib/github-app"; @@ -13,12 +14,13 @@ import { ArrowUpRight, Ban, EllipsisVertical } from "lucide-react"; const Installations = () => { const { user } = useUser(); + const t = useTranslations("Installations"); if (!user || !user.accounts) { return (
    - No account with the Github application installed. + {t("empty")}
    ); } @@ -42,7 +44,7 @@ const Installations = () => { @@ -52,7 +54,7 @@ const Installations = () => { target="_blank" rel="noreferrer" > - Manage GitHub App + {t("manage")} diff --git a/components/settings/profile.tsx b/components/settings/profile.tsx index ec6279ce9..55cc0f7f5 100644 --- a/components/settings/profile.tsx +++ b/components/settings/profile.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { toast } from "sonner"; import { getInitialsFromName } from "@/lib/utils/avatar"; import { @@ -30,6 +31,7 @@ type ProfileProps = { export function Profile({ name, email, githubUsername }: ProfileProps) { const router = useRouter(); + const t = useTranslations("ProfileCard"); const [displayName, setDisplayName] = useState(name?.trim() || ""); const [isSaving, setIsSaving] = useState(false); @@ -50,13 +52,16 @@ export function Profile({ name, email, githubUsername }: ProfileProps) { }); const payload = await response.json().catch(() => null); if (!response.ok || !payload?.status) { - throw new Error(payload?.message || "Failed to update profile."); + throw new Error(payload?.message || t("updateFailed")); } - toast.success("Profile updated."); + toast.success(t("updated")); router.refresh(); } catch (error) { - const message = error instanceof Error ? error.message : "Failed to update profile."; + const message = + error instanceof Error + ? error.message + : t("updateFailed"); toast.error(message); } finally { setIsSaving(false); @@ -66,8 +71,8 @@ export function Profile({ name, email, githubUsername }: ProfileProps) { return ( - Profile - Manage the information displayed to other users. + {t("title")} + {t("desc")}
    @@ -123,7 +128,7 @@ export function Profile({ name, email, githubUsername }: ProfileProps) { onClick={() => void handleSave()} disabled={!canSave} > - Save profile + {t("save")} {isSaving && } diff --git a/components/sign-in.tsx b/components/sign-in.tsx index f8ce4924c..425eb807e 100644 --- a/components/sign-in.tsx +++ b/components/sign-in.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { emailOtp, signIn } from "@/lib/auth-client"; import { getAuthCallbackURL, getSafeRedirect } from "@/lib/auth-redirect"; import { Input } from "@/components/ui/input"; @@ -11,6 +12,7 @@ import { toast } from "sonner"; import { Loader } from "lucide-react"; export function SignIn() { + const t = useTranslations("AuthPage"); const [email, setEmail] = useState(""); const [otp, setOtp] = useState(""); const [step, setStep] = useState<"email" | "otp">("email"); @@ -30,9 +32,12 @@ export function SignIn() { : `/sign-in?redirect=${encodeURIComponent(safeRedirect)}`; const getErrorMessage = (value: string) => { + if (value.toLowerCase() === "email_not_found") { + return t("githubEmailMissing"); + } if (value.toLowerCase() !== "unable_to_get_user_info") return value; return [ - "GitHub denied profile access. Re-authorize Pages CMS in GitHub Settings > Applications > Authorized GitHub Apps / Authorized OAuth Apps, then try again.", + t("githubDenied"), "https://github.com/settings/applications", ].join(" "); }; @@ -67,9 +72,9 @@ export function SignIn() { } setSubmittingMethod(null); - toast.error("Could not start GitHub sign-in. Please try again."); + toast.error(t("startGithubFailed")); } catch (error: any) { - toast.error(error?.message || "Could not start GitHub sign-in."); + toast.error(error?.message || t("startGithubFailed")); setSubmittingMethod(null); } }; @@ -77,7 +82,7 @@ export function SignIn() { const handleEmailSignIn = async () => { const normalizedEmail = email.trim().toLowerCase(); if (!normalizedEmail) { - toast.error("Invalid email"); + toast.error(t("invalidEmail")); return; } @@ -96,7 +101,7 @@ export function SignIn() { setEmail(normalizedEmail); setOtp(""); setStep("otp"); - toast.success("We sent you a sign-in code.", { duration: 8000 }); + toast.success(t("codeSent"), { duration: 8000 }); } finally { setSubmittingMethod(null); } @@ -104,7 +109,7 @@ export function SignIn() { const handleOtpSignIn = async () => { if (otp.length !== 6) { - toast.error("Enter the 6-digit code."); + toast.error(t("enterCode")); return; } @@ -133,21 +138,21 @@ export function SignIn() { const legalCopy = (

    - By clicking continue, you agree to our{" "} + {t("legalPrefix")}{" "} - Terms of Service + {t("terms")} {" "} - and{" "} + {t("and")}{" "} - Privacy Policy + {t("privacy")} .

    @@ -165,6 +170,11 @@ export function SignIn() { pending={submittingMethod === "otp"} resendDisabled={submittingMethod === "otp"} resendPending={submittingMethod === "email"} + submitLabel={t("verifyCode")} + title={t("otpTitle")} + description={t("otpDesc").replace("{email}", email)} + resendLabel={t("resendCode")} + signInAnotherWayLabel={t("signInAnotherWay")} onChange={setOtp} onResend={() => void handleEmailSignIn()} onSignInAnotherWay={resetToFullSignIn} @@ -178,7 +188,7 @@ export function SignIn() { ) : (

    - Sign in to Pages CMS + {t("title")}

    - Or + {t("or")}
    setEmail(event.target.value)} /> diff --git a/app/not-found.tsx b/app/not-found.tsx index 4cac55f1c..c71842617 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,17 +1,20 @@ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty"; import Link from "next/link"; import { buttonVariants } from "@/components/ui/button"; +import { getTranslations } from "next-intl/server"; + +export default async function NotFound() { + const t = await getTranslations("NotFoundPage"); -export default function NotFound() { return ( - Page not found - The page or resource you requested could not be found. + {t("title")} + {t("desc")} - Go home + {t("goHome")} diff --git a/components/admin-button.tsx b/components/admin-button.tsx index 507e75122..6fe5177a5 100644 --- a/components/admin-button.tsx +++ b/components/admin-button.tsx @@ -2,17 +2,19 @@ import Link from "next/link"; import { Settings } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useUser } from "@/contexts/user-context"; import { Button } from "@/components/ui/button"; export function AdminButton() { const { user } = useUser(); + const t = useTranslations("AdminButton"); if (!user?.isAdmin) return null; return ( diff --git a/components/github-auth-expired.tsx b/components/github-auth-expired.tsx index 86433d1c4..16607e592 100644 --- a/components/github-auth-expired.tsx +++ b/components/github-auth-expired.tsx @@ -6,8 +6,10 @@ import { getSafeRedirect } from "@/lib/auth-redirect"; import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty"; import { Button } from "@/components/ui/button"; import { ArrowLeft, Loader } from "lucide-react"; +import { useTranslations } from "next-intl"; const GithubAuthExpired = () => { + const t = useTranslations("GithubAuthExpired"); const [loading, setLoading] = useState(false); const handleSignInAgain = async () => { @@ -28,11 +30,11 @@ const GithubAuthExpired = () => { return ( - GitHub session expired - Your GitHub session has expired. You'll need to sign in again. + {t("title")} + {t("desc")} diff --git a/components/invite-sign-in.tsx b/components/invite-sign-in.tsx index 1c84e81eb..1a5b42eb7 100644 --- a/components/invite-sign-in.tsx +++ b/components/invite-sign-in.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import { Loader } from "lucide-react"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; import { authClient } from "@/lib/auth-client"; import { Button, buttonVariants } from "@/components/ui/button"; import { OtpVerificationForm } from "@/components/otp-verification-form"; @@ -28,6 +29,7 @@ type InviteState = }; export function InviteSignIn({ token }: { token: string }) { + const t = useTranslations("InviteSignIn"); const [state, setState] = useState({ status: "loading" }); const [otp, setOtp] = useState(""); const [pending, setPending] = useState(null); @@ -81,7 +83,7 @@ export function InviteSignIn({ token }: { token: string }) { toast.error(result.error.message); } } catch { - toast.error("Unable to send sign-in code."); + toast.error(t("unableToSendCode")); } finally { setPending(null); } @@ -90,7 +92,7 @@ export function InviteSignIn({ token }: { token: string }) { async function verifyOtp() { if (state.status !== "otp_required") return; if (otp.length !== 6) { - toast.error("Enter the 6-digit code."); + toast.error(t("enterCode")); return; } @@ -121,10 +123,10 @@ export function InviteSignIn({ token }: { token: string }) { return; } - toast.error("Unable to claim this invitation."); + toast.error(t("unableToClaim")); setPending(null); } catch { - toast.error("Unable to verify code."); + toast.error(t("unableToVerify")); setPending(null); } } @@ -143,12 +145,12 @@ export function InviteSignIn({ token }: { token: string }) { return ( - Invite unavailable - This invitation is no longer available. + {t("unavailableTitle")} + {t("unavailableDesc")} - Sign in + {t("signIn")} @@ -159,8 +161,8 @@ export function InviteSignIn({ token }: { token: string }) { return ( - Wrong account - This invitation was sent to another account. + {t("wrongAccountTitle")} + {t("wrongAccountDesc")} - Go home + {t("goHome")} diff --git a/messages/de.json b/messages/de.json index acb9917b4..c889e2afb 100644 --- a/messages/de.json +++ b/messages/de.json @@ -105,5 +105,37 @@ "system": "System", "settings": "Einstellungen", "signOut": "Abmelden" + }, + "InviteSignIn": { + "unavailableTitle": "Einladung nicht verfügbar", + "unavailableDesc": "Diese Einladung ist nicht mehr verfügbar.", + "unavailableLinkDesc": "Dieser Einladungslink ist ungültig.", + "signIn": "Anmelden", + "wrongAccountTitle": "Falsches Konto", + "wrongAccountDesc": "Diese Einladung wurde an ein anderes Konto gesendet.", + "signOut": "Abmelden", + "goHome": "Zur Startseite", + "unableToSendCode": "Anmeldecode konnte nicht gesendet werden.", + "enterCode": "Gib den 6-stelligen Code ein.", + "unableToClaim": "Diese Einladung konnte nicht angenommen werden.", + "unableToVerify": "Code konnte nicht bestätigt werden." + }, + "GithubAuthExpired": { + "title": "GitHub-Sitzung abgelaufen", + "desc": "Deine GitHub-Sitzung ist abgelaufen. Du musst dich erneut anmelden.", + "signInAnotherWay": "Anders anmelden" + }, + "NotFoundPage": { + "title": "Seite nicht gefunden", + "desc": "Die angeforderte Seite oder Ressource konnte nicht gefunden werden.", + "goHome": "Zur Startseite" + }, + "ErrorPage": { + "title": "Etwas ist schiefgelaufen", + "goHome": "Zur Startseite", + "tryAgain": "Erneut versuchen" + }, + "AdminButton": { + "label": "Admin-Bereich" } } diff --git a/messages/en.json b/messages/en.json index 00c687ef9..9fee184b1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -105,5 +105,37 @@ "system": "System", "settings": "Settings", "signOut": "Sign out" + }, + "InviteSignIn": { + "unavailableTitle": "Invite unavailable", + "unavailableDesc": "This invitation is no longer available.", + "unavailableLinkDesc": "This invitation link is invalid.", + "signIn": "Sign in", + "wrongAccountTitle": "Wrong account", + "wrongAccountDesc": "This invitation was sent to another account.", + "signOut": "Sign out", + "goHome": "Go home", + "unableToSendCode": "Unable to send sign-in code.", + "enterCode": "Enter the 6-digit code.", + "unableToClaim": "Unable to claim this invitation.", + "unableToVerify": "Unable to verify code." + }, + "GithubAuthExpired": { + "title": "GitHub session expired", + "desc": "Your GitHub session has expired. You'll need to sign in again.", + "signInAnotherWay": "Sign in another way" + }, + "NotFoundPage": { + "title": "Page not found", + "desc": "The page or resource you requested could not be found.", + "goHome": "Go home" + }, + "ErrorPage": { + "title": "Something went wrong", + "goHome": "Go home", + "tryAgain": "Try again" + }, + "AdminButton": { + "label": "Admin panel" } }