feature: i18n support

This commit is contained in:
Alikia2x 2024-03-31 00:56:04 +08:00
parent fc9aa10402
commit 304ea14003
15 changed files with 287 additions and 57 deletions

28
app/[locale]/global.css Normal file
View File

@ -0,0 +1,28 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

30
app/[locale]/layout.tsx Normal file
View File

@ -0,0 +1,30 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./global.css";
import { NextIntlClientProvider, useMessages } from "next-intl";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "SparkHome",
description: "Your best browser homepage, with a magic searchbox."
};
export default function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const messages = useMessages();
return (
<html lang={locale}>
<body className={inter.className}>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}

View File

@ -0,0 +1,11 @@
"use client";
import { useTranslations } from "next-intl";
export default function NotFound() {
const t = useTranslations("404");
return (
<div>
<h1>{t('title')}</h1>
</div>
);
}

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { RecoilRoot } from "recoil"; import { RecoilRoot } from "recoil";
import Homepage from "../components"; import Homepage from "../../components";
export default function Home() { export default function Home() {
return ( return (

View File

@ -1,33 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

View File

@ -1,22 +1,7 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./global.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app"
};
export default function RootLayout({ export default function RootLayout({
children children,
}: Readonly<{ }: {
children: React.ReactNode; children: React.ReactNode;
}>) { }) {
return ( return children;
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
} }

9
app/not-found.tsx Normal file
View File

@ -0,0 +1,9 @@
export default function NotFound() {
return (
<html>
<body>
<h1>Not Found</h1>
</body>
</html>
);
}

View File

@ -1,10 +1,14 @@
'use client';
import { atom, useRecoilValue } from "recoil"; import { atom, useRecoilValue } from "recoil";
import { settingsState } from "../state/settings"; import { settingsState } from "../state/settings";
import { useTranslations } from "next-intl";
export default function Search(props: { export default function Search(props: {
onFocus: () => void; onFocus: () => void;
}) { }) {
const settings = useRecoilValue(settingsState); const settings = useRecoilValue(settingsState);
const t = useTranslations('Search');
let style = "default"; let style = "default";
if (style === "default") { if (style === "default") {
return ( return (
@ -17,7 +21,7 @@ export default function Search(props: {
dark:placeholder:text-slate-400 text-slate-900 dark:text-white" dark:placeholder:text-slate-400 text-slate-900 dark:text-white"
id="searchBox" id="searchBox"
type="text" type="text"
placeholder="Search" placeholder={t('placeholder')}
onFocus={props.onFocus} onFocus={props.onFocus}
/> />
</div> </div>
@ -39,7 +43,7 @@ export default function Search(props: {
} }
id="searchBox" id="searchBox"
type="text" type="text"
placeholder="Search" placeholder={t('placeholder')}
onFocus={props.onFocus} onFocus={props.onFocus}
/> />
</div> </div>

14
i18n.ts Normal file
View File

@ -0,0 +1,14 @@
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
// Can be imported from a shared config
const locales = ['en', 'zh'];
export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`/messages/${locale}.json`)).default
};
});

8
messages/en.json Normal file
View File

@ -0,0 +1,8 @@
{
"Search": {
"placeholder": "Search"
},
"404":{
"title": "Page Not Found"
}
}

8
messages/zh.json Normal file
View File

@ -0,0 +1,8 @@
{
"Search": {
"placeholder": "搜索"
},
"404":{
"title": "未找到"
}
}

14
middleware.ts Normal file
View File

@ -0,0 +1,14 @@
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
// A list of all locales that are supported
locales: ['en', 'zh'],
// Used when no locale matches
defaultLocale: 'en'
});
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(zh|en)/:path*']
};

View File

@ -1,4 +1,9 @@
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
images: { images: {
remotePatterns: [ remotePatterns: [
@ -12,4 +17,4 @@ const nextConfig = {
} }
}; };
export default nextConfig; export default withNextIntl(nextConfig);

View File

@ -1,6 +1,6 @@
{ {
"name": "sparkhome", "name": "sparkhome",
"version": "4.1.0", "version": "4.2.0",
"private": false, "private": false,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@ -9,10 +9,14 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"clsx": "^2.1.0",
"framer-motion": "^11.0.24",
"next": "14.1.1", "next": "14.1.1",
"next-intl": "^3.10.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"tailwind-merge": "^2.2.2",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"validate-color": "^2.2.4" "validate-color": "^2.2.4"
}, },

View File

@ -5,9 +5,18 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: dependencies:
clsx:
specifier: ^2.1.0
version: 2.1.0
framer-motion:
specifier: ^11.0.24
version: 11.0.24(react-dom@18.2.0)(react@18.2.0)
next: next:
specifier: 14.1.1 specifier: 14.1.1
version: 14.1.1(react-dom@18.2.0)(react@18.2.0) version: 14.1.1(react-dom@18.2.0)(react@18.2.0)
next-intl:
specifier: ^3.10.0
version: 3.10.0(next@14.1.1)(react@18.2.0)
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0
@ -17,6 +26,9 @@ dependencies:
recoil: recoil:
specifier: ^0.7.7 specifier: ^0.7.7
version: 0.7.7(react-dom@18.2.0)(react@18.2.0) version: 0.7.7(react-dom@18.2.0)(react@18.2.0)
tailwind-merge:
specifier: ^2.2.2
version: 2.2.2
valid-url: valid-url:
specifier: ^1.0.9 specifier: ^1.0.9
version: 1.0.9 version: 1.0.9
@ -57,6 +69,66 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/@babel/runtime@7.24.1:
resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
dev: false
/@formatjs/ecma402-abstract@1.11.4:
resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==}
dependencies:
'@formatjs/intl-localematcher': 0.2.25
tslib: 2.6.2
dev: false
/@formatjs/ecma402-abstract@1.18.2:
resolution: {integrity: sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==}
dependencies:
'@formatjs/intl-localematcher': 0.5.4
tslib: 2.6.2
dev: false
/@formatjs/fast-memoize@1.2.1:
resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==}
dependencies:
tslib: 2.6.2
dev: false
/@formatjs/icu-messageformat-parser@2.1.0:
resolution: {integrity: sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==}
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/icu-skeleton-parser': 1.3.6
tslib: 2.6.2
dev: false
/@formatjs/icu-skeleton-parser@1.3.6:
resolution: {integrity: sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==}
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
tslib: 2.6.2
dev: false
/@formatjs/intl-localematcher@0.2.25:
resolution: {integrity: sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==}
dependencies:
tslib: 2.6.2
dev: false
/@formatjs/intl-localematcher@0.2.32:
resolution: {integrity: sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==}
dependencies:
tslib: 2.6.2
dev: false
/@formatjs/intl-localematcher@0.5.4:
resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==}
dependencies:
tslib: 2.6.2
dev: false
/@isaacs/cliui@8.0.2: /@isaacs/cliui@8.0.2:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -371,6 +443,11 @@ packages:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
dev: false dev: false
/clsx@2.1.0:
resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==}
engines: {node: '>=6'}
dev: false
/color-convert@2.0.1: /color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -471,6 +548,25 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true dev: true
/framer-motion@11.0.24(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-l2iM8NR53qtcujgAqYvGPJJGModPNWEVUaATRDLfnaLvUoFpImovBm0AHalSSsY8tW6knP8mfJTW4WYGbnAe4w==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0
react-dom: ^18.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.6.2
dev: false
/fsevents@2.3.3: /fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -524,6 +620,15 @@ packages:
function-bind: 1.1.2 function-bind: 1.1.2
dev: true dev: true
/intl-messageformat@9.13.0:
resolution: {integrity: sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==}
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/fast-memoize': 1.2.1
'@formatjs/icu-messageformat-parser': 2.1.0
tslib: 2.6.2
dev: false
/is-binary-path@2.1.0: /is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -645,6 +750,24 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
/negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
dev: false
/next-intl@3.10.0(next@14.1.1)(react@18.2.0):
resolution: {integrity: sha512-kqQvOxLvI3ZjvcMFwOBjh71ufNWxHEMaEgxhFZGIXYAvkRI//9zbqeQJkQMwEpI6mDBM+6n+SJd0+pLt0t5GVw==}
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
dependencies:
'@formatjs/intl-localematcher': 0.2.32
negotiator: 0.6.3
next: 14.1.1(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
use-intl: 3.10.0(react@18.2.0)
dev: false
/next@14.1.1(react-dom@18.2.0)(react@18.2.0): /next@14.1.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww==} resolution: {integrity: sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww==}
engines: {node: '>=18.17.0'} engines: {node: '>=18.17.0'}
@ -873,6 +996,10 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: false
/resolve@1.22.8: /resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true hasBin: true
@ -993,6 +1120,12 @@ packages:
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dev: true dev: true
/tailwind-merge@2.2.2:
resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==}
dependencies:
'@babel/runtime': 7.24.1
dev: false
/tailwindcss@3.4.1: /tailwindcss@3.4.1:
resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -1073,6 +1206,16 @@ packages:
picocolors: 1.0.0 picocolors: 1.0.0
dev: true dev: true
/use-intl@3.10.0(react@18.2.0):
resolution: {integrity: sha512-rKCDbszyUP88477VP6DBCN3t3xdTta2o5GwgN1Rlquctm4PErO1YPDY+UI8DgfkVAWNt6X9gfF7ntQZ20H2ivg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@formatjs/ecma402-abstract': 1.18.2
intl-messageformat: 9.13.0
react: 18.2.0
dev: false
/util-deprecate@1.0.2: /util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true dev: true