feat: setup i18next
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m27s

This commit is contained in:
Hewston Fox
2026-03-10 03:05:06 +02:00
parent ced418544b
commit fcb8dab8a0
19 changed files with 290 additions and 4 deletions

0
.env Normal file
View File

0
.env.production Normal file
View File

0
.env.staging Normal file
View File

View File

@@ -65,7 +65,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
run: pnpm run build:staging
- name: Deploy dist to VPS dist
run: |

21
i18next.config.ts Normal file
View File

@@ -0,0 +1,21 @@
import { readdirSync } from "fs";
import { join } from "path";
export const LOCALES_PATH = join("locales");
export const LOCAL_LOCALES_PATH = join("public", LOCALES_PATH);
export const DEFAULT_LANGUAGE = "en";
export default {
locales: readdirSync(LOCAL_LOCALES_PATH)
.filter((file) => file.endsWith(".json"))
.map((locale) => locale.replace(".json", "")),
defaultLocale: DEFAULT_LANGUAGE,
fallbackLng: DEFAULT_LANGUAGE,
react: {
useSuspense: false,
},
extract: {
input: "src/**/*.{js,jsx,ts,tsx}",
output: join(LOCAL_LOCALES_PATH, "{{language}}.json"),
},
};

View File

@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build:staging": "tsc -b && vite build --mode staging",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"fmt": "oxfmt",
@@ -21,12 +22,16 @@
"@tanstack/react-router": "^1.166.3",
"@tanstack/react-router-devtools": "^1.166.3",
"arktype": "^2.2.0",
"i18next": "^25.8.17",
"i18next-http-backend": "^3.0.2",
"motion": "^12.35.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-i18next": "^16.5.6",
"tailwindcss": "^4.2.1"
},
"devDependencies": {
"@i18next-selector/vite-plugin": "^0.0.18",
"@tanstack/router-plugin": "^1.166.3",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",

166
pnpm-lock.yaml generated
View File

@@ -32,6 +32,12 @@ importers:
arktype:
specifier: ^2.2.0
version: 2.2.0
i18next:
specifier: ^25.8.17
version: 25.8.17(typescript@5.9.3)
i18next-http-backend:
specifier: ^3.0.2
version: 3.0.2
motion:
specifier: ^12.35.1
version: 12.35.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -41,10 +47,16 @@ importers:
react-dom:
specifier: ^19.2.4
version: 19.2.4(react@19.2.4)
react-i18next:
specifier: ^16.5.6
version: 16.5.6(i18next@25.8.17(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
tailwindcss:
specifier: ^4.2.1
version: 4.2.1
devDependencies:
'@i18next-selector/vite-plugin':
specifier: ^0.0.18
version: 0.0.18(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2))
'@tanstack/router-plugin':
specifier: ^1.166.3
version: 1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2))
@@ -161,6 +173,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.28.6':
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
'@babel/template@7.28.6':
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'}
@@ -332,6 +348,11 @@ packages:
cpu: [x64]
os: [win32]
'@i18next-selector/vite-plugin@0.0.18':
resolution: {integrity: sha512-jCdVJdaYDqa3dE8LCscCa7OzCN7UvUP+FDgSuc3L3mkPRlmhiXwNpCSWNUpSgGfubGBcpIE9vexHaLcS2KU0Bg==}
peerDependencies:
vite: 6 - 7
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -1159,6 +1180,18 @@ packages:
resolution: {integrity: sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w==}
engines: {node: '>=20.19'}
'@traversable/json@0.0.26':
resolution: {integrity: sha512-oXKX0eNxbbHGLjLV27nTuV0uyR6uSoWi0BF+FYJu4jXRcSsWqCHOqNjIb2x/0usKd70rnKLGyHxurlTBTpQVOw==}
peerDependencies:
'@traversable/registry': ^0.0.25
fast-check: ^3
peerDependenciesMeta:
fast-check:
optional: true
'@traversable/registry@0.0.25':
resolution: {integrity: sha512-idu2DhzoHOeqO+FZSpnDTgrFQWZL+poyxO9KozHeW7KdVqecrtYwR10vCVB/dItVdMBVZbavbNWO6PgUYN1KLg==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -1204,6 +1237,9 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
arkregex@0.0.5:
resolution: {integrity: sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==}
@@ -1278,6 +1314,9 @@ packages:
cookie-es@2.0.0:
resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==}
cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1395,11 +1434,25 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
hasBin: true
i18next-http-backend@3.0.2:
resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==}
i18next@25.8.17:
resolution: {integrity: sha512-vWtCttyn5bpOK4hWbRAe1ZXkA+Yzcn2OcACT+WJavtfGMcxzkfvXTLMeOU8MUhRmAySKjU4VVuKlo0sSGeBokA==}
peerDependencies:
typescript: ^5
peerDependenciesMeta:
typescript:
optional: true
immutable@5.1.5:
resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==}
@@ -1434,6 +1487,10 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -1579,6 +1636,15 @@ packages:
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-releases@2.0.36:
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
@@ -1633,6 +1699,22 @@ packages:
peerDependencies:
react: ^19.2.4
react-i18next@16.5.6:
resolution: {integrity: sha512-Ua7V2/efA88ido7KyK51fb8Ki8M/sRfW8LR/rZ/9ZKr2luhuTI7kwYZN5agT1rWG7aYm5G0RYE/6JR8KJoCMDw==}
peerDependencies:
i18next: '>= 25.6.2'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
typescript: ^5
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
typescript:
optional: true
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
@@ -1894,6 +1976,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -1968,9 +2053,19 @@ packages:
yaml:
optional: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
wrap-ansi@9.0.2:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
@@ -2095,6 +2190,8 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/runtime@7.28.6': {}
'@babel/template@7.28.6':
dependencies:
'@babel/code-frame': 7.29.0
@@ -2199,6 +2296,15 @@ snapshots:
'@esbuild/win32-x64@0.27.3':
optional: true
'@i18next-selector/vite-plugin@0.0.18(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@traversable/json': 0.0.26(@traversable/registry@0.0.25)
'@traversable/registry': 0.0.25
js-yaml: 4.1.1
vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- fast-check
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -2810,6 +2916,12 @@ snapshots:
'@tanstack/virtual-file-routes@1.161.4': {}
'@traversable/json@0.0.26(@traversable/registry@0.0.25)':
dependencies:
'@traversable/registry': 0.0.25
'@traversable/registry@0.0.25': {}
'@types/estree@1.0.8': {}
'@types/node@24.12.0':
@@ -2849,6 +2961,8 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
argparse@2.0.1: {}
arkregex@0.0.5:
dependencies:
'@ark/util': 0.56.0
@@ -2931,6 +3045,12 @@ snapshots:
cookie-es@2.0.0: {}
cross-fetch@4.0.0:
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
csstype@3.2.3: {}
dayjs@1.11.19: {}
@@ -3032,8 +3152,24 @@ snapshots:
has-flag@4.0.0:
optional: true
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
husky@9.1.7: {}
i18next-http-backend@3.0.2:
dependencies:
cross-fetch: 4.0.0
transitivePeerDependencies:
- encoding
i18next@25.8.17(typescript@5.9.3):
dependencies:
'@babel/runtime': 7.28.6
optionalDependencies:
typescript: 5.9.3
immutable@5.1.5:
optional: true
@@ -3059,6 +3195,10 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
jsesc@3.1.0: {}
json5@2.2.3: {}
@@ -3179,6 +3319,10 @@ snapshots:
node-addon-api@7.1.1:
optional: true
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-releases@2.0.36: {}
normalize-path@3.0.0: {}
@@ -3254,6 +3398,17 @@ snapshots:
react: 19.2.4
scheduler: 0.27.0
react-i18next@16.5.6(i18next@25.8.17(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3):
dependencies:
'@babel/runtime': 7.28.6
html-parse-stringify: 3.0.1
i18next: 25.8.17(typescript@5.9.3)
react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4)
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
typescript: 5.9.3
react@19.2.4: {}
readdirp@3.6.0:
@@ -3500,6 +3655,8 @@ snapshots:
dependencies:
is-number: 7.0.0
tr46@0.0.3: {}
tslib@2.8.1: {}
tsx@4.21.0:
@@ -3551,8 +3708,17 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.2
void-elements@3.1.0: {}
webidl-conversions@3.0.1: {}
webpack-virtual-modules@0.6.2: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
wrap-ansi@9.0.2:
dependencies:
ansi-styles: 6.2.3

1
public/locales/en.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export declare const resources: { hello: "Hello World!" };

3
public/locales/en.json Normal file
View File

@@ -0,0 +1,3 @@
{
"hello": "Hello World!"
}

1
public/locales/ru.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export declare const resources: { hello: "Привет мир" };

3
public/locales/ru.json Normal file
View File

@@ -0,0 +1,3 @@
{
"hello": "Привет мир"
}

9
src/i18next.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import "i18next";
import type { resources } from "../public/locales/en.d.ts";
declare module "i18next" {
interface CustomTypeOptions {
resources: { translation: typeof resources };
}
}

27
src/i18next.ts Normal file
View File

@@ -0,0 +1,27 @@
import i18next from "i18next";
import Backend, { type HttpBackendOptions } from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
declare const __LANGS__: string[];
declare const __LOCALES_PATH__: string;
declare const __DEFAULT_LANG__: string;
export const languages = __LANGS__.map((key) => ({
key,
label: key.toUpperCase(),
}));
i18next
.use(Backend)
.use(initReactI18next)
.init<HttpBackendOptions>({
backend: {
loadPath: `/${__LOCALES_PATH__}/{{lng}}.json`,
},
fallbackLng: __DEFAULT_LANG__,
supportedLngs: __LANGS__,
debug: import.meta.env.DEV,
interpolation: {
escapeValue: false,
},
});

View File

@@ -1,10 +1,11 @@
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./index.css";
import { routeTree } from "./routeTree.gen";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./i18next";
const router = createRouter({ routeTree });

View File

@@ -17,6 +17,12 @@ export const Route = createRootRoute({
<hr />
<Outlet />
<TanStackDevtools
config={{
defaultOpen: false,
panelLocation: "bottom",
theme: "dark",
position: "middle-left",
}}
plugins={[
{
name: "TanStack Query",

View File

@@ -1,10 +1,25 @@
import { createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import { languages } from "../i18next";
export const Route = createFileRoute("/")({
component: () => {
const { t, i18n } = useTranslation();
const [langIdx, setLangIdx] = useState(0);
useEffect(() => {
i18n.changeLanguage(languages[langIdx].key);
}, [langIdx, i18n]);
return (
<div className="p-2">
<h3>Welcome Home!</h3>
<h3>
{t("hello")}
<button onClick={() => setLangIdx((langIdx + 1) % languages.length)}>
{i18n.language}
</button>
</h3>
</div>
);
},

11
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
interface ViteTypeOptions {
strictImportMetaEnv: unknown;
}
interface ImportMetaEnv {
MODE: "development" | "production" | "staging";
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -1,4 +1,7 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": {
"resolveJsonModule": true
}
}

View File

@@ -3,9 +3,18 @@ import react from "@vitejs/plugin-react-swc";
import tailwindcss from "@tailwindcss/vite";
import tanstackRouter from "@tanstack/router-plugin/vite";
import { devtools as tanstackDevtools } from "@tanstack/devtools-vite";
import { i18nextVitePlugin } from "@i18next-selector/vite-plugin";
import i18nextConfig, {
DEFAULT_LANGUAGE,
LOCALES_PATH,
LOCAL_LOCALES_PATH,
} from "./i18next.config";
export default defineConfig({
plugins: [
i18nextVitePlugin({
sourceDir: LOCAL_LOCALES_PATH,
}),
tanstackDevtools({
removeDevtoolsOnBuild: true,
logging: true,
@@ -32,4 +41,9 @@ export default defineConfig({
react(),
tailwindcss(),
],
define: {
__LANGS__: JSON.stringify(i18nextConfig.locales),
__LOCALES_PATH__: JSON.stringify(LOCALES_PATH),
__DEFAULT_LANG__: JSON.stringify(DEFAULT_LANGUAGE),
},
});