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