Skip to content

Commit 2ae9574

Browse files
committed
feat: remade i18n workflow
1 parent d6783b4 commit 2ae9574

File tree

122 files changed

+2079
-2045
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

122 files changed

+2079
-2045
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.eslintrc.js
22
do not edit these files
3+
generated

.eslintrc.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module.exports = {
1515
'plugin:import/recommended',
1616
'plugin:import/typescript',
1717
'plugin:react/jsx-runtime',
18-
"plugin:react-hooks/recommended"
18+
'plugin:react-hooks/recommended',
1919
],
2020
parser: '@typescript-eslint/parser',
2121
parserOptions: {
@@ -25,8 +25,8 @@ module.exports = {
2525
ecmaVersion: 'latest',
2626
sourceType: 'module',
2727

28-
project: './tsconfig.json',
29-
tsconfigRootDir: './',
28+
project: ['./tsconfig.json', './tsconfig.node.json'],
29+
tsconfigRootDir: __dirname,
3030
},
3131
plugins: ['react', '@typescript-eslint', 'prettier', 'import'],
3232
rules: {
@@ -36,7 +36,7 @@ module.exports = {
3636
'react/self-closing-comp': 'error',
3737
'no-unused-vars': 'off',
3838
eqeqeq: 'error',
39-
"react-hooks/exhaustive-deps": "error",
39+
'react-hooks/exhaustive-deps': 'error',
4040
},
4141
settings: {
4242
react: {

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ dist-ssr
2525

2626
build/
2727
public/locales/*.json
28+
src/i18n/generated

package.json

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
"private": true,
44
"version": "0.0.0",
55
"scripts": {
6-
"dev": "node scripts/split-translations.js && vite",
7-
"build": "node scripts/split-translations.js && tsc && vite build",
8-
"build:skiptsc": "node scripts/split-translations.js && vite build",
9-
"build:azure": "node scripts/split-translations.js && yarn build:skiptsc && node post-build.mjs",
10-
"preview": "node scripts/split-translations.js && vite preview",
6+
"dev": "vite",
7+
"build": "tsc && vite build",
8+
"build:skiptsc": "vite build",
9+
"build:azure": "yarn build:skiptsc && node post-build.mjs",
10+
"preview": "vite preview",
1111
"lint": "npm-run-all --parallel lint:check:eslint lint:check:prettier",
1212
"lint:check:eslint": "eslint src/**/*",
1313
"lint:check:prettier": "prettier --check src/**/*",
@@ -17,8 +17,7 @@
1717
"scripts:update": "yarn install && npm-run-all --parallel scripts:update-operators scripts:update-operator-avatars scripts:update-prof-icons",
1818
"scripts:update-operators": "esno scripts/update-operators.ts",
1919
"scripts:update-operator-avatars": "esno scripts/update-operator-avatars.ts",
20-
"scripts:update-prof-icons": "yarn install && esno scripts/update-prof-icons.ts",
21-
"scripts:split-translations": "node scripts/split-translations.js"
20+
"scripts:update-prof-icons": "yarn install && esno scripts/update-prof-icons.ts"
2221
},
2322
"dependencies": {
2423
"@blueprintjs/core": "^4.15.1",
@@ -41,21 +40,18 @@
4140
"eslint-plugin-react": "^7.33.2",
4241
"eslint-plugin-react-hooks": "^4.6.0",
4342
"fuse.js": "^6.6.2",
44-
"i18next": "^25.0.0",
45-
"i18next-browser-languagedetector": "^8.0.4",
46-
"i18next-http-backend": "^3.0.2",
4743
"jotai": "^2.7.0",
4844
"linkify-react": "^3.0.4",
4945
"linkifyjs": "^3.0.5",
5046
"lodash-es": "^4.17.21",
5147
"maa-copilot-client": "https://github.com/MaaAssistantArknights/maa-copilot-client-ts.git#0.1.0-SNAPSHOT.824.f8ad839",
48+
"mitt": "^3.0.1",
5249
"normalize.css": "^8.0.1",
5350
"prettier": "^3.2.5",
5451
"react": "^18.0.0",
5552
"react-dom": "^18.0.0",
5653
"react-ga-neo": "^2.2.0",
5754
"react-hook-form": "^7.33.1",
58-
"react-i18next": "^15.4.1",
5955
"react-markdown": "^8.0.5",
6056
"react-rating": "^2.0.5",
6157
"react-rnd": "^10.4.1",

scripts/generate-translations.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { isObject } from 'lodash'
2+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3+
import { join } from 'node:path'
4+
import { fileURLToPath } from 'node:url'
5+
import { inspect } from 'node:util'
6+
import { Plugin } from 'vite'
7+
8+
const translationsFile = fileURLToPath(
9+
new URL('../src/i18n/translations.json', import.meta.url),
10+
)
11+
const outputDir = fileURLToPath(
12+
new URL('../src/i18n/generated', import.meta.url),
13+
)
14+
15+
export function generateTranslations(): Plugin {
16+
splitTranslations()
17+
18+
return {
19+
name: 'generate-translations',
20+
apply: 'serve',
21+
configureServer(server) {
22+
server.watcher.on('change', (filePath) => {
23+
if (filePath === translationsFile) {
24+
try {
25+
splitTranslations()
26+
} catch (e) {
27+
console.error('Failed to generate translations:', e)
28+
}
29+
}
30+
})
31+
},
32+
}
33+
}
34+
35+
function splitTranslations() {
36+
if (!existsSync(outputDir)) {
37+
mkdirSync(outputDir, { recursive: true })
38+
}
39+
40+
const translationsJson = readFileSync(translationsFile, 'utf-8')
41+
const languages = Object.keys(
42+
JSON.parse(translationsJson).essentials.language,
43+
)
44+
const essentials: Record<string, Record<string, unknown>> = {}
45+
46+
for (const language of languages) {
47+
const languageTranslations = JSON.parse(translationsJson, (key, value) => {
48+
if (
49+
isObject(value) &&
50+
Object.keys(value).some((key) => languages.includes(key))
51+
) {
52+
return value[language] || '__NOT_TRANSLATED__'
53+
}
54+
return value
55+
})
56+
57+
essentials[language] = languageTranslations.essentials
58+
delete languageTranslations.essentials
59+
60+
writeTsFile(`${language}.ts`, languageTranslations)
61+
}
62+
63+
writeTsFile(`essentials.ts`, essentials)
64+
65+
console.log(`Translations generated for ${languages.join(', ')}`)
66+
}
67+
68+
function writeTsFile(filename: string, jsonObject: Record<string, unknown>) {
69+
const filePath = join(outputDir, filename)
70+
71+
const literalObject = inspect(jsonObject, {
72+
depth: 100,
73+
maxStringLength: Infinity,
74+
breakLength: Infinity,
75+
sorted: false,
76+
compact: false,
77+
})
78+
79+
const content = `// This file is auto-generated by generate-translations.ts, do not edit it directly.
80+
export default ${literalObject} as const`
81+
82+
if (existsSync(filePath) && readFileSync(filePath, 'utf-8') === content) {
83+
// skip writing to avoid unnecessary hot reloads
84+
return
85+
}
86+
writeFileSync(filePath, content, 'utf-8')
87+
}

scripts/split-translations.js

Lines changed: 0 additions & 53 deletions
This file was deleted.

src/App.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { authAtom } from 'store/auth'
66
import { TokenManager } from 'utils/token-manager'
77

88
import { GlobalErrorBoundary } from './components/GlobalErrorBoundary'
9+
import { I18NProvider } from './i18n/I18NProvider'
910
import { FCC } from './types'
1011

1112
// jotai 在没有 Provider 时会使用默认的 store
@@ -14,18 +15,18 @@ TokenManager.setAuthSetter((v) => getDefaultStore().set(authAtom, v))
1415

1516
export const App: FCC = ({ children }) => {
1617
return (
17-
<>
18-
<SWRConfig
19-
value={{
20-
focusThrottleInterval: 1000 * 60,
21-
errorRetryInterval: 1000 * 3,
22-
errorRetryCount: 3,
23-
}}
24-
>
25-
<GlobalErrorBoundary>
18+
<SWRConfig
19+
value={{
20+
focusThrottleInterval: 1000 * 60,
21+
errorRetryInterval: 1000 * 3,
22+
errorRetryCount: 3,
23+
}}
24+
>
25+
<GlobalErrorBoundary>
26+
<I18NProvider>
2627
<BrowserRouter>{children}</BrowserRouter>
27-
</GlobalErrorBoundary>
28-
</SWRConfig>
29-
</>
28+
</I18NProvider>
29+
</GlobalErrorBoundary>
30+
</SWRConfig>
3031
)
3132
}

src/apis/announcement.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useTranslation } from 'react-i18next'
21
import useSWR from 'swr'
32

3+
import { i18n } from '../i18n/i18n'
44
import mockFile from './mock/announcements.md?url'
55

66
const isMock = process.env.NODE_ENV === 'development'
@@ -14,16 +14,14 @@ export const announcementBaseURL = isMock
1414
: announcementURL.slice(0, announcementURL.lastIndexOf('/') + 1)
1515

1616
export function useAnnouncement() {
17-
const { t } = useTranslation()
18-
1917
return useSWR<string>(
2018
announcementURL,
2119
(url) =>
2220
fetch(url)
2321
.then((res) => res.text())
2422
.catch((e) => {
2523
if ((e as Error).message === 'Failed to fetch') {
26-
throw new Error(t('apis.announcement.network_error'))
24+
throw new Error(i18n.apis.announcement.network_error)
2725
}
2826

2927
throw e

src/apis/comment.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import {
22
CommentsAreaInfo,
33
QueriesCommentsAreaRequest,
44
} from 'maa-copilot-client'
5-
import { useTranslation } from 'react-i18next'
65
import useSWRInfinite from 'swr/infinite'
76

87
import { CommentApi } from 'utils/maa-copilot-client'
98

9+
import { i18n } from '../i18n/i18n'
1010
import { CommentRating } from '../models/comment'
1111
import { Operation } from '../models/operation'
1212

@@ -24,7 +24,6 @@ export function useComments({
2424
orderBy,
2525
suspense,
2626
}: UseCommentsParams) {
27-
const { t } = useTranslation()
2827
const {
2928
data: pages,
3029
setSize,
@@ -37,7 +36,7 @@ export function useComments({
3736
}
3837

3938
if (!isFinite(+operationId)) {
40-
throw new Error(t('apis.comment.invalid_operation_id'))
39+
throw new Error(i18n.apis.comment.invalid_operation_id)
4140
}
4241

4342
return [

0 commit comments

Comments
 (0)