From 304ea1400372284d274c02b26ad82507a8ee9668 Mon Sep 17 00:00:00 2001 From: Alikia2x Date: Sun, 31 Mar 2024 00:56:04 +0800 Subject: [PATCH] feature: i18n support --- app/[locale]/global.css | 28 +++++++ app/[locale]/layout.tsx | 30 ++++++++ app/[locale]/not-found.tsx | 11 +++ app/{ => [locale]}/page.tsx | 2 +- app/global.css | 33 -------- app/layout.tsx | 23 +----- app/not-found.tsx | 9 +++ components/search/search.tsx | 8 +- i18n.ts | 14 ++++ messages/en.json | 8 ++ messages/zh.json | 8 ++ middleware.ts | 14 ++++ next.config.mjs | 7 +- package.json | 6 +- pnpm-lock.yaml | 143 +++++++++++++++++++++++++++++++++++ 15 files changed, 287 insertions(+), 57 deletions(-) create mode 100644 app/[locale]/global.css create mode 100644 app/[locale]/layout.tsx create mode 100644 app/[locale]/not-found.tsx rename app/{ => [locale]}/page.tsx (81%) delete mode 100644 app/global.css create mode 100644 app/not-found.tsx create mode 100644 i18n.ts create mode 100644 messages/en.json create mode 100644 messages/zh.json create mode 100644 middleware.ts diff --git a/app/[locale]/global.css b/app/[locale]/global.css new file mode 100644 index 0000000..f6c5a4b --- /dev/null +++ b/app/[locale]/global.css @@ -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; + } +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..427d778 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -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 ( + + + + {children} + + + + ); +} diff --git a/app/[locale]/not-found.tsx b/app/[locale]/not-found.tsx new file mode 100644 index 0000000..6bb1994 --- /dev/null +++ b/app/[locale]/not-found.tsx @@ -0,0 +1,11 @@ +"use client"; +import { useTranslations } from "next-intl"; + +export default function NotFound() { + const t = useTranslations("404"); + return ( +
+

{t('title')}

+
+ ); +} diff --git a/app/page.tsx b/app/[locale]/page.tsx similarity index 81% rename from app/page.tsx rename to app/[locale]/page.tsx index 3299a36..9e29a97 100644 --- a/app/page.tsx +++ b/app/[locale]/page.tsx @@ -1,7 +1,7 @@ "use client"; import { RecoilRoot } from "recoil"; -import Homepage from "../components"; +import Homepage from "../../components"; export default function Home() { return ( diff --git a/app/global.css b/app/global.css deleted file mode 100644 index 875c01e..0000000 --- a/app/global.css +++ /dev/null @@ -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; - } -} diff --git a/app/layout.tsx b/app/layout.tsx index 45ba7e3..7121949 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - - {children} - - ); +}) { + return children; } diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..9a3b830 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,9 @@ +export default function NotFound() { + return ( + + +

Not Found

+ + + ); +} diff --git a/components/search/search.tsx b/components/search/search.tsx index 99d3cdf..6643cff 100644 --- a/components/search/search.tsx +++ b/components/search/search.tsx @@ -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} /> @@ -39,7 +43,7 @@ export default function Search(props: { } id="searchBox" type="text" - placeholder="Search" + placeholder={t('placeholder')} onFocus={props.onFocus} /> diff --git a/i18n.ts b/i18n.ts new file mode 100644 index 0000000..756b407 --- /dev/null +++ b/i18n.ts @@ -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 + }; +}); \ No newline at end of file diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..9548fd1 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,8 @@ +{ + "Search": { + "placeholder": "Search" + }, + "404":{ + "title": "Page Not Found" + } +} diff --git a/messages/zh.json b/messages/zh.json new file mode 100644 index 0000000..9b7d749 --- /dev/null +++ b/messages/zh.json @@ -0,0 +1,8 @@ +{ + "Search": { + "placeholder": "搜索" + }, + "404":{ + "title": "未找到" + } +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..2eeb5b8 --- /dev/null +++ b/middleware.ts @@ -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*'] +}; \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 263122c..dac4aea 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -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; \ No newline at end of file +export default withNextIntl(nextConfig); \ No newline at end of file diff --git a/package.json b/package.json index 3fef6d2..46c8cea 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbc33cd..e2bdc9f 100644 --- a/pnpm-lock.yaml +++ b/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