Skip to content

Commit e19a987

Browse files
authored
Merge pull request #172 from sheepzh/crowdin
Crowdin
2 parents 13aed99 + e5b59d5 commit e19a987

File tree

128 files changed

+2065
-902
lines changed

Some content is hidden

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

128 files changed

+2065
-902
lines changed

global.d.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,16 +168,38 @@ declare namespace timer {
168168
}
169169
}
170170

171+
/**
172+
* The source locale
173+
*
174+
* @since 1.4.0
175+
*/
176+
type SourceLocale = 'en'
177+
171178
/**
172179
* @since 0.8.0
173180
*/
174-
type Locale =
181+
type Locale = SourceLocale
175182
| 'zh_CN'
176-
| 'en'
177183
| 'ja'
178184
// @since 0.9.0
179185
| 'zh_TW'
180186

187+
/**
188+
* Translating locales
189+
*
190+
* @since 1.4.0
191+
*/
192+
type TranslatingLocale =
193+
| 'de'
194+
| 'en_GB'
195+
| 'en_US'
196+
| 'es'
197+
| 'ko'
198+
| 'pl'
199+
| 'pt'
200+
| 'pt_BR'
201+
| 'ru'
202+
181203
namespace stat {
182204
/**
183205
* The dimension to statistics
@@ -564,4 +586,4 @@ declare namespace timer {
564586
*/
565587
type Callback<T = any> = (result?: Response<T>) => void
566588
}
567-
}
589+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"author": "zhy",
1616
"license": "MIT",
1717
"devDependencies": {
18+
"@crowdin/crowdin-api-client": "^1.19.2",
1819
"@types/chrome": "0.0.199",
1920
"@types/copy-webpack-plugin": "^8.0.1",
2021
"@types/echarts": "^4.9.16",

script/crowdin/client.ts

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
import Crowdin, {
2+
Credentials,
3+
Pagination,
4+
PatchRequest,
5+
ResponseList,
6+
SourceFilesModel,
7+
SourceStringsModel,
8+
StringTranslationsModel,
9+
UploadStorageModel,
10+
} from '@crowdin/crowdin-api-client'
11+
import axios from 'axios'
12+
13+
const PROJECT_ID = 516822
14+
15+
const MAIN_BRANCH_NAME = 'main'
16+
17+
/**
18+
* The iterator of response
19+
*/
20+
class PaginationIterator<T> {
21+
private offset = 0
22+
private limit = 25
23+
private isEnd = false
24+
private buf: T[] = []
25+
private cursor = 0
26+
private query: (pagination: Pagination) => Promise<ResponseList<T>>
27+
28+
constructor(query: (pagination: Pagination) => Promise<ResponseList<T>>) {
29+
this.query = query
30+
}
31+
32+
reset(): void {
33+
this.offset = 0
34+
this.isEnd = false
35+
this.buf = []
36+
this.cursor = 0
37+
}
38+
39+
async findFirst(predicate: (ele: T) => boolean): Promise<T> {
40+
while (true) {
41+
const data = await this.next()
42+
if (!data) {
43+
break
44+
}
45+
if (data && predicate(data)) {
46+
return data
47+
}
48+
}
49+
return undefined
50+
}
51+
52+
async findAll(predicate?: ((ele: T) => boolean)): Promise<T[]> {
53+
const result = []
54+
while (true) {
55+
const data = await this.next()
56+
if (!data) {
57+
break
58+
}
59+
if (predicate ? predicate(data) : true) {
60+
result.push(data)
61+
}
62+
}
63+
return result
64+
}
65+
66+
async next(): Promise<T> {
67+
if (this.isEnd) {
68+
return undefined
69+
}
70+
if (this.cursor >= this.buf.length) {
71+
await this.processBuf()
72+
}
73+
if (this.isEnd) {
74+
return undefined
75+
}
76+
return this.buf[this.cursor++]
77+
}
78+
79+
private async processBuf() {
80+
const pagination: Pagination = { offset: this.offset, limit: this.limit }
81+
const list = await this.query(pagination)
82+
const data = list?.data
83+
if (!data?.length) {
84+
this.isEnd = true
85+
} else {
86+
this.buf = data.map(obj => obj.data)
87+
this.cursor = 0
88+
this.offset += this.buf.length
89+
}
90+
}
91+
}
92+
93+
async function createStorage(fileName: string, content: any): Promise<UploadStorageModel.Storage> {
94+
const response = await this.crowdin.uploadStorageApi.addStorage(fileName, content)
95+
return response.data
96+
}
97+
98+
async function createFile(this: CrowdinClient,
99+
directoryId: number,
100+
storage: UploadStorageModel.Storage,
101+
fileName: string
102+
): Promise<SourceFilesModel.File> {
103+
const request: SourceFilesModel.CreateFileRequest = {
104+
name: fileName,
105+
storageId: storage.id,
106+
directoryId,
107+
type: 'json',
108+
}
109+
const response = await this.crowdin.sourceFilesApi.createFile(PROJECT_ID, request)
110+
return response.data
111+
}
112+
113+
async function restoreFile(this: CrowdinClient, storage: UploadStorageModel.Storage, existFile: SourceFilesModel.File): Promise<SourceFilesModel.File> {
114+
const response = await this.crowdin.sourceFilesApi.updateOrRestoreFile(PROJECT_ID, existFile.id, { storageId: storage.id })
115+
return response.data
116+
}
117+
118+
async function getMainBranch(this: CrowdinClient): Promise<SourceFilesModel.Branch> {
119+
return new PaginationIterator(
120+
pagination => this.crowdin.sourceFilesApi.listProjectBranches(PROJECT_ID, { ...pagination })
121+
).findFirst(e => e.name === MAIN_BRANCH_NAME)
122+
}
123+
124+
async function createMainBranch(): Promise<SourceFilesModel.Branch> {
125+
const request: SourceFilesModel.CreateBranchRequest = {
126+
name: MAIN_BRANCH_NAME
127+
}
128+
const res = await this.crowdin.sourceFilesApi.createBranch(PROJECT_ID, request)
129+
return res.data
130+
}
131+
132+
async function getOrCreateMainBranch(this: CrowdinClient): Promise<SourceFilesModel.Branch> {
133+
let branch = await this.getMainBranch()
134+
if (!branch) {
135+
branch = await this.createMainBranch()
136+
}
137+
console.info("getOrCreateMainBranch: " + JSON.stringify(branch))
138+
return branch
139+
}
140+
141+
function getFileByName(this: CrowdinClient, param: NameKey): Promise<SourceFilesModel.File> {
142+
return new PaginationIterator(
143+
p => this.crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID, { ...p, branchId: param.branchId })
144+
).findFirst(t => t.name === param.name)
145+
}
146+
147+
function getDirByName(this: CrowdinClient, param: NameKey): Promise<SourceFilesModel.Directory> {
148+
return new PaginationIterator(
149+
p => this.crowdin.sourceFilesApi.listProjectDirectories(PROJECT_ID, { ...p, branchId: param.branchId })
150+
).findFirst(d => d.name === param.name)
151+
}
152+
153+
async function createDirectory(this: CrowdinClient, param: NameKey): Promise<SourceFilesModel.Directory> {
154+
const res = await this.crowdin.sourceFilesApi.createDirectory(PROJECT_ID, {
155+
name: param.name,
156+
branchId: param.branchId,
157+
})
158+
return res.data
159+
}
160+
161+
function listFilesByDirectory(this: CrowdinClient, directoryId: number) {
162+
return new PaginationIterator(
163+
p => this.crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID, { ...p, directoryId: directoryId })
164+
).findAll()
165+
}
166+
167+
function listStringsByFile(this: CrowdinClient, fileId: number): Promise<SourceStringsModel.String[]> {
168+
return new PaginationIterator(
169+
p => this.crowdin.sourceStringsApi.listProjectStrings(PROJECT_ID, { ...p, fileId: fileId })
170+
).findAll()
171+
}
172+
173+
async function batchCreateString(
174+
this: CrowdinClient,
175+
fileId: number,
176+
content: ItemSet,
177+
): Promise<void> {
178+
for (const [path, value] of Object.entries(content)) {
179+
const request: SourceStringsModel.CreateStringRequest = {
180+
fileId,
181+
text: value,
182+
identifier: path,
183+
}
184+
console.log(`Try to create new string: ${JSON.stringify(request)}`)
185+
await this.crowdin.sourceStringsApi.addString(PROJECT_ID, request)
186+
}
187+
}
188+
189+
async function batchUpdateIfNecessary(this: CrowdinClient,
190+
content: ItemSet,
191+
existStringsKeyMap: { [path: string]: SourceStringsModel.String }
192+
): Promise<void> {
193+
console.log("=========start to update strings========")
194+
console.log("Content length: " + Object.keys(content).length)
195+
for (const [path, value] of Object.entries(content)) {
196+
const string = existStringsKeyMap[path]
197+
const patch: PatchRequest[] = []
198+
string?.text !== value && patch.push({
199+
op: 'replace',
200+
path: '/text',
201+
value: value
202+
})
203+
if (!patch.length) {
204+
continue
205+
}
206+
console.log('Try to edit string: ' + string.identifier)
207+
await this.crowdin.sourceStringsApi.editString(PROJECT_ID, string.id, patch)
208+
}
209+
console.log("=========end to update strings========")
210+
}
211+
212+
async function batchDeleteString(this: CrowdinClient, stringIds: number[]): Promise<void> {
213+
console.log("=========start to delete strings========")
214+
for (const stringId of stringIds) {
215+
await this.crowdin.sourceStringsApi.deleteString(PROJECT_ID, stringId)
216+
console.log("Delete string: id=" + stringId)
217+
}
218+
console.log("=========end to delete strings========")
219+
}
220+
221+
async function listAllTranslationByStringAndLang(this: CrowdinClient, transKey: TranslationKey): Promise<StringTranslationsModel.StringTranslation[]> {
222+
const { stringId, lang } = transKey
223+
return new PaginationIterator(
224+
p => this.crowdin.stringTranslationsApi.listStringTranslations(PROJECT_ID, stringId, lang, { ...p })
225+
).findAll()
226+
}
227+
228+
async function existTranslationByStringAndLang(this: CrowdinClient, transKey: TranslationKey): Promise<boolean> {
229+
const { stringId, lang } = transKey
230+
const trans = await new PaginationIterator(
231+
p => this.crowdin.stringTranslationsApi.listStringTranslations(PROJECT_ID, stringId, lang, { ...p })
232+
).findFirst(_ => true)
233+
return !!trans
234+
}
235+
236+
async function createTranslation(this: CrowdinClient, transKey: TranslationKey, text: string) {
237+
const { stringId, lang } = transKey
238+
const request: StringTranslationsModel.AddStringTranslationRequest = {
239+
stringId,
240+
languageId: lang,
241+
text
242+
}
243+
await this.crowdin.stringTranslationsApi.addTranslation(PROJECT_ID, request)
244+
}
245+
246+
247+
const CROWDIN_XML_PATTERN = /<string name="(.*?)">(.*?)<\/string>/g
248+
249+
async function downloadTranslations(this: CrowdinClient, fileId: number, lang: CrowdinLanguage): Promise<ItemSet> {
250+
const res = await this.crowdin.translationsApi.exportProjectTranslation(PROJECT_ID, {
251+
targetLanguageId: lang,
252+
fileIds: [fileId],
253+
format: 'android',
254+
})
255+
const downloadUrl = res?.data?.url
256+
const fileRes = await axios.get(downloadUrl)
257+
const xmlData: string = fileRes.data
258+
const items = xmlData.matchAll(CROWDIN_XML_PATTERN)
259+
const itemSet: ItemSet = {}
260+
for (const item of Array.from(items)) {
261+
const result = new RegExp(CROWDIN_XML_PATTERN).exec(item[0])
262+
const key = result[1]
263+
const text = result[2]
264+
itemSet[key] = text
265+
}
266+
return itemSet
267+
}
268+
269+
/**
270+
* The wrapper of client with auth
271+
*/
272+
export class CrowdinClient {
273+
crowdin: Crowdin
274+
275+
constructor(token: string) {
276+
const credentials: Credentials = {
277+
token: token
278+
}
279+
this.crowdin = new Crowdin(credentials)
280+
console.info("Intialized client successfully")
281+
}
282+
createStorage = createStorage
283+
/**
284+
* Get the main branch
285+
*
286+
* @returns main branch or undefined
287+
*/
288+
getMainBranch = getMainBranch
289+
290+
/**
291+
* Create the main branch
292+
*/
293+
createMainBranch = createMainBranch
294+
295+
getOrCreateMainBranch = getOrCreateMainBranch
296+
297+
createFile = createFile
298+
299+
restoreFile = restoreFile
300+
301+
getFileByName = getFileByName
302+
303+
getDirByName = getDirByName
304+
305+
createDirectory = createDirectory
306+
307+
listFilesByDirectory = listFilesByDirectory
308+
309+
listStringsByFile = listStringsByFile
310+
311+
batchCreateString = batchCreateString
312+
313+
batchUpdateIfNecessary = batchUpdateIfNecessary
314+
315+
batchDeleteString = batchDeleteString
316+
317+
listAllTranslationByStringAndLang = listAllTranslationByStringAndLang
318+
319+
existTranslationByStringAndLang = existTranslationByStringAndLang
320+
321+
createTranslation = createTranslation
322+
323+
downloadTranslations = downloadTranslations
324+
}
325+
326+
/**
327+
* Get the client from environment variable [TIMER_CROWDIN_AUTH]
328+
*
329+
* @returns client
330+
*/
331+
export function getClientFromEnv(): CrowdinClient {
332+
const envVar = process.env?.TIMER_CROWDIN_AUTH
333+
if (!envVar) {
334+
console.error("Failed to get the variable named [TIMER_CROWDIN_AUTH]")
335+
process.exit(1)
336+
}
337+
return new CrowdinClient(envVar)
338+
}

0 commit comments

Comments
 (0)