@yiitap/i18n 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/index.cjs +4 -0
- package/dist/index.js +4 -0
- package/dist/index.mjs +2720 -0
- package/dist/types/__tests__/index.spec.d.ts +1 -0
- package/dist/types/generate/config/index.d.ts +0 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/languages.d.ts +9 -0
- package/dist/types/messages/de-DE.d.ts +206 -0
- package/dist/types/messages/en-US.d.ts +206 -0
- package/dist/types/messages/fr-FR.d.ts +206 -0
- package/dist/types/messages/id-ID.d.ts +206 -0
- package/dist/types/messages/ja-JP.d.ts +206 -0
- package/dist/types/messages/ko-KR.d.ts +206 -0
- package/dist/types/messages/pl-PL.d.ts +206 -0
- package/dist/types/messages/pt-BR.d.ts +206 -0
- package/dist/types/messages/ru-RU.d.ts +206 -0
- package/dist/types/messages/vi-VN.d.ts +206 -0
- package/dist/types/messages/zh-Hans.d.ts +204 -0
- package/dist/types/messages/zh-Hant.d.ts +204 -0
- package/dist/types/messages.d.ts +4 -0
- package/dist/types/types/index.d.ts +5 -0
- package/package.json +45 -0
- package/src/__tests__/index.spec.ts +7 -0
- package/src/generate/README.md +29 -0
- package/src/generate/config/index.ts +0 -0
- package/src/generate/config/languages.json +189 -0
- package/src/generate/index.js +398 -0
- package/src/generate/meta/en-US.meta.json +189 -0
- package/src/generate/meta/zh-Hans.meta.json +187 -0
- package/src/index.ts +2 -0
- package/src/languages.ts +126 -0
- package/src/messages/de-DE.ts +206 -0
- package/src/messages/en-US.ts +206 -0
- package/src/messages/fr-FR.ts +206 -0
- package/src/messages/id-ID.ts +206 -0
- package/src/messages/ja-JP.ts +205 -0
- package/src/messages/ko-KR.ts +205 -0
- package/src/messages/pl-PL.ts +206 -0
- package/src/messages/pt-BR.ts +206 -0
- package/src/messages/ru-RU.ts +206 -0
- package/src/messages/vi-VN.ts +206 -0
- package/src/messages/zh-Hans.ts +203 -0
- package/src/messages/zh-Hant.ts +203 -0
- package/src/messages.ts +37 -0
- package/src/types/index.ts +6 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"label": "English (United States)",
|
|
4
|
+
"value": "en-US",
|
|
5
|
+
"locale": "en",
|
|
6
|
+
"prompt_name": "English",
|
|
7
|
+
"example": "Hello, PileaX!",
|
|
8
|
+
"supported": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"label": "简体中文",
|
|
12
|
+
"value": "zh-Hans",
|
|
13
|
+
"locale": "zh-cn",
|
|
14
|
+
"prompt_name": "Chinese Simplified",
|
|
15
|
+
"example": "你好,PileaX!",
|
|
16
|
+
"supported": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"label": "繁體中文",
|
|
20
|
+
"value": "zh-Hant",
|
|
21
|
+
"locale": "zh-tw",
|
|
22
|
+
"base": "zh-Hans",
|
|
23
|
+
"prompt_name": "Chinese Traditional",
|
|
24
|
+
"example": "你好,PileaX!",
|
|
25
|
+
"supported": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"label": "Português (Brasil)",
|
|
29
|
+
"value": "pt-BR",
|
|
30
|
+
"locale": "pt-br",
|
|
31
|
+
"base": "en-US",
|
|
32
|
+
"prompt_name": "Portuguese",
|
|
33
|
+
"example": "Olá, PileaX!",
|
|
34
|
+
"supported": true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"label": "Español (España)",
|
|
38
|
+
"value": "es-ES",
|
|
39
|
+
"locale": "es",
|
|
40
|
+
"base": "en-US",
|
|
41
|
+
"prompt_name": "Spanish",
|
|
42
|
+
"example": "¡Hola, PileaX!",
|
|
43
|
+
"supported": false
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"label": "Français (France)",
|
|
47
|
+
"value": "fr-FR",
|
|
48
|
+
"locale": "fr",
|
|
49
|
+
"base": "en-US",
|
|
50
|
+
"prompt_name": "French",
|
|
51
|
+
"example": "Bonjour, PileaX!",
|
|
52
|
+
"supported": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"label": "Deutsch (Deutschland)",
|
|
56
|
+
"value": "de-DE",
|
|
57
|
+
"locale": "de",
|
|
58
|
+
"base": "en-US",
|
|
59
|
+
"prompt_name": "German",
|
|
60
|
+
"example": "Hallo, PileaX!",
|
|
61
|
+
"supported": true
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"label": "日本語 (日本)",
|
|
65
|
+
"value": "ja-JP",
|
|
66
|
+
"locale": "ja",
|
|
67
|
+
"base": "en-US",
|
|
68
|
+
"prompt_name": "Japanese",
|
|
69
|
+
"example": "こんにちは、PileaX!",
|
|
70
|
+
"supported": true
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"label": "한국어 (대한민국)",
|
|
74
|
+
"value": "ko-KR",
|
|
75
|
+
"locale": "ko",
|
|
76
|
+
"base": "en-US",
|
|
77
|
+
"prompt_name": "Korean",
|
|
78
|
+
"example": "안녕하세요, PileaX!",
|
|
79
|
+
"supported": true
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"label": "Русский (Россия)",
|
|
83
|
+
"value": "ru-RU",
|
|
84
|
+
"locale": "ru",
|
|
85
|
+
"base": "en-US",
|
|
86
|
+
"prompt_name": "Russian",
|
|
87
|
+
"example": "Привет, PileaX!",
|
|
88
|
+
"supported": true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"label": "Italiano (Italia)",
|
|
92
|
+
"value": "it-IT",
|
|
93
|
+
"locale": "it",
|
|
94
|
+
"base": "en-US",
|
|
95
|
+
"prompt_name": "Italian",
|
|
96
|
+
"example": "Ciao, PileaX!",
|
|
97
|
+
"supported": false
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"label": "ไทย (ประเทศไทย)",
|
|
101
|
+
"value": "th-TH",
|
|
102
|
+
"locale": "th",
|
|
103
|
+
"base": "en-US",
|
|
104
|
+
"prompt_name": "Thai",
|
|
105
|
+
"example": "สวัสดี PileaX!",
|
|
106
|
+
"supported": false
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"label": "Українська (Україна)",
|
|
110
|
+
"value": "uk-UA",
|
|
111
|
+
"locale": "uk",
|
|
112
|
+
"base": "en-US",
|
|
113
|
+
"prompt_name": "Ukrainian",
|
|
114
|
+
"example": "Привет, PileaX!",
|
|
115
|
+
"supported": false
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"label": "Tiếng Việt (Việt Nam)",
|
|
119
|
+
"value": "vi-VN",
|
|
120
|
+
"locale": "vi",
|
|
121
|
+
"base": "en-US",
|
|
122
|
+
"prompt_name": "Vietnamese",
|
|
123
|
+
"example": "Xin chào, PileaX!",
|
|
124
|
+
"supported": true
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"label": "Română (România)",
|
|
128
|
+
"value": "ro-RO",
|
|
129
|
+
"locale": "ro",
|
|
130
|
+
"base": "en-US",
|
|
131
|
+
"prompt_name": "Romanian",
|
|
132
|
+
"example": "Salut, PileaX!",
|
|
133
|
+
"supported": false
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"label": "Polski (Polish)",
|
|
137
|
+
"value": "pl-PL",
|
|
138
|
+
"locale": "pl",
|
|
139
|
+
"base": "en-US",
|
|
140
|
+
"prompt_name": "Polish",
|
|
141
|
+
"example": "Cześć, PileaX!",
|
|
142
|
+
"supported": true
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"label": "Hindi (India)",
|
|
146
|
+
"value": "hi-IN",
|
|
147
|
+
"locale": "hi",
|
|
148
|
+
"base": "en-US",
|
|
149
|
+
"prompt_name": "Hindi",
|
|
150
|
+
"example": "नमस्ते, PileaX!",
|
|
151
|
+
"supported": false
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"label": "Türkçe",
|
|
155
|
+
"value": "tr-TR",
|
|
156
|
+
"locale": "tr",
|
|
157
|
+
"base": "en-US",
|
|
158
|
+
"prompt_name": "Türkçe",
|
|
159
|
+
"example": "Selam!",
|
|
160
|
+
"supported": false
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"label": "Farsi (Iran)",
|
|
164
|
+
"value": "fa-IR",
|
|
165
|
+
"locale": "fa",
|
|
166
|
+
"base": "en-US",
|
|
167
|
+
"prompt_name": "Farsi",
|
|
168
|
+
"example": "سلام, دیفای!",
|
|
169
|
+
"supported": false
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"label": "Slovensko (Slovenija)",
|
|
173
|
+
"value": "sl-SI",
|
|
174
|
+
"locale": "sl",
|
|
175
|
+
"base": "en-US",
|
|
176
|
+
"prompt_name": "Slovensko",
|
|
177
|
+
"example": "Zdravo, PileaX!",
|
|
178
|
+
"supported": false
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"label": "Bahasa Indonesia",
|
|
182
|
+
"value": "id-ID",
|
|
183
|
+
"locale": "id",
|
|
184
|
+
"base": "en-US",
|
|
185
|
+
"prompt_name": "Indonesian",
|
|
186
|
+
"example": "Halo, PileaX!",
|
|
187
|
+
"supported": true
|
|
188
|
+
}
|
|
189
|
+
]
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import googleTranslateApi from '@vitalets/google-translate-api'
|
|
3
|
+
import bingTranslateApi from 'bing-translate-api'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import crypto from 'node:crypto'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { HttpProxyAgent } from 'http-proxy-agent'
|
|
8
|
+
import languages from './config/languages.json' with { type: 'json' }
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const agent = new HttpProxyAgent('http://127.0.0.1:7890')
|
|
12
|
+
|
|
13
|
+
const ARGS = parseArgs(process.argv)
|
|
14
|
+
|
|
15
|
+
const TRANSLATOR = 'google'
|
|
16
|
+
const BASE_LANG = ARGS.base || 'en-US'
|
|
17
|
+
const MANUAL_LANGS = ['en-US', 'zh-Hans']
|
|
18
|
+
const TARGET_LANGS = languages.filter(
|
|
19
|
+
(item) => item.supported && item.base === BASE_LANG
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const LOCALE_DIR = path.join(__dirname, '../messages')
|
|
23
|
+
const BASE_LANG_FILE = path.join(LOCALE_DIR, `${BASE_LANG}.ts`)
|
|
24
|
+
const META_DIR = path.join(__dirname, 'meta')
|
|
25
|
+
const BASE_META_FILE = path.join(META_DIR, `${BASE_LANG}.meta.json`)
|
|
26
|
+
|
|
27
|
+
// ==================================================
|
|
28
|
+
// Utility
|
|
29
|
+
// ==================================================
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const args = {}
|
|
32
|
+
for (const arg of argv.slice(2)) {
|
|
33
|
+
if (arg.startsWith('--')) {
|
|
34
|
+
const [key, val] = arg.slice(2).split('=')
|
|
35
|
+
args[key] = val === undefined ? true : val
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return args
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sleep(ms) {
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hashText(text) {
|
|
46
|
+
return crypto.createHash('md5').update(text).digest('hex').slice(0, 8)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getLanguageKey(lang) {
|
|
50
|
+
if (lang === 'zh-Hans' || lang === 'zh-Hant') {
|
|
51
|
+
return lang
|
|
52
|
+
} else {
|
|
53
|
+
return lang.split('-')[0]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sortDeep(obj) {
|
|
58
|
+
if (typeof obj !== 'object' || obj === null) return obj
|
|
59
|
+
if (Array.isArray(obj)) return obj.map(sortDeep)
|
|
60
|
+
|
|
61
|
+
return Object.keys(obj)
|
|
62
|
+
.sort((a, b) => a.localeCompare(b))
|
|
63
|
+
.reduce((acc, key) => {
|
|
64
|
+
acc[key] = sortDeep(obj[key])
|
|
65
|
+
return acc
|
|
66
|
+
}, {})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sortJsonKeys(jsonFile) {
|
|
70
|
+
if (!fs.existsSync(jsonFile)) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// sort keys
|
|
75
|
+
const json = JSON.parse(fs.readFileSync(jsonFile, 'utf8'))
|
|
76
|
+
const sorted = sortDeep(json)
|
|
77
|
+
fs.writeFileSync(jsonFile, JSON.stringify(sorted, null, 2), 'utf8')
|
|
78
|
+
|
|
79
|
+
console.log('⛵ Sorted keys:', jsonFile)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ==================================================
|
|
83
|
+
// Load/Write ts files
|
|
84
|
+
// ==================================================
|
|
85
|
+
function loadTsObject(filePath) {
|
|
86
|
+
const code = fs.readFileSync(filePath, 'utf8')
|
|
87
|
+
const match = code.match(/export\s+default\s+({[\s\S]*})\s*$/)
|
|
88
|
+
|
|
89
|
+
if (!match) {
|
|
90
|
+
throw new Error(`Invalid i18n ts file format: ${filePath}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return eval(`(${match[1]})`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeTsFile(filePath, data) {
|
|
97
|
+
const content = `export default ${serializeToTs(sortDeep(data), 2)}
|
|
98
|
+
`
|
|
99
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
100
|
+
fs.writeFileSync(filePath, content, 'utf8')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function serializeToTs(obj, indent = 2, level = 0) {
|
|
104
|
+
const pad = ' '.repeat(indent * level)
|
|
105
|
+
const padInner = ' '.repeat(indent * (level + 1))
|
|
106
|
+
|
|
107
|
+
if (typeof obj === 'string') {
|
|
108
|
+
return `'${obj.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(obj)) {
|
|
112
|
+
if (obj.length === 0) return '[]'
|
|
113
|
+
return `[\n${obj
|
|
114
|
+
.map((v) => `${padInner}${serializeToTs(v, indent, level + 1)}`)
|
|
115
|
+
.join(',\n')}\n${pad}]`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
119
|
+
const entries = Object.entries(obj)
|
|
120
|
+
if (entries.length === 0) return '{}'
|
|
121
|
+
|
|
122
|
+
return `{\n${entries
|
|
123
|
+
.map(([key, val]) => {
|
|
124
|
+
return `${padInner}${key}: ${serializeToTs(val, indent, level + 1)}`
|
|
125
|
+
})
|
|
126
|
+
.join(',\n')}\n${pad}}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// number / boolean / null
|
|
130
|
+
return String(obj)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ==================================================
|
|
134
|
+
// Translate
|
|
135
|
+
// ==================================================
|
|
136
|
+
async function translateText(text, lang) {
|
|
137
|
+
console.log(`➡️ Translating (${lang.value}): ${text}`)
|
|
138
|
+
await sleep(200 + Math.random() * 200)
|
|
139
|
+
|
|
140
|
+
switch (TRANSLATOR) {
|
|
141
|
+
case 'bing':
|
|
142
|
+
return bintTranslate(text, lang)
|
|
143
|
+
default:
|
|
144
|
+
return googleTranslate(text, lang)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function googleTranslate(text, lang) {
|
|
149
|
+
try {
|
|
150
|
+
const res = await googleTranslateApi.translate(text, {
|
|
151
|
+
to: lang.value,
|
|
152
|
+
fetchOptions: { agent },
|
|
153
|
+
})
|
|
154
|
+
return res.text
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(`❌ Translation failed:`, err.message)
|
|
157
|
+
return text // Return original text if failed
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function bintTranslate(text, lang) {
|
|
162
|
+
try {
|
|
163
|
+
const res = await bingTranslateApi.translate(
|
|
164
|
+
text,
|
|
165
|
+
getLanguageKey(BASE_LANG),
|
|
166
|
+
getLanguageKey(lang.value)
|
|
167
|
+
)
|
|
168
|
+
return res.translation
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(`❌ Translation failed:`, err.message)
|
|
171
|
+
return text // Return original text if failed
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ==================================================
|
|
176
|
+
// Sync removed key
|
|
177
|
+
// ==================================================
|
|
178
|
+
function pruneRemovedKeys(baseObj, targetObj, metaObj, parentKey = '') {
|
|
179
|
+
if (typeof targetObj !== 'object' || targetObj === null) {
|
|
180
|
+
return targetObj
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = Array.isArray(targetObj) ? [] : {}
|
|
184
|
+
|
|
185
|
+
for (const key in targetObj) {
|
|
186
|
+
const fullKey = parentKey ? `${parentKey}.${key}` : key
|
|
187
|
+
const existsInBase = baseObj && key in baseObj
|
|
188
|
+
const manual = metaObj?.[fullKey]?.manual
|
|
189
|
+
|
|
190
|
+
// ❌ base deleted
|
|
191
|
+
if (!existsInBase) {
|
|
192
|
+
if (manual) {
|
|
193
|
+
result[key] = targetObj[key]
|
|
194
|
+
console.log(`🛑 Keep manual key: ${fullKey}`)
|
|
195
|
+
} else {
|
|
196
|
+
console.log(`🗑 Remove key: ${fullKey}`)
|
|
197
|
+
delete metaObj[fullKey]
|
|
198
|
+
}
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const baseVal = baseObj[key]
|
|
203
|
+
const targetVal = targetObj[key]
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
typeof baseVal === 'object' &&
|
|
207
|
+
baseVal !== null &&
|
|
208
|
+
typeof targetVal === 'object'
|
|
209
|
+
) {
|
|
210
|
+
const pruned = pruneRemovedKeys(baseVal, targetVal, metaObj, fullKey)
|
|
211
|
+
|
|
212
|
+
// Blank object
|
|
213
|
+
if (
|
|
214
|
+
typeof pruned === 'object' &&
|
|
215
|
+
pruned !== null &&
|
|
216
|
+
Object.keys(pruned).length === 0
|
|
217
|
+
) {
|
|
218
|
+
delete metaObj[fullKey]
|
|
219
|
+
} else {
|
|
220
|
+
result[key] = pruned
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
result[key] = targetVal
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function pruneMetaByBase(baseObj, metaObj, parentKey = '') {
|
|
231
|
+
for (const key in metaObj) {
|
|
232
|
+
const path = key.split('.')
|
|
233
|
+
let cur = baseObj
|
|
234
|
+
|
|
235
|
+
for (const p of path) {
|
|
236
|
+
if (!cur || !(p in cur)) {
|
|
237
|
+
console.log(`🧹 Remove stale meta: ${key}`)
|
|
238
|
+
delete metaObj[key]
|
|
239
|
+
break
|
|
240
|
+
}
|
|
241
|
+
cur = cur[p]
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ==================================================
|
|
247
|
+
// Fill Translation
|
|
248
|
+
// ==================================================
|
|
249
|
+
let baseMeta = {}
|
|
250
|
+
|
|
251
|
+
function loadBaseMeta() {
|
|
252
|
+
if (fs.existsSync(BASE_META_FILE)) {
|
|
253
|
+
baseMeta = JSON.parse(fs.readFileSync(BASE_META_FILE, 'utf8'))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function fillTranslations(
|
|
258
|
+
baseObj,
|
|
259
|
+
targetObj,
|
|
260
|
+
metaObj,
|
|
261
|
+
lang,
|
|
262
|
+
updatedList,
|
|
263
|
+
parentKey = ''
|
|
264
|
+
) {
|
|
265
|
+
const result = { ...targetObj }
|
|
266
|
+
|
|
267
|
+
for (const key in baseObj) {
|
|
268
|
+
const fullKey = parentKey ? `${parentKey}.${key}` : key
|
|
269
|
+
const baseVal = baseObj[key]
|
|
270
|
+
const targetVal = targetObj?.[key]
|
|
271
|
+
|
|
272
|
+
if (typeof baseVal === 'string') {
|
|
273
|
+
const currentHash = hashText(baseVal)
|
|
274
|
+
const oldHash = baseMeta[fullKey]
|
|
275
|
+
const manual = metaObj[fullKey]?.manual
|
|
276
|
+
const manualUpdate = targetVal && manual
|
|
277
|
+
const needUpdate = !targetVal || oldHash !== currentHash
|
|
278
|
+
|
|
279
|
+
// missing OR changed
|
|
280
|
+
if (manualUpdate) {
|
|
281
|
+
// ignore
|
|
282
|
+
console.log(`👉🏼 Manual: ${fullKey} → ${targetVal}`)
|
|
283
|
+
} else if (needUpdate) {
|
|
284
|
+
const translated = await translateText(baseVal, lang)
|
|
285
|
+
result[key] = translated
|
|
286
|
+
|
|
287
|
+
updatedList.push({
|
|
288
|
+
key: fullKey,
|
|
289
|
+
from: baseVal,
|
|
290
|
+
to: translated,
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Update meta hash
|
|
295
|
+
baseMeta[fullKey] = currentHash
|
|
296
|
+
} else if (typeof baseVal === 'object' && baseVal !== null) {
|
|
297
|
+
result[key] = await fillTranslations(
|
|
298
|
+
baseVal,
|
|
299
|
+
targetVal || {},
|
|
300
|
+
metaObj,
|
|
301
|
+
lang,
|
|
302
|
+
updatedList,
|
|
303
|
+
fullKey
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return result
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ==================================================
|
|
312
|
+
// Main process
|
|
313
|
+
// ==================================================
|
|
314
|
+
function sortBaseFiles() {
|
|
315
|
+
if (!fs.existsSync(BASE_LANG_FILE)) return
|
|
316
|
+
|
|
317
|
+
const obj = loadTsObject(BASE_LANG_FILE)
|
|
318
|
+
const sortedObj = sortDeep(obj)
|
|
319
|
+
writeTsFile(BASE_LANG_FILE, sortedObj)
|
|
320
|
+
console.log(`⛵ Sorted base file: ${BASE_LANG_FILE}`)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function prepare() {
|
|
324
|
+
sortBaseFiles()
|
|
325
|
+
sortJsonKeys(BASE_META_FILE)
|
|
326
|
+
loadBaseMeta()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function main() {
|
|
330
|
+
// prepare
|
|
331
|
+
prepare()
|
|
332
|
+
|
|
333
|
+
// translate
|
|
334
|
+
console.log(`📌 Translator: ${TRANSLATOR}`)
|
|
335
|
+
console.log(`📌 Base language: ${BASE_LANG}`)
|
|
336
|
+
console.log(
|
|
337
|
+
`📌 Target languages: ${TARGET_LANGS.map((item) => item.value).join(', ')}`
|
|
338
|
+
)
|
|
339
|
+
console.log('')
|
|
340
|
+
|
|
341
|
+
const baseObj = loadTsObject(BASE_LANG_FILE)
|
|
342
|
+
for (const lang of TARGET_LANGS) {
|
|
343
|
+
console.log('============================================================')
|
|
344
|
+
|
|
345
|
+
if (MANUAL_LANGS.includes(lang.value)) {
|
|
346
|
+
console.log(`👉🏻 Translate manually → ${lang.value} | ${lang.prompt_name}`)
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.log(`🌍 Translating → ${lang.value} | ${lang.prompt_name}`)
|
|
351
|
+
|
|
352
|
+
// Target lang dir
|
|
353
|
+
const targetFile = path.join(LOCALE_DIR, `${lang.value}.ts`)
|
|
354
|
+
const targetObj = fs.existsSync(targetFile) ? loadTsObject(targetFile) : {}
|
|
355
|
+
|
|
356
|
+
// Target lang meta
|
|
357
|
+
const langMetaFile = path.join(META_DIR, `${lang.value}.meta.json`)
|
|
358
|
+
const metaObj = fs.existsSync(langMetaFile)
|
|
359
|
+
? JSON.parse(fs.readFileSync(langMetaFile, 'utf8'))
|
|
360
|
+
: {}
|
|
361
|
+
|
|
362
|
+
// Translate
|
|
363
|
+
const updatedList = []
|
|
364
|
+
const filled = await fillTranslations(
|
|
365
|
+
baseObj,
|
|
366
|
+
targetObj,
|
|
367
|
+
metaObj,
|
|
368
|
+
lang,
|
|
369
|
+
updatedList,
|
|
370
|
+
''
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
// Clean removed
|
|
374
|
+
const cleaned = pruneRemovedKeys(baseObj, filled, metaObj, '')
|
|
375
|
+
|
|
376
|
+
writeTsFile(targetFile, cleaned)
|
|
377
|
+
|
|
378
|
+
if (updatedList.length) {
|
|
379
|
+
console.log(`🔄 ${targetFile}: ${updatedList.length} updated`)
|
|
380
|
+
} else {
|
|
381
|
+
console.log(`✨ ${targetFile}: up to date`)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
console.log('============================================================')
|
|
385
|
+
console.log('')
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Save meta file
|
|
389
|
+
pruneMetaByBase(baseObj, baseMeta)
|
|
390
|
+
fs.mkdirSync(META_DIR, { recursive: true })
|
|
391
|
+
fs.writeFileSync(BASE_META_FILE, JSON.stringify(baseMeta, null, 2), 'utf8')
|
|
392
|
+
console.log(`📝 Updated meta: ${BASE_META_FILE}`)
|
|
393
|
+
|
|
394
|
+
console.log('')
|
|
395
|
+
console.log('🎉 All languages have been updated completely!')
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
main().catch(console.error)
|