Skip to content

Commit 1ffb905

Browse files
committed
Merge branch 'main' into qr
2 parents 47c45f9 + 3cc4f3a commit 1ffb905

30 files changed

+384
-196
lines changed

.github/workflows/crowdin-export.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
name: Export Crowdin
1+
name: Export translations from Crowdin
22
on: [workflow_dispatch]
33
jobs:
44
sync:
55
runs-on: ubuntu-latest
66
env:
77
TIMER_CROWDIN_AUTH: ${{ secrets.TIMER_CROWDIN_AUTH }}
88
steps:
9-
- name: Checkout
9+
- name: Checkout branch i18n-export
1010
uses: actions/checkout@v4
1111
with:
12-
fetch-depth: 0
12+
fetch-depth: 1
1313
token: ${{secrets.GITHUB_TOKEN}}
14+
ref: i18n-export
1415
- name: Test using Node.js
1516
uses: actions/setup-node@v1
1617
with:
@@ -23,7 +24,7 @@ jobs:
2324
run: ts-node ./script/crowdin/export-translation.ts
2425
- name: Commit change
2526
run: |
26-
git config --global user.name 'sheepzh'
27+
git config --global user.name 'Crowdin Bot'
2728
git config --global user.email 'returnzhy1996@outlook.com'
2829
git add .
2930
git commit -m "Export translations by Github Action"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@ script/icons_history
2121

2222
coverage
2323

24+
.crowdin-temp/
25+
.crowdin-temp.zip
26+
2427
package-lock.json
2528
aaa

package.json

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,23 @@
2727
"@babel/plugin-transform-modules-commonjs": "^7.24.8",
2828
"@babel/preset-env": "^7.25.4",
2929
"@crowdin/crowdin-api-client": "^1.35.0",
30-
"@types/chrome": "0.0.270",
30+
"@types/chrome": "0.0.271",
3131
"@types/copy-webpack-plugin": "^8.0.1",
32+
"@types/decompress": "^4.2.7",
3233
"@types/echarts": "^4.9.22",
3334
"@types/generate-json-webpack-plugin": "^0.3.7",
34-
"@types/jest": "^29.5.12",
35-
"@types/node": "^22.5.1",
35+
"@types/jest": "^29.5.13",
36+
"@types/node": "^22.5.5",
3637
"@types/psl": "^1.1.3",
3738
"@types/punycode": "^2.1.4",
3839
"@types/webpack": "^5.28.5",
3940
"@types/webpack-bundle-analyzer": "^4.7.0",
40-
"@vue/babel-plugin-jsx": "^1.2.2",
41-
"babel-loader": "^9.1.3",
41+
"@vue/babel-plugin-jsx": "^1.2.5",
42+
"babel-loader": "^9.2.1",
4243
"copy-webpack-plugin": "^12.0.2",
4344
"css-loader": "^7.1.2",
44-
"eslint": "^9.9.1",
45+
"decompress": "^4.2.1",
46+
"eslint": "^9.10.0",
4547
"filemanager-webpack-plugin": "^8.0.0",
4648
"generate-json-webpack-plugin": "^2.0.0",
4749
"html-webpack-plugin": "^5.6.0",
@@ -56,23 +58,23 @@
5658
"ts-node": "^10.9.2",
5759
"tsconfig-paths": "^4.2.0",
5860
"tslib": "^2.7.0",
59-
"typescript": "5.5.4",
61+
"typescript": "5.6.2",
6062
"url-loader": "^4.1.1",
6163
"webpack": "^5.94.0",
6264
"webpack-bundle-analyzer": "^4.10.2",
6365
"webpack-cli": "^5.1.4"
6466
},
6567
"dependencies": {
6668
"@element-plus/icons-vue": "^2.3.1",
67-
"@vueuse/core": "^11.0.3",
69+
"@vueuse/core": "^11.1.0",
6870
"countup.js": "^2.8.0",
6971
"echarts": "^5.5.1",
70-
"element-plus": "2.8.1",
72+
"element-plus": "2.8.3",
7173
"js-base64": "^3.7.7",
7274
"punycode": "^2.3.1",
7375
"stream-browserify": "^3.0.0",
74-
"vue": "^3.4.38",
75-
"vue-router": "^4.4.3"
76+
"vue": "^3.5.6",
77+
"vue-router": "^4.4.5"
7678
},
7779
"engines": {
7880
"node": ">=20"

script/crowdin/client.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import Crowdin, {
88
StringTranslationsModel,
99
UploadStorageModel,
1010
} from '@crowdin/crowdin-api-client'
11-
import axios from 'axios'
12-
import { transMsg } from './common'
11+
import { ALL_CROWDIN_LANGUAGES, CrowdinLanguage, Dir, ItemSet } from './common'
1312

1413
const PROJECT_ID = 516822
1514

@@ -139,6 +138,14 @@ async function getOrCreateMainBranch(this: CrowdinClient): Promise<SourceFilesMo
139138
return branch
140139
}
141140

141+
/**
142+
* Key of crowdin file/directory
143+
*/
144+
export type NameKey = {
145+
name: Dir
146+
branchId: number
147+
}
148+
142149
function getFileByName(this: CrowdinClient, param: NameKey): Promise<SourceFilesModel.File> {
143150
return new PaginationIterator(
144151
p => this.crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID, { ...p, branchId: param.branchId })
@@ -219,11 +226,9 @@ async function batchDeleteString(this: CrowdinClient, stringIds: number[]): Prom
219226
console.log("=========end to delete strings========")
220227
}
221228

222-
async function listAllTranslationByStringAndLang(this: CrowdinClient, transKey: TranslationKey): Promise<StringTranslationsModel.StringTranslation[]> {
223-
const { stringId, lang } = transKey
224-
return new PaginationIterator(
225-
p => this.crowdin.stringTranslationsApi.listStringTranslations(PROJECT_ID, stringId, lang, { ...p })
226-
).findAll()
229+
export type TranslationKey = {
230+
stringId: number
231+
lang: CrowdinLanguage
227232
}
228233

229234
async function existTranslationByStringAndLang(this: CrowdinClient, transKey: TranslationKey): Promise<boolean> {
@@ -244,18 +249,15 @@ async function createTranslation(this: CrowdinClient, transKey: TranslationKey,
244249
await this.crowdin.stringTranslationsApi.addTranslation(PROJECT_ID, request)
245250
}
246251

247-
async function downloadTranslations(this: CrowdinClient, fileId: number, lang: CrowdinLanguage): Promise<ItemSet> {
248-
const res = await this.crowdin.translationsApi.buildProjectFileTranslation(PROJECT_ID, fileId, {
249-
targetLanguageId: lang,
252+
async function buildProjectTranslation(this: CrowdinClient, branchId: number) {
253+
const buildRes = await this.crowdin.translationsApi.buildProject(PROJECT_ID, {
254+
branchId,
255+
targetLanguageIds: [...ALL_CROWDIN_LANGUAGES],
250256
skipUntranslatedStrings: true,
251-
exportApprovedOnly: false,
252-
// exportWithMinApprovalsCount: 2,
253257
})
254-
const downloadUrl = res?.data?.url
255-
const fileRes = await axios.get(downloadUrl)
256-
// JSON object
257-
const translation = fileRes.data
258-
return transMsg(translation, '')
258+
const buildId = buildRes?.data?.id
259+
const res = await this.crowdin.translationsApi.downloadTranslations(PROJECT_ID, buildId)
260+
return res?.data?.url
259261
}
260262

261263
/**
@@ -306,13 +308,11 @@ export class CrowdinClient {
306308

307309
batchDeleteString = batchDeleteString
308310

309-
listAllTranslationByStringAndLang = listAllTranslationByStringAndLang
310-
311311
existTranslationByStringAndLang = existTranslationByStringAndLang
312312

313313
createTranslation = createTranslation
314314

315-
downloadTranslations = downloadTranslations
315+
buildProjectTranslation = buildProjectTranslation
316316
}
317317

318318
/**

script/crowdin/common.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1-
import path from 'path'
21
import fs from 'fs'
3-
import { CrowdinClient } from './client'
2+
import path from 'path'
43
import { exitWith } from '../util/process'
4+
import { CrowdinClient } from './client'
5+
6+
export const ALL_DIRS = ['app', 'common', 'popup', 'side', 'cs'] as const
7+
8+
/**
9+
* The directory of messages
10+
*/
11+
export type Dir = typeof ALL_DIRS[number]
512

6-
export const ALL_DIRS: Dir[] = ['app', 'common', 'popup', 'side', 'cs']
13+
/**
14+
* Items of message
15+
*/
16+
export type ItemSet = {
17+
[path: string]: string
18+
}
19+
20+
export const ALL_CROWDIN_LANGUAGES = ['zh-TW', 'ja', 'pt-PT', 'uk', 'es-ES', 'de', 'fr'] as const
21+
22+
/**
23+
* The language code of crowdin
24+
*
25+
* @see https://developer.crowdin.com/language-codes/
26+
*/
27+
export type CrowdinLanguage = typeof ALL_CROWDIN_LANGUAGES[number]
728

829
export const SOURCE_LOCALE: timer.SourceLocale = 'en'
930

@@ -19,9 +40,7 @@ export const ALL_TRANS_LOCALES: timer.Locale[] = [
1940
]
2041

2142
const CROWDIN_I18N_MAP: Record<CrowdinLanguage, timer.Locale> = {
22-
en: 'en',
2343
ja: 'ja',
24-
'zh-CN': 'zh_CN',
2544
'zh-TW': 'zh_TW',
2645
'pt-PT': 'pt_PT',
2746
uk: 'uk',
@@ -30,10 +49,8 @@ const CROWDIN_I18N_MAP: Record<CrowdinLanguage, timer.Locale> = {
3049
fr: 'fr',
3150
}
3251

33-
const I18N_CROWDIN_MAP: Record<timer.Locale, CrowdinLanguage> = {
34-
en: 'en',
52+
const I18N_CROWDIN_MAP: Record<timer.OptionalLocale, CrowdinLanguage> = {
3553
ja: 'ja',
36-
zh_CN: 'zh-CN',
3754
zh_TW: 'zh-TW',
3855
pt_PT: 'pt-PT',
3956
uk: 'uk',

script/crowdin/crowdin.d.ts

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

script/crowdin/export-translation.ts

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,67 @@
1-
import { SourceFilesModel } from "@crowdin/crowdin-api-client"
2-
import { CrowdinClient, getClientFromEnv } from "./client"
3-
import { ALL_DIRS, ALL_TRANS_LOCALES, RSC_FILE_SUFFIX, checkMainBranch, crowdinLangOf, mergeMessage } from "./common"
1+
import decompress from "decompress"
2+
import { existsSync, readdirSync, readFileSync, rm, writeFile } from "fs"
3+
import { join } from "path"
4+
import { getClientFromEnv } from "./client"
5+
import { ALL_DIRS, ALL_TRANS_LOCALES, checkMainBranch, crowdinLangOf, Dir, ItemSet, mergeMessage, RSC_FILE_SUFFIX, transMsg } from "./common"
46

5-
async function processFile(client: CrowdinClient, file: SourceFilesModel.File, dir: Dir): Promise<void> {
6-
const itemSets: Partial<Record<timer.Locale, ItemSet>> = {}
7+
const TEMP_FILE_NAME = join(process.cwd(), ".crowdin-temp.zip")
8+
const TEMP_DIR = join(process.cwd(), ".crowdin-temp")
9+
10+
async function processDir(dir: Dir): Promise<void> {
11+
const fileSets: Record<string, Partial<Record<timer.Locale, ItemSet>>> = {}
712
for (const locale of ALL_TRANS_LOCALES) {
8-
const lang = crowdinLangOf(locale)
9-
const items: ItemSet = await client.downloadTranslations(file.id, lang)
10-
items && Object.keys(items).length && (itemSets[locale] = items)
13+
const crowdinLang = crowdinLangOf(locale)
14+
const dirPath = join(TEMP_DIR, crowdinLang, dir)
15+
const files = readdirSync(dirPath)
16+
for (const fileName of files) {
17+
const json = readFileSync(join(dirPath, fileName)).toString()
18+
const itemSets = fileSets[fileName] || {}
19+
itemSets[locale] = transMsg(JSON.parse(json))
20+
fileSets[fileName] = itemSets
21+
}
22+
}
23+
for (const [fileName, itemSets] of Object.entries(fileSets)) {
24+
await mergeMessage(dir, fileName.replace('.json', RSC_FILE_SUFFIX), itemSets)
1125
}
12-
await mergeMessage(dir, file.name.replace('.json', RSC_FILE_SUFFIX), itemSets)
1326
}
1427

15-
async function processDir(client: CrowdinClient, branch: SourceFilesModel.Branch, dir: Dir): Promise<void> {
16-
const directory = await client.getDirByName({ name: dir, branchId: branch.id })
17-
const files = await client.listFilesByDirectory(directory.id)
18-
for (const file of files) {
19-
processFile(client, file, dir)
28+
async function downloadProjectZip(url: string): Promise<void> {
29+
const res = await fetch(url)
30+
const blob = await res.blob()
31+
const buffer = Buffer.from(await blob.arrayBuffer())
32+
await new Promise(resolve => writeFile(TEMP_FILE_NAME, buffer, resolve))
33+
}
34+
35+
async function compressProjectZip(): Promise<void> {
36+
if (existsSync(TEMP_DIR)) {
37+
await new Promise(resolve => rm(TEMP_DIR, { recursive: true }, resolve))
2038
}
39+
await decompress(TEMP_FILE_NAME, TEMP_DIR)
40+
}
41+
42+
async function clearTempFile() {
43+
await new Promise(resolve => rm(TEMP_FILE_NAME, resolve))
44+
await new Promise(resolve => rm(TEMP_DIR, { recursive: true }, resolve))
2145
}
2246

2347
async function main() {
2448
const client = getClientFromEnv()
2549
const branch = await checkMainBranch(client)
26-
for (const dir of ALL_DIRS) {
27-
await processDir(client, branch, dir)
50+
const zipUrl = await client.buildProjectTranslation(branch.id)
51+
console.log("Built project translations")
52+
console.log(zipUrl)
53+
await downloadProjectZip(zipUrl)
54+
console.log("Downloaded project zip file")
55+
try {
56+
await compressProjectZip()
57+
console.log("Compressed zip file")
58+
for (const dir of ALL_DIRS) {
59+
await processDir(dir)
60+
console.log("Processed dir: " + dir)
61+
}
62+
} finally {
63+
clearTempFile()
64+
console.log("Cleaned temp files")
2865
}
2966
}
3067

script/crowdin/sync-source.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { SourceFilesModel, SourceStringsModel } from "@crowdin/crowdin-api-client"
22
import { groupBy } from "@util/array"
3-
import { CrowdinClient, getClientFromEnv } from "./client"
4-
import { ALL_DIRS, isIgnored, readAllMessages, SOURCE_LOCALE, transMsg } from "./common"
3+
import { CrowdinClient, getClientFromEnv, NameKey } from "./client"
4+
import {
5+
ALL_DIRS,
6+
Dir,
7+
isIgnored,
8+
ItemSet,
9+
readAllMessages,
10+
SOURCE_LOCALE,
11+
transMsg
12+
} from "./common"
513

614
async function initBranch(client: CrowdinClient): Promise<SourceFilesModel.Branch> {
715
const branch = await client.getOrCreateMainBranch()

0 commit comments

Comments
 (0)