diff --git a/next-i18next.config.js b/next-i18next.config.js
new file mode 100644
index 00000000..7cf82e97
--- /dev/null
+++ b/next-i18next.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ i18n: {
+ defaultLocale: "en",
+ locales: ["en", "zh"],
+ },
+}
diff --git a/next.config.js b/next.config.js
index 4d7c76a8..1341a1fa 100644
--- a/next.config.js
+++ b/next.config.js
@@ -5,9 +5,11 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
})
const execSync = require("child_process").execSync
+const { i18n } = require("./next-i18next.config")
const cache = require("next-pwa/cache")
const withPWA = require("next-pwa")({
+ disable: process.env.NODE_ENV === "development",
dest: "public",
publicExcludes: ["*"],
runtimeCaching: [
@@ -110,5 +112,10 @@ module.exports = withBundleAnalyzer(
ENV_OPENSEA_API_KEY: process.env.OPENSEA_API_KEY,
ENV_POAP_API_KEY: process.env.POAP_API_KEY,
},
+
+ i18n: {
+ ...i18n,
+ localeDetection: false,
+ },
}),
)
diff --git a/package.json b/package.json
index c7af6e4c..1e9ae8ce 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"emoji-mart": "5.5.2",
"fast-average-color": "^9.3.0",
"fast-deep-equal": "3.1.3",
+ "i18next": "^22.4.10",
"idb-keyval": "6.2.0",
"immer": "9.0.19",
"ioredis": "5.3.1",
@@ -63,6 +64,7 @@
"mdast-util-toc": "6.1.1",
"nanoid": "4.0.1",
"next": "13.1.6",
+ "next-i18next": "^13.1.5",
"nextjs-progressbar": "0.0.16",
"nodejieba": "2.6.0",
"pinyin": "3.0.0-alpha.5",
@@ -71,6 +73,7 @@
"react-dom": "18.2.0",
"react-hook-form": "7.43.1",
"react-hot-toast": "2.4.0",
+ "react-i18next": "^12.1.5",
"react-infinite-scroller": "1.2.6",
"react-scroll": "1.8.9",
"react-sortablejs": "6.1.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c861a6cc..f69c74cf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,6 +71,7 @@ specifiers:
fast-average-color: ^9.3.0
fast-deep-equal: 3.1.3
husky: 8.0.3
+ i18next: ^22.4.10
idb-keyval: 6.2.0
immer: 9.0.19
ioredis: 5.3.1
@@ -81,6 +82,7 @@ specifiers:
mdast-util-toc: 6.1.1
nanoid: 4.0.1
next: 13.1.6
+ next-i18next: ^13.1.5
next-pwa: ^5.6.0
nextjs-progressbar: 0.0.16
nodejieba: 2.6.0
@@ -92,6 +94,7 @@ specifiers:
react-dom: 18.2.0
react-hook-form: 7.43.1
react-hot-toast: 2.4.0
+ react-i18next: ^12.1.5
react-infinite-scroller: 1.2.6
react-scroll: 1.8.9
react-sortablejs: 6.1.4
@@ -165,6 +168,7 @@ dependencies:
emoji-mart: 5.5.2
fast-average-color: 9.3.0
fast-deep-equal: 3.1.3
+ i18next: 22.4.10
idb-keyval: 6.2.0
immer: 9.0.19
ioredis: 5.3.1
@@ -174,6 +178,7 @@ dependencies:
mdast-util-toc: 6.1.1
nanoid: 4.0.1
next: 13.1.6_biqbaboplfbrettd7655fr4n2y
+ next-i18next: 13.1.5_ict23jwgb5pzpgamw2nvwotxa4
nextjs-progressbar: 0.0.16_next@13.1.6+react@18.2.0
nodejieba: 2.6.0
pinyin: 3.0.0-alpha.5
@@ -182,6 +187,7 @@ dependencies:
react-dom: 18.2.0_react@18.2.0
react-hook-form: 7.43.1_react@18.2.0
react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y
+ react-i18next: 12.1.5_3yopsigl4h4eb2nqrqfsy65uwi
react-infinite-scroller: 1.2.6_react@18.2.0
react-scroll: 1.8.9_biqbaboplfbrettd7655fr4n2y
react-sortablejs: 6.1.4_pkvhgat2fktt3g4eqjjiisgkie
@@ -4015,6 +4021,13 @@ packages:
dependencies:
'@types/unist': 2.0.6
+ /@types/hoist-non-react-statics/3.3.1:
+ resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
+ dependencies:
+ '@types/react': 18.0.28
+ hoist-non-react-statics: 3.3.2
+ dev: false
+
/@types/js-yaml/4.0.5:
resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==}
dev: true
@@ -6151,6 +6164,11 @@ packages:
dependencies:
browserslist: 4.21.4
+ /core-js/3.28.0:
+ resolution: {integrity: sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw==}
+ requiresBuild: true
+ dev: false
+
/cosmiconfig/6.0.0:
resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==}
engines: {node: '>=8'}
@@ -8125,6 +8143,12 @@ packages:
react-is: 16.13.1
dev: false
+ /html-parse-stringify/3.0.1:
+ resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
+ dependencies:
+ void-elements: 3.1.0
+ dev: false
+
/html-void-elements/2.0.1:
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
dev: false
@@ -8161,6 +8185,16 @@ packages:
hasBin: true
dev: true
+ /i18next-fs-backend/2.1.1:
+ resolution: {integrity: sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA==}
+ dev: false
+
+ /i18next/22.4.10:
+ resolution: {integrity: sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA==}
+ dependencies:
+ '@babel/runtime': 7.20.13
+ dev: false
+
/idb-keyval/6.2.0:
resolution: {integrity: sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==}
dependencies:
@@ -9792,6 +9826,26 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
+ /next-i18next/13.1.5_ict23jwgb5pzpgamw2nvwotxa4:
+ resolution: {integrity: sha512-gULKQNQmFYIW6C45cb5ZOHa+sw8XUkibFHTqRqrcfbAa1fcBCh3TgX1KtzA0vzeh9R93BkkvFhm+FuhO3kR9jQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ i18next: ^22.0.6
+ next: '>= 12.0.0'
+ react: '>= 17.0.2'
+ react-i18next: ^12.1.5
+ dependencies:
+ '@babel/runtime': 7.20.13
+ '@types/hoist-non-react-statics': 3.3.1
+ core-js: 3.28.0
+ hoist-non-react-statics: 3.3.2
+ i18next: 22.4.10
+ i18next-fs-backend: 2.1.1
+ next: 13.1.6_biqbaboplfbrettd7655fr4n2y
+ react: 18.2.0
+ react-i18next: 12.1.5_3yopsigl4h4eb2nqrqfsy65uwi
+ dev: false
+
/next-pwa/5.6.0_esbuild@0.17.8+next@13.1.6:
resolution: {integrity: sha512-XV8g8C6B7UmViXU8askMEYhWwQ4qc/XqJGnexbLV68hzKaGHZDMtHsm2TNxFcbR7+ypVuth/wwpiIlMwpRJJ5A==}
peerDependencies:
@@ -10758,6 +10812,26 @@ packages:
- csstype
dev: false
+ /react-i18next/12.1.5_3yopsigl4h4eb2nqrqfsy65uwi:
+ resolution: {integrity: sha512-7PQAv6DA0TcStG96fle+8RfTwxVbHVlZZJPoEszwUNvDuWpGldJmNWa3ZPesEsZQZGF6GkzwvEh6p57qpFD2gQ==}
+ peerDependencies:
+ i18next: '>= 19.0.0'
+ react: '>= 16.8.0'
+ react-dom: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.20.13
+ html-parse-stringify: 3.0.1
+ i18next: 22.4.10
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ dev: false
+
/react-infinite-scroller/1.2.6_react@18.2.0:
resolution: {integrity: sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==}
peerDependencies:
@@ -12832,6 +12906,11 @@ packages:
- terser
dev: true
+ /void-elements/3.1.0:
+ resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/w3c-keyname/2.2.6:
resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==}
dev: false
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
new file mode 100644
index 00000000..0222f651
--- /dev/null
+++ b/public/locales/en/common.json
@@ -0,0 +1,3 @@
+{
+ "easy": "Easy"
+}
\ No newline at end of file
diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json
new file mode 100644
index 00000000..0bddd792
--- /dev/null
+++ b/public/locales/zh/common.json
@@ -0,0 +1,3 @@
+{
+ "easy": "简单"
+}
\ No newline at end of file
diff --git a/src/lib/language-detector.ts b/src/lib/language-detector.ts
new file mode 100644
index 00000000..90080832
--- /dev/null
+++ b/src/lib/language-detector.ts
@@ -0,0 +1,5 @@
+import type { GetServerSidePropsContext } from "next"
+
+export const languageDetector = (ctx: GetServerSidePropsContext) => {
+ return ctx.req.headers["accept-language"]?.split(",")[0].split("-")[0] || "en"
+}
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 1f44e21c..a8f285b4 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -11,6 +11,7 @@ import { NotificationModal } from "@crossbell/notification"
import { InitContractProvider } from "@crossbell/contract"
import NextNProgress from "nextjs-progressbar"
import { Network } from "crossbell.js"
+import { appWithTranslation } from "next-i18next"
import { IPFS_GATEWAY } from "~/lib/env"
import { toGateway } from "~/lib/ipfs-parser"
@@ -78,4 +79,4 @@ function MyApp({ Component, pageProps }: any) {
// return { ...appProps }
// }
-export default MyApp
+export default appWithTranslation(MyApp)
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index d659c06a..4d2350f0 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -33,6 +33,9 @@ import {
XShopLogo,
CrossbellChainLogo,
} from "@crossbell/ui"
+import { useTranslation } from "next-i18next"
+import { serverSideTranslations } from "next-i18next/serverSideTranslations"
+import { languageDetector } from "~/lib/language-detector"
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const queryClient = new QueryClient()
@@ -40,6 +43,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
return {
props: {
+ ...(await serverSideTranslations(languageDetector(ctx), ["common"])),
dehydratedState: dehydrate(queryClient),
},
}
@@ -49,6 +53,7 @@ export default function Home() {
const isConnected = useAccountState((s) => !!s.computed.account)
const router = useRouter()
const showcaseSites = useGetSites(showcase)
+ const { t } = useTranslation("common")
const tryNow = useConnectedAction(() => {
router.push("/dashboard")
@@ -60,7 +65,7 @@ export default function Home() {
src: "/assets/easy.png",
},
icon: ,
- title: "Easy",
+ title: t("easy"),
text: (
<>
Connect with your Web3 Wallet or{" "}