feature: i18n support
This commit is contained in:
parent
fc9aa10402
commit
304ea14003
28
app/[locale]/global.css
Normal file
28
app/[locale]/global.css
Normal 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
30
app/[locale]/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
11
app/[locale]/not-found.tsx
Normal file
11
app/[locale]/not-found.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { RecoilRoot } from "recoil";
|
||||
import Homepage from "../components";
|
||||
import Homepage from "../../components";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
@ -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;
|
||||
}
|
||||
}
|
@ -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({
|
||||
children
|
||||
}: Readonly<{
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
|
9
app/not-found.tsx
Normal file
9
app/not-found.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<h1>Not Found</h1>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { atom, useRecoilValue } from "recoil";
|
||||
import { settingsState } from "../state/settings";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function Search(props: {
|
||||
onFocus: () => void;
|
||||
}) {
|
||||
const settings = useRecoilValue(settingsState);
|
||||
const t = useTranslations('Search');
|
||||
let style = "default";
|
||||
if (style === "default") {
|
||||
return (
|
||||
@ -17,7 +21,7 @@ export default function Search(props: {
|
||||
dark:placeholder:text-slate-400 text-slate-900 dark:text-white"
|
||||
id="searchBox"
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
placeholder={t('placeholder')}
|
||||
onFocus={props.onFocus}
|
||||
/>
|
||||
</div>
|
||||
@ -39,7 +43,7 @@ export default function Search(props: {
|
||||
}
|
||||
id="searchBox"
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
placeholder={t('placeholder')}
|
||||
onFocus={props.onFocus}
|
||||
/>
|
||||
</div>
|
||||
|
14
i18n.ts
Normal file
14
i18n.ts
Normal 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
8
messages/en.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Search": {
|
||||
"placeholder": "Search"
|
||||
},
|
||||
"404":{
|
||||
"title": "Page Not Found"
|
||||
}
|
||||
}
|
8
messages/zh.json
Normal file
8
messages/zh.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Search": {
|
||||
"placeholder": "搜索"
|
||||
},
|
||||
"404":{
|
||||
"title": "未找到"
|
||||
}
|
||||
}
|
14
middleware.ts
Normal file
14
middleware.ts
Normal 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*']
|
||||
};
|
@ -1,4 +1,9 @@
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
@ -12,4 +17,4 @@ const nextConfig = {
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
export default withNextIntl(nextConfig);
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sparkhome",
|
||||
"version": "4.1.0",
|
||||
"version": "4.2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@ -9,10 +9,14 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.0",
|
||||
"framer-motion": "^11.0.24",
|
||||
"next": "14.1.1",
|
||||
"next-intl": "^3.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"recoil": "^0.7.7",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"valid-url": "^1.0.9",
|
||||
"validate-color": "^2.2.4"
|
||||
},
|
||||
|
143
pnpm-lock.yaml
143
pnpm-lock.yaml
@ -5,9 +5,18 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
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:
|
||||
specifier: 14.1.1
|
||||
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:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
@ -17,6 +26,9 @@ dependencies:
|
||||
recoil:
|
||||
specifier: ^0.7.7
|
||||
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:
|
||||
specifier: ^1.0.9
|
||||
version: 1.0.9
|
||||
@ -57,6 +69,66 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
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:
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@ -371,6 +443,11 @@ packages:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
dev: false
|
||||
|
||||
/clsx@2.1.0:
|
||||
resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@ -471,6 +548,25 @@ packages:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
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:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@ -524,6 +620,15 @@ packages:
|
||||
function-bind: 1.1.2
|
||||
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:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
@ -645,6 +750,24 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
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):
|
||||
resolution: {integrity: sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
@ -873,6 +996,10 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
dev: false
|
||||
|
||||
/resolve@1.22.8:
|
||||
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
||||
hasBin: true
|
||||
@ -993,6 +1120,12 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
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:
|
||||
resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -1073,6 +1206,16 @@ packages:
|
||||
picocolors: 1.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: true
|
||||
|
Loading…
Reference in New Issue
Block a user