@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/dist/index.cjs +4 -0
  4. package/dist/index.js +4 -0
  5. package/dist/index.mjs +2720 -0
  6. package/dist/types/__tests__/index.spec.d.ts +1 -0
  7. package/dist/types/generate/config/index.d.ts +0 -0
  8. package/dist/types/index.d.ts +2 -0
  9. package/dist/types/languages.d.ts +9 -0
  10. package/dist/types/messages/de-DE.d.ts +206 -0
  11. package/dist/types/messages/en-US.d.ts +206 -0
  12. package/dist/types/messages/fr-FR.d.ts +206 -0
  13. package/dist/types/messages/id-ID.d.ts +206 -0
  14. package/dist/types/messages/ja-JP.d.ts +206 -0
  15. package/dist/types/messages/ko-KR.d.ts +206 -0
  16. package/dist/types/messages/pl-PL.d.ts +206 -0
  17. package/dist/types/messages/pt-BR.d.ts +206 -0
  18. package/dist/types/messages/ru-RU.d.ts +206 -0
  19. package/dist/types/messages/vi-VN.d.ts +206 -0
  20. package/dist/types/messages/zh-Hans.d.ts +204 -0
  21. package/dist/types/messages/zh-Hant.d.ts +204 -0
  22. package/dist/types/messages.d.ts +4 -0
  23. package/dist/types/types/index.d.ts +5 -0
  24. package/package.json +45 -0
  25. package/src/__tests__/index.spec.ts +7 -0
  26. package/src/generate/README.md +29 -0
  27. package/src/generate/config/index.ts +0 -0
  28. package/src/generate/config/languages.json +189 -0
  29. package/src/generate/index.js +398 -0
  30. package/src/generate/meta/en-US.meta.json +189 -0
  31. package/src/generate/meta/zh-Hans.meta.json +187 -0
  32. package/src/index.ts +2 -0
  33. package/src/languages.ts +126 -0
  34. package/src/messages/de-DE.ts +206 -0
  35. package/src/messages/en-US.ts +206 -0
  36. package/src/messages/fr-FR.ts +206 -0
  37. package/src/messages/id-ID.ts +206 -0
  38. package/src/messages/ja-JP.ts +205 -0
  39. package/src/messages/ko-KR.ts +205 -0
  40. package/src/messages/pl-PL.ts +206 -0
  41. package/src/messages/pt-BR.ts +206 -0
  42. package/src/messages/ru-RU.ts +206 -0
  43. package/src/messages/vi-VN.ts +206 -0
  44. package/src/messages/zh-Hans.ts +203 -0
  45. package/src/messages/zh-Hant.ts +203 -0
  46. package/src/messages.ts +37 -0
  47. 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)