diff --git a/next-i18next.config.js b/next-i18next.config.js new file mode 100644 index 0000000..b9a1286 --- /dev/null +++ b/next-i18next.config.js @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable no-undef */ + +const path = require('path'); + +module.exports = { + i18n: { + locales: ['en', 'es'], + defaultLocale: 'en', + }, + localePath: typeof window === 'undefined' ? path.resolve('./public/locales') : '/locales', +}; diff --git a/next.config.mjs b/next.config.mjs index 4678774..404aa72 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,10 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; + +const nextConfig = { + i18n: { + locales: ['en', 'es'], + defaultLocale: 'en', + }, +}; export default nextConfig; diff --git a/package.json b/package.json index e82fb5b..6079f6d 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,7 @@ { - "name": "web3-react-boilerplate", + "name": "ZKchainHub", "private": true, "version": "0.0.0", - "type": "module", "license": "MIT", "author": "Wonderland", "engines": { @@ -41,9 +40,12 @@ "@next/eslint-plugin-next": "14.1.3", "@rainbow-me/rainbowkit": "2.0.2", "@tanstack/react-query": "5.28.0", + "i18next": "23.7.6", "next": "14.1.3", + "next-i18next": "15.2.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-i18next": "14.0.0", "react-router-dom": "6.15.0", "viem": "2.8.6", "wagmi": "2.5.7" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c0d77b..fcd394e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,15 +35,24 @@ dependencies: '@tanstack/react-query': specifier: 5.28.0 version: 5.28.0(react@18.2.0) + i18next: + specifier: 23.7.6 + version: 23.7.6 next: specifier: 14.1.3 version: 14.1.3(@babel/core@7.24.1)(react-dom@18.2.0)(react@18.2.0) + next-i18next: + specifier: 15.2.0 + version: 15.2.0(i18next@23.7.6)(next@14.1.3)(react-i18next@14.0.0)(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + react-i18next: + specifier: 14.0.0 + version: 14.0.0(i18next@23.7.6)(react-dom@18.2.0)(react-native@0.73.6)(react@18.2.0) react-router-dom: specifier: 6.15.0 version: 6.15.0(react-dom@18.2.0)(react@18.2.0) @@ -1529,6 +1538,12 @@ packages: dependencies: regenerator-runtime: 0.14.1 + /@babel/runtime@7.24.8: + resolution: {integrity: sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + /@babel/template@7.24.0: resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} @@ -2570,7 +2585,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 '@mui/utils': 5.15.14(@types/react@18.2.15)(react@18.2.0) '@types/react': 18.2.15 prop-types: 15.8.1 @@ -2590,7 +2605,7 @@ packages: '@emotion/styled': optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 '@emotion/cache': 11.11.0 '@emotion/react': 11.11.3(@types/react@18.2.15)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.15)(react@18.2.0) @@ -3730,6 +3745,13 @@ packages: resolution: {integrity: sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==} dev: false + /@types/hoist-non-react-statics@3.3.5: + resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} + dependencies: + '@types/react': 18.2.15 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -4911,7 +4933,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 cosmiconfig: 7.1.0 resolve: 1.22.8 dev: false @@ -5486,6 +5508,11 @@ packages: dependencies: browserslist: 4.23.0 + /core-js@3.37.1: + resolution: {integrity: sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==} + requiresBuild: true + dev: false + /core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} dev: true @@ -5739,7 +5766,7 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 dev: false /dayjs@1.11.10: @@ -5969,7 +5996,7 @@ packages: /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 csstype: 3.1.3 dev: false @@ -7420,13 +7447,23 @@ packages: /i18next-browser-languagedetector@7.2.0: resolution: {integrity: sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 + dev: false + + /i18next-fs-backend@2.3.1: + resolution: {integrity: sha512-tvfXskmG/9o+TJ5Fxu54sSO5OkY6d+uMn+K6JiUGLJrwxAVfer+8V3nU8jq3ts9Pe5lXJv4b1N7foIjJ8Iy2Gg==} dev: false /i18next@22.5.1: resolution: {integrity: sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 + dev: false + + /i18next@23.7.6: + resolution: {integrity: sha512-O66BhXBw0fH4bEJMA0/klQKPEbcwAp5wjXEL803pdAynNbg2f4qhLIYlNHJyE7icrL6XmSZKPYaaXwy11kJ6YQ==} + dependencies: + '@babel/runtime': 7.24.8 dev: false /iconv-lite@0.6.3: @@ -8887,7 +8924,7 @@ packages: /media-query-parser@2.0.2: resolution: {integrity: sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 dev: false /memoize-one@5.2.1: @@ -8995,7 +9032,7 @@ packages: resolution: {integrity: sha512-21GQVd0pp2nACoK0C2PL8mBsEhIFUFFntYrWRlYNHtPQoqDzddrPEIgkyaABGXGued+dZoBlFQl+LASlmmfkvw==} engines: {node: '>=18'} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 dev: false /metro-source-map@0.80.6: @@ -9302,6 +9339,26 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: false + /next-i18next@15.2.0(i18next@23.7.6)(next@14.1.3)(react-i18next@14.0.0)(react@18.2.0): + resolution: {integrity: sha512-Rl5yZ4oGffsB0AjRykZ5PzNQ2M6am54MaMayldGmH/UKZisrIxk2SKEPJvaHhKlWe1qgdNi2FkodwK8sEjfEmg==} + engines: {node: '>=14'} + peerDependencies: + i18next: '>= 23.7.13' + next: '>= 12.0.0' + react: '>= 17.0.2' + react-i18next: '>= 13.5.0' + dependencies: + '@babel/runtime': 7.24.8 + '@types/hoist-non-react-statics': 3.3.5 + core-js: 3.37.1 + hoist-non-react-statics: 3.3.2 + i18next: 23.7.6 + i18next-fs-backend: 2.3.1 + next: 14.1.3(@babel/core@7.24.1)(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-i18next: 14.0.0(i18next@23.7.6)(react-dom@18.2.0)(react-native@0.73.6)(react@18.2.0) + dev: false + /next@14.1.3(@babel/core@7.24.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-oexgMV2MapI0UIWiXKkixF8J8ORxpy64OuJ/J9oVUmIthXOUCcuVEZX+dtpgq7wIfIqtBwQsKEDXejcjTsan9g==} engines: {node: '>=18.17.0'} @@ -10098,7 +10155,7 @@ packages: react-native: optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 html-parse-stringify: 3.0.1 i18next: 22.5.1 react: 18.2.0 @@ -10106,6 +10163,27 @@ packages: react-native: 0.73.6(@babel/core@7.24.1)(@babel/preset-env@7.24.1)(react@18.2.0) dev: false + /react-i18next@14.0.0(i18next@23.7.6)(react-dom@18.2.0)(react-native@0.73.6)(react@18.2.0): + resolution: {integrity: sha512-OCrS8rHNAmnr8ggGRDxjakzihrMW7HCbsplduTm3EuuQ6fyvWGT41ksZpqbduYoqJurBmEsEVZ1pILSUWkHZng==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.24.8 + html-parse-stringify: 3.0.1 + i18next: 23.7.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-native: 0.73.6(@babel/core@7.24.1)(@babel/preset-env@7.24.1)(react@18.2.0) + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -10400,7 +10478,7 @@ packages: /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.8 /regexp.prototype.flags@1.5.2: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..5451f12 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,58 @@ +{ + "HOME": { + "title": "ZKsync Ecosystem", + "gasPrice": "Gas Price", + "transfer": "ERC-20 Transfer", + "lockedAssets": "Locked assets in shared bridge", + "DASHBOARD": { + "title": "Chain list", + "chain": "Chain", + "chainId": "Chain ID", + "nativeToken": "Native token", + "tvl": "TVL - L1", + "type": "Type", + "search": "Search by chain name or id..." + } + }, + "CHAINPAGE": { + "website": "Website", + "explorer": "Explorer", + "launchDate": "Launch date", + "environment": "Environment", + "nativeToken": "Native token", + "CHAININFORMATION": { + "title": "Chain information", + "chainType": "Chain type", + "lastBlock": "Last block", + "lastBlockVerified": "Last block verified", + "transactionsPerSecond": "Transactions per second", + "totalBatchesCommitted": "Total batches committed", + "totalBatchesVerified": "Total batches verified", + "averageBlockTime": "Average block time" + }, + "ZKCHAINTVL": { + "title": "ZKchain TVL" + }, + "RPC": { + "title": "RPC", + "status": "Status" + }, + "FEEPARAMS": { + "batch": "Batch Overhead L1 Gas", + "compute": "Compute Overhead Part", + "maxGasBatch": "Max Gas per Batch" + } + }, + "FOOTER": { + "docs": "Documentation", + "github": "GitHub", + "madeWithLove": "Made with ❤️ by" + }, + "DISCLAIMER": { + "disclaimer": "Disclaimer: This application is currently in beta. Please proceed at your own risk. Any funds lost through its use are non-recoverable." + }, + "LOCALES": { + "en": "English", + "es": "Spanish" + } +} diff --git a/public/locales/es/common.json b/public/locales/es/common.json new file mode 100644 index 0000000..6c8e324 --- /dev/null +++ b/public/locales/es/common.json @@ -0,0 +1,58 @@ +{ + "HOME": { + "title": "Ecosistema ZKsync", + "gasPrice": "Precio del gas", + "transfer": "Transferencia ERC-20", + "lockedAssets": "Activos bloqueados en puente compartido", + "DASHBOARD": { + "title": "Lista de cadenas", + "chain": "Cadena", + "chainId": "ID de cadena", + "nativeToken": "Token nativo", + "tvl": "TVL - L1", + "type": "Tipo", + "search": "Buscar por nombre o ID de la cadena..." + } + }, + "CHAINPAGE": { + "website": "Sitio web", + "explorer": "Explorador", + "launchDate": "Fecha de lanzamiento", + "environment": "Entorno", + "nativeToken": "Token nativo", + "CHAININFORMATION": { + "title": "Información de la cadena", + "chainType": "Tipo de cadena", + "lastBlock": "Último bloque", + "lastBlockVerified": "Último bloque verificado", + "transactionsPerSecond": "Transacciones por segundo", + "totalBatchesCommitted": "Total de lotes comprometidos", + "totalBatchesVerified": "Total de lotes verificados", + "averageBlockTime": "Tiempo promedio de bloque" + }, + "ZKCHAINTVL": { + "title": "TVL de ZKchain" + }, + "RPC": { + "title": "RPC", + "status": "Estado" + }, + "FEEPARAMS": { + "batch": "Sobrecarga de lote L1 Gas", + "compute": "Parte de sobrecarga de cómputo", + "maxGasBatch": "Máximo gas por lote" + } + }, + "FOOTER": { + "docs": "Documentación", + "github": "GitHub", + "madeWithLove": "Hecho con ❤️ por" + }, + "DISCLAIMER": { + "disclaimer": "Advertencia: Esta aplicación está actualmente en beta. Por favor, proceda bajo su propio riesgo. Cualquier fondo perdido a través de su uso no es recuperable." + }, + "LOCALES": { + "en": "Inglés", + "es": "Español" + } +} diff --git a/src/components/Disclaimer.tsx b/src/components/Disclaimer.tsx index 81d4764..d201257 100644 --- a/src/components/Disclaimer.tsx +++ b/src/components/Disclaimer.tsx @@ -1,14 +1,14 @@ import { styled, Box, Typography } from '@mui/material'; +import { useTranslation } from 'next-i18next'; import { DISCLAIMER_HEIGHT } from '~/utils'; export const Disclaimer = () => { + const { t } = useTranslation(); + return ( - - Disclaimer: This application is currently in beta. Please proceed at your own risk. Any funds lost through its - use are non-recoverable. - + {t('DISCLAIMER.disclaimer')} ); }; diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 94fb92a..f4bdda0 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,16 +1,20 @@ +import { useTranslation } from 'next-i18next'; + interface SearchBarProps { value: string; onChange: (value: string) => void; } export const SearchBar = ({ value, onChange }: SearchBarProps) => { + const { t } = useTranslation(); + const handleChange = (event: React.ChangeEvent) => { onChange(event.target.value); }; return (
- +
); }; diff --git a/src/components/Table.tsx b/src/components/Table.tsx index c1de352..4aec5f6 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from 'next-i18next'; + import { EcosystemChainData } from '~/types'; import { formatDataNumber } from '~/utils'; @@ -6,14 +8,16 @@ interface TableProps { } export const Table = ({ chains }: TableProps) => { + const { t } = useTranslation(); + return ( - - - - - + + + + + {chains?.map((data, index) => { diff --git a/src/components/TitleBanner.tsx b/src/components/TitleBanner.tsx index 36a52f4..3d650d8 100644 --- a/src/components/TitleBanner.tsx +++ b/src/components/TitleBanner.tsx @@ -1,9 +1,14 @@ +import { useTranslation } from 'next-i18next'; + export const TitleBanner = () => { + const { t } = useTranslation(); + return (
- zkSync Ecosystem + {t('HOME.title')} +
- Gas Price: 10 wei · ERC-20 Transfer: $10 + {`${t('HOME.gasPrice')}: 10 wei ${t('HOME.transfer')} $10`}
); diff --git a/src/containers/Dashboard/index.tsx b/src/containers/Dashboard/index.tsx index e968b94..eb023e4 100644 --- a/src/containers/Dashboard/index.tsx +++ b/src/containers/Dashboard/index.tsx @@ -1,8 +1,11 @@ +import { useTranslation } from 'next-i18next'; import { useState } from 'react'; + import { SearchBar, Table, Title } from '~/components'; import { useData } from '~/hooks'; export const Dashboard = () => { + const { t } = useTranslation(); const { ecosystemData } = useData(); const [searchTerm, setSearchTerm] = useState(''); @@ -18,7 +21,7 @@ export const Dashboard = () => { return (
- + <Title title={t('HOME.DASHBOARD.title')} /> <SearchBar value={searchTerm} onChange={handleChange} /> </header> diff --git a/src/containers/Footer/index.tsx b/src/containers/Footer/index.tsx index 5d98840..45f49cb 100644 --- a/src/containers/Footer/index.tsx +++ b/src/containers/Footer/index.tsx @@ -1,14 +1,17 @@ +import { useTranslation } from 'next-i18next'; import { styled } from '@mui/material/styles'; -import { useCustomTheme } from '~/hooks/useContext/useTheme'; +import { useCustomTheme } from '~/hooks'; import { FOOTER_HEIGHT } from '~/utils'; export const Footer = () => { + const { t } = useTranslation(); + return ( <FooterContainer> <h1>Footer</h1> <Subtitle> - <p>Made with 💜 by</p> + <p>{t('FOOTER.madeWithLove')}</p> <a href='https://defi.sucks'>Wonderland</a> </Subtitle> </FooterContainer> diff --git a/src/containers/LockedAssets/index.tsx b/src/containers/LockedAssets/index.tsx index 2b9a6a3..73f4b89 100644 --- a/src/containers/LockedAssets/index.tsx +++ b/src/containers/LockedAssets/index.tsx @@ -1,15 +1,18 @@ +import { useTranslation } from 'next-i18next'; + import { TotalValueLocked, Title } from '~/components'; import { useData } from '~/hooks'; import { formatDataNumber } from '~/utils'; export const LockedAssets = () => { + const { t } = useTranslation(); const { ecosystemData } = useData(); return ( <section> {ecosystemData && ( <> - <Title title={`Locked assets in shared bridge: ${formatDataNumber(ecosystemData.total, 0, true, true)}`} /> + <Title title={`${t('HOME.lockedAssets')}: ${formatDataNumber(ecosystemData.total, 0, true, true)}`}/> <TotalValueLocked tvl={ecosystemData.tvl} /> </> )} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index bc4ba68..71e725f 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,4 +1,6 @@ import { AppProps } from 'next/app'; +import { appWithTranslation } from 'next-i18next'; + import { Providers } from '~/providers'; import Layout from './layout'; @@ -12,4 +14,4 @@ const Home = ({ Component, pageProps }: AppProps) => { ); }; -export default Home; +export default appWithTranslation(Home); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index d753b21..1320a60 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,7 +1,17 @@ import Head from 'next/head'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { GetStaticProps, GetStaticPropsContext } from 'next'; import { Landing } from '~/containers'; +export const getStaticProps: GetStaticProps = async ({ locale }: GetStaticPropsContext) => { + return { + props: { + ...(await serverSideTranslations(locale ?? 'en', ['common'])), + }, + }; +}; + const Ecosystem = () => { return ( <>
ChainChain IDNative tokenTVL - L1Type{t('HOME.DASHBOARD.chain')}{t('HOME.DASHBOARD.chainId')}{t('HOME.DASHBOARD.nativeToken')}{t('HOME.DASHBOARD.tvl')}{t('HOME.DASHBOARD.type')}