scratch-l10n 5.0.309 → 6.0.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/CHANGELOG.md +50 -0
- package/README.md +6 -0
- package/dist/l10n.js +3 -0
- package/dist/l10n.js.map +1 -1
- package/dist/localeData.js +3 -0
- package/dist/localeData.js.map +1 -1
- package/dist/supportedLocales.js +3 -0
- package/dist/supportedLocales.js.map +1 -1
- package/package.json +23 -21
- package/scripts/{build-data.mjs → build-data.mts} +15 -6
- package/scripts/{build-i18n-src.js → build-i18n-src.mts} +16 -11
- package/scripts/lib/concurrent.mts +37 -0
- package/scripts/lib/freshdesk-api.mts +322 -0
- package/scripts/lib/help-utils.mts +221 -0
- package/{lib/progress-logger.mjs → scripts/lib/progress-logger.mts} +10 -5
- package/scripts/lib/transifex-formats.mts +53 -0
- package/scripts/lib/transifex-objects.mts +143 -0
- package/scripts/lib/transifex.mts +284 -0
- package/scripts/lib/validate.mts +107 -0
- package/scripts/tsconfig.json +20 -0
- package/scripts/tx-pull-editor.mts +74 -0
- package/scripts/{tx-pull-help-articles.js → tx-pull-help-articles.mts} +5 -13
- package/scripts/{tx-pull-help-names.js → tx-pull-help-names.mts} +5 -13
- package/scripts/{tx-pull-locale-articles.js → tx-pull-locale-articles.mts} +5 -13
- package/scripts/{tx-pull-www.mjs → tx-pull-www.mts} +16 -29
- package/scripts/{tx-push-help.mjs → tx-push-help.mts} +39 -37
- package/scripts/{tx-push-src.js → tx-push-src.mts} +13 -20
- package/scripts/update-translations.sh +2 -2
- package/scripts/{validate-extension-inputs.mjs → validate-extension-inputs.mts} +20 -10
- package/scripts/{validate-translations.mjs → validate-translations.mts} +7 -12
- package/scripts/{validate-www.mjs → validate-www.mts} +15 -13
- package/src/supported-locales.mjs +3 -0
- package/www/scratch-website.about-l10njson/nn.json +1 -1
- package/www/scratch-website.splash-l10njson/mi.json +2 -2
- package/www/scratch-website.teacher-faq-l10njson/cy.json +1 -1
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -75
- package/.github/workflows/ci-cd.yml +0 -55
- package/.github/workflows/commitlint.yml +0 -12
- package/.github/workflows/daily-help-update.yml +0 -40
- package/.github/workflows/daily-tx-pull.yml +0 -54
- package/.github/workflows/signature-assistant.yml +0 -31
- package/lib/batch.js +0 -15
- package/lib/transifex.js +0 -242
- package/lib/validate.mjs +0 -48
- package/scripts/freshdesk-api.js +0 -149
- package/scripts/help-utils.js +0 -190
- package/scripts/tx-pull-editor.mjs +0 -83
@@ -0,0 +1,284 @@
|
|
1
|
+
/**
|
2
|
+
* @file
|
3
|
+
* Utilities for interfacing with Transifex API 3.
|
4
|
+
*/
|
5
|
+
import { transifexApi, Collection, JsonApiResource } from '@transifex/api'
|
6
|
+
import { TransifexStrings } from './transifex-formats.mts'
|
7
|
+
import { TransifexLanguageObject, TransifexResourceObject } from './transifex-objects.mts'
|
8
|
+
|
9
|
+
const ORG_NAME = 'llk'
|
10
|
+
const SOURCE_LOCALE = 'en'
|
11
|
+
|
12
|
+
if (!process.env.TX_TOKEN) {
|
13
|
+
throw new Error('TX_TOKEN is not defined.')
|
14
|
+
}
|
15
|
+
|
16
|
+
transifexApi.setup({
|
17
|
+
auth: process.env.TX_TOKEN,
|
18
|
+
})
|
19
|
+
|
20
|
+
/*
|
21
|
+
* The Transifex JS API wraps the Transifex JSON API, and is built around the concept of a `Collection`.
|
22
|
+
* A `Collection` begins as a URL builder: methods like `filter` and `sort` add query parameters to the URL.
|
23
|
+
* The `download` method doesn't actually download anything: it returns the built URL. It seems to be intended
|
24
|
+
* primarily for internal use, but shows up in the documentation despite not being advertised in the .d.ts file.
|
25
|
+
* The `download` method is mainly used to skip the `fetch` method in favor of downloading the resource yourself.
|
26
|
+
* The `fetch` method sends a request to the URL and returns a promise that resolves to the first page of results.
|
27
|
+
* If there's only one page of results, the `data` property of the collection object will be an array of all results.
|
28
|
+
* However, if there are multiple pages of results, the `data` property will only contain the first page of results.
|
29
|
+
* Previous versions of this code would unsafely assume that the `data` property contained all results.
|
30
|
+
* The `all` method returns an async iterator that yields all results, fetching additional pages as needed.
|
31
|
+
*/
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Collects all resources from all pages of a potentially-paginated JSON API collection.
|
35
|
+
* It's not necessary, but also not harmful, to call `fetch()` on the collection before calling this function.
|
36
|
+
* @param collection A collection of JSON API resources.
|
37
|
+
* @returns An array of all resources in the collection.
|
38
|
+
* @todo This seems necessary with the latest Transifex API..?
|
39
|
+
*/
|
40
|
+
const collectAll = async function <T extends JsonApiResource>(collection: Collection): Promise<T[]> {
|
41
|
+
await collection.fetch() // fetch the first page if it hasn't already been fetched
|
42
|
+
const collected: T[] = []
|
43
|
+
for (const item of collection.all()) {
|
44
|
+
collected.push(item as T)
|
45
|
+
}
|
46
|
+
return collected
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* Creates a download event for a specific project, resource, and locale.
|
51
|
+
* Returns the URL to download the resource.
|
52
|
+
* @param projectSlug - project slug (for example, "scratch-editor")
|
53
|
+
* @param resourceSlug - resource slug (for example, "blocks")
|
54
|
+
* @param localeCode - language code (for example, "ko")
|
55
|
+
* @param mode - translation status of strings to include
|
56
|
+
* @returns URL to download the resource
|
57
|
+
*/
|
58
|
+
const getResourceLocation = async function (
|
59
|
+
projectSlug: string,
|
60
|
+
resourceSlug: string,
|
61
|
+
localeCode: string,
|
62
|
+
mode = 'default',
|
63
|
+
): Promise<string> {
|
64
|
+
const resource = {
|
65
|
+
data: {
|
66
|
+
id: `o:${ORG_NAME}:p:${projectSlug}:r:${resourceSlug}`,
|
67
|
+
type: 'resources',
|
68
|
+
},
|
69
|
+
}
|
70
|
+
|
71
|
+
// if locale is English, create a download event of the source file
|
72
|
+
if (localeCode === SOURCE_LOCALE) {
|
73
|
+
return (await transifexApi.ResourceStringsAsyncDownload.download({
|
74
|
+
resource,
|
75
|
+
})) as string
|
76
|
+
}
|
77
|
+
|
78
|
+
const language = {
|
79
|
+
data: {
|
80
|
+
id: `l:${localeCode}`,
|
81
|
+
type: 'languages',
|
82
|
+
},
|
83
|
+
}
|
84
|
+
|
85
|
+
// if locale is not English, create a download event of the translation file
|
86
|
+
return (await transifexApi.ResourceTranslationsAsyncDownload.download({
|
87
|
+
mode,
|
88
|
+
resource,
|
89
|
+
language,
|
90
|
+
})) as string
|
91
|
+
}
|
92
|
+
|
93
|
+
/**
|
94
|
+
* Pulls a translation JSON from transifex, for a specific project, resource, and locale.
|
95
|
+
* @template T - resource file type, such as `TransifexStringsKeyValueJson`
|
96
|
+
* @param project - project slug (for example, `scratch-editor`)
|
97
|
+
* @param resource - resource slug (for example, `blocks`)
|
98
|
+
* @param locale - language code (for example, `ko`)
|
99
|
+
* @param mode - translation status of strings to include
|
100
|
+
* @returns JSON object of translated resource strings (or, of the original resource strings, if the local is the
|
101
|
+
* source language)
|
102
|
+
*/
|
103
|
+
export const txPull = async function <T>(
|
104
|
+
project: string,
|
105
|
+
resource: string,
|
106
|
+
locale: string,
|
107
|
+
mode = 'default',
|
108
|
+
): Promise<TransifexStrings<T>> {
|
109
|
+
let buffer: string | null = null
|
110
|
+
try {
|
111
|
+
const url = await getResourceLocation(project, resource, locale, mode)
|
112
|
+
for (let i = 0; i < 5; i++) {
|
113
|
+
if (i > 0) {
|
114
|
+
console.log(`Retrying txPull download after ${i} failed attempt(s)`)
|
115
|
+
}
|
116
|
+
try {
|
117
|
+
const response = await fetch(url)
|
118
|
+
if (!response.ok) {
|
119
|
+
throw new Error(`Failed to download resource: ${response.statusText}`)
|
120
|
+
}
|
121
|
+
buffer = await response.text()
|
122
|
+
break
|
123
|
+
} catch (e) {
|
124
|
+
console.error(e, { project, resource, locale, buffer })
|
125
|
+
}
|
126
|
+
}
|
127
|
+
if (!buffer) {
|
128
|
+
throw Error(`txPull download failed after 5 retries: ${url}`)
|
129
|
+
}
|
130
|
+
return JSON.parse(buffer) as TransifexStrings<T>
|
131
|
+
} catch (e) {
|
132
|
+
;(e as Error).cause = {
|
133
|
+
project,
|
134
|
+
resource,
|
135
|
+
locale,
|
136
|
+
buffer,
|
137
|
+
}
|
138
|
+
throw e
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
/**
|
143
|
+
* Given a project, returns a list of the slugs of all resources in the project
|
144
|
+
* @param project - project slug (for example, "scratch-website")
|
145
|
+
* @returns - array of strings, slugs identifying each resource in the project
|
146
|
+
*/
|
147
|
+
export const txResources = async function (project: string): Promise<string[]> {
|
148
|
+
const resources = transifexApi.Resource.filter({
|
149
|
+
project: `o:${ORG_NAME}:p:${project}`,
|
150
|
+
})
|
151
|
+
|
152
|
+
const resourcesData = await collectAll<TransifexResourceObject>(resources)
|
153
|
+
|
154
|
+
const slugs = resourcesData.map(
|
155
|
+
r =>
|
156
|
+
// r.id is a longer id string, like "o:llk:p:scratch-website:r:about-l10njson"
|
157
|
+
// We just want the slug that comes after ":r:" ("about-l10njson")
|
158
|
+
r.id.split(':r:')[1],
|
159
|
+
)
|
160
|
+
return slugs
|
161
|
+
}
|
162
|
+
|
163
|
+
/**
|
164
|
+
* @param project - project slug (for example)
|
165
|
+
* @returns - array of resource objects
|
166
|
+
*/
|
167
|
+
export const txResourcesObjects = async function (project: string): Promise<TransifexResourceObject[]> {
|
168
|
+
const resources = transifexApi.Resource.filter({
|
169
|
+
project: `o:${ORG_NAME}:p:${project}`,
|
170
|
+
})
|
171
|
+
|
172
|
+
return collectAll<TransifexResourceObject>(resources)
|
173
|
+
}
|
174
|
+
|
175
|
+
/**
|
176
|
+
* Gets available languages for a project
|
177
|
+
* @param slug - project slug (for example, "scratch-editor")
|
178
|
+
* @returns - list of language codes
|
179
|
+
*/
|
180
|
+
export const txAvailableLanguages = async function (slug: string): Promise<string[]> {
|
181
|
+
const project = await transifexApi.Project.get({
|
182
|
+
organization: `o:${ORG_NAME}`,
|
183
|
+
slug: slug,
|
184
|
+
})
|
185
|
+
|
186
|
+
const languages = (await project.fetch('languages', false)) as Collection
|
187
|
+
const languagesData = await collectAll<TransifexLanguageObject>(languages)
|
188
|
+
return languagesData.map(l => l.attributes.code)
|
189
|
+
}
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Uploads English source strings to a resource in transifex
|
193
|
+
* @param project - project slug (for example, "scratch-editor")
|
194
|
+
* @param resource - resource slug (for example, "blocks")
|
195
|
+
* @param sourceStrings - json of source strings
|
196
|
+
*/
|
197
|
+
export const txPush = async function (project: string, resource: string, sourceStrings: TransifexStrings<unknown>) {
|
198
|
+
const resourceObj = {
|
199
|
+
data: {
|
200
|
+
id: `o:${ORG_NAME}:p:${project}:r:${resource}`,
|
201
|
+
type: 'resources',
|
202
|
+
},
|
203
|
+
}
|
204
|
+
|
205
|
+
await transifexApi.ResourceStringsAsyncUpload.upload({
|
206
|
+
resource: resourceObj,
|
207
|
+
content: JSON.stringify(sourceStrings),
|
208
|
+
})
|
209
|
+
}
|
210
|
+
|
211
|
+
/**
|
212
|
+
* Creates a new resource, and then uploads source strings to it if they are provided
|
213
|
+
* @param project - project slug (for example, "scratch-editor")
|
214
|
+
* @param resource - object of resource information
|
215
|
+
* @param resource.slug - resource slug (for example, "blocks")
|
216
|
+
* @param resource.name - human-readable name for the resource
|
217
|
+
* @param resource.i18nType - i18n format id
|
218
|
+
* @param resource.sourceStrings - json object of source strings
|
219
|
+
*/
|
220
|
+
export const txCreateResource = async function (
|
221
|
+
project: string,
|
222
|
+
{
|
223
|
+
slug,
|
224
|
+
name,
|
225
|
+
i18nType,
|
226
|
+
sourceStrings,
|
227
|
+
}: {
|
228
|
+
slug: string
|
229
|
+
name: string
|
230
|
+
i18nType: string
|
231
|
+
sourceStrings?: TransifexStrings<unknown>
|
232
|
+
},
|
233
|
+
) {
|
234
|
+
const i18nFormat = {
|
235
|
+
data: {
|
236
|
+
id: i18nType || 'KEYVALUEJSON',
|
237
|
+
type: 'i18n_formats',
|
238
|
+
},
|
239
|
+
}
|
240
|
+
|
241
|
+
const projectObj = {
|
242
|
+
data: {
|
243
|
+
id: `o:${ORG_NAME}:p:${project}`,
|
244
|
+
type: 'projects',
|
245
|
+
},
|
246
|
+
}
|
247
|
+
|
248
|
+
// @ts-expect-error This omits "required" props but has been like this for ages and I'm not sure how to best fix it
|
249
|
+
await transifexApi.Resource.create({
|
250
|
+
attributes: { slug: slug, name: name },
|
251
|
+
relationships: {
|
252
|
+
i18n_format: i18nFormat,
|
253
|
+
project: projectObj,
|
254
|
+
},
|
255
|
+
})
|
256
|
+
|
257
|
+
if (sourceStrings) {
|
258
|
+
await txPush(project, slug, sourceStrings)
|
259
|
+
}
|
260
|
+
}
|
261
|
+
|
262
|
+
/**
|
263
|
+
* Information about an error condition generated by Transifex's JSON API
|
264
|
+
* @see https://github.com/transifex/transifex-api-python/blob/master/src/jsonapi/exceptions.py
|
265
|
+
* @see https://github.com/transifex/transifex-javascript/blob/master/packages/jsonapi/src/errors.js
|
266
|
+
*/
|
267
|
+
export interface JsonApiError {
|
268
|
+
status: number
|
269
|
+
code: string
|
270
|
+
title: string
|
271
|
+
detail: string
|
272
|
+
source?: string
|
273
|
+
}
|
274
|
+
|
275
|
+
/**
|
276
|
+
* A JS `Error` thrown by Transifex's JSON API
|
277
|
+
* @see https://github.com/transifex/transifex-api-python/blob/master/src/jsonapi/exceptions.py
|
278
|
+
* @see https://github.com/transifex/transifex-javascript/blob/master/packages/jsonapi/src/errors.js
|
279
|
+
*/
|
280
|
+
export interface JsonApiException extends Error {
|
281
|
+
statusCode: number
|
282
|
+
errors: JsonApiError[]
|
283
|
+
message: string
|
284
|
+
}
|
@@ -0,0 +1,107 @@
|
|
1
|
+
import assert from 'assert'
|
2
|
+
import parse from 'format-message-parse'
|
3
|
+
import {
|
4
|
+
TransifexStringChrome,
|
5
|
+
TransifexStringKeyValueJson,
|
6
|
+
TransifexStringsChrome,
|
7
|
+
TransifexStringsKeyValueJson,
|
8
|
+
} from './transifex-formats.mts'
|
9
|
+
|
10
|
+
export type TransifexEditorString = TransifexStringChrome | TransifexStringKeyValueJson
|
11
|
+
export type TransifexEditorStrings = TransifexStringsChrome | TransifexStringsKeyValueJson
|
12
|
+
|
13
|
+
/**
|
14
|
+
* filter placeholders out of a message
|
15
|
+
* @param message - the message to parse
|
16
|
+
* @returns an array of placeholder information
|
17
|
+
* @example
|
18
|
+
* parse('a message with a {value} and {count, plural, one {one} other {more}}.')
|
19
|
+
* returns an array:
|
20
|
+
* [ 'a message with a ',
|
21
|
+
* [ 'value' ],
|
22
|
+
* ' and ',
|
23
|
+
* [ 'count', 'plural', 0, { one: [Array], other: [Array] } ],
|
24
|
+
* '.'
|
25
|
+
* ]
|
26
|
+
* placeholders are always an array, so filter for array elements to find the placeholders
|
27
|
+
*/
|
28
|
+
const placeholders = (message: string): parse.Placeholder[] =>
|
29
|
+
// this will throw an error if the message is not valid ICU
|
30
|
+
// single quote (as in French l'année) messes up the parse and is not
|
31
|
+
// relevant for this check, so strip them out
|
32
|
+
parse(message.replace(/'/g, '')).filter(item => Array.isArray(item))
|
33
|
+
|
34
|
+
const getMessageText = (m: TransifexStringKeyValueJson | TransifexStringChrome): string =>
|
35
|
+
typeof m === 'string' ? m : m.message
|
36
|
+
|
37
|
+
/**
|
38
|
+
* @param a - one array of items
|
39
|
+
* @param b - another array of items
|
40
|
+
* @returns true if the two arrays contain the same items, without consideration of order and duplicates, judged by
|
41
|
+
* shallow equality.
|
42
|
+
* @example
|
43
|
+
* sameItems(['a', 'b'], ['a', 'b']) === true
|
44
|
+
* sameItems(['a', 'b'], ['b', 'a']) === true
|
45
|
+
* sameItems(['a', 'b'], ['b', 'a', 'b']) === true
|
46
|
+
* sameItems(['a', 'b'], ['a']) === false
|
47
|
+
*/
|
48
|
+
function sameItems<T>(a: T[], b: T[]): boolean {
|
49
|
+
if (!a.every(x => b.includes(x))) {
|
50
|
+
return false
|
51
|
+
}
|
52
|
+
if (!b.every(x => a.includes(x))) {
|
53
|
+
return false
|
54
|
+
}
|
55
|
+
return true
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* @param message - the translated message to validate
|
60
|
+
* @param source - the source string for this translated message
|
61
|
+
* @returns `false` if the message definitely has a problem, or `true` if the message might be OK.
|
62
|
+
*/
|
63
|
+
export const validMessage = (message: TransifexEditorString, source: TransifexEditorString): boolean => {
|
64
|
+
const msgText = getMessageText(message)
|
65
|
+
const srcText = getMessageText(source)
|
66
|
+
|
67
|
+
// Check ICU placeholder names (but not extended plural info)
|
68
|
+
const msgPlaceholderNamesICU = placeholders(msgText).map(x => x[0])
|
69
|
+
const srcPlaceholderNamesICU = placeholders(srcText).map(x => x[0])
|
70
|
+
if (!sameItems(msgPlaceholderNamesICU, srcPlaceholderNamesICU)) {
|
71
|
+
return false
|
72
|
+
}
|
73
|
+
|
74
|
+
// Check scratch-blocks numeric placeholders like '%1'
|
75
|
+
// TODO: apply this only for resources that use numeric placeholders.
|
76
|
+
// Otherwise, sentences with percentages can cause failures in some languages. Example: "göre %48'lik bir artış"
|
77
|
+
// const msgPlaceholdersNumeric: string[] = msgText.match(/%[0-9]+/g) ?? []
|
78
|
+
// const srcPlaceholdersNumeric: string[] = srcText.match(/%[0-9]+/g) ?? []
|
79
|
+
// if (!sameItems(msgPlaceholdersNumeric, srcPlaceholdersNumeric)) {
|
80
|
+
// return false
|
81
|
+
// }
|
82
|
+
|
83
|
+
return true
|
84
|
+
}
|
85
|
+
|
86
|
+
/**
|
87
|
+
* @param translation - the translations to validate and their corresponding source strings
|
88
|
+
* @param translation.locale - the Transifex locale, for error reporting
|
89
|
+
* @param translation.translations - the translations to validate
|
90
|
+
* @param source - the source strings for the translations
|
91
|
+
*/
|
92
|
+
export const validateTranslations = (
|
93
|
+
{ locale, translations }: { locale: string; translations: TransifexEditorStrings },
|
94
|
+
source: TransifexEditorStrings,
|
95
|
+
) => {
|
96
|
+
const transKeys = Object.keys(translations)
|
97
|
+
const sourceKeys = Object.keys(source)
|
98
|
+
assert.strictEqual(transKeys.length, sourceKeys.length, `locale ${locale} has a different number of message keys`)
|
99
|
+
transKeys.forEach(item => assert(sourceKeys.includes(item), `locale ${locale} has key ${item} not in the source`))
|
100
|
+
sourceKeys.forEach(item => assert(transKeys.includes(item), `locale ${locale} is missing key ${item}`))
|
101
|
+
sourceKeys.forEach(item =>
|
102
|
+
assert(
|
103
|
+
validMessage(translations[item], source[item]),
|
104
|
+
`locale ${locale} / item ${item}: message validation failed:\n msg: ${getMessageText(translations[item])}\n src: ${getMessageText(source[item])}`,
|
105
|
+
),
|
106
|
+
)
|
107
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
// tsconfig.json
|
2
|
+
{
|
3
|
+
"compilerOptions": {
|
4
|
+
"target": "ESNext",
|
5
|
+
"module": "NodeNext",
|
6
|
+
"moduleResolution": "NodeNext",
|
7
|
+
"allowJs": true,
|
8
|
+
"checkJs": true,
|
9
|
+
"allowImportingTsExtensions": true,
|
10
|
+
"esModuleInterop": true,
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
12
|
+
"strict": true,
|
13
|
+
"noEmit": true,
|
14
|
+
"resolveJsonModule": true,
|
15
|
+
"isolatedModules": true,
|
16
|
+
"incremental": true
|
17
|
+
},
|
18
|
+
"include": ["**/*.js", "**/*.mjs", "**/*.ts", "**/*.mts"],
|
19
|
+
"exclude": ["node_modules"]
|
20
|
+
}
|
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env tsx
|
2
|
+
/**
|
3
|
+
* @file
|
4
|
+
* Script to pull translations from transifex and generate the editor-msgs file.
|
5
|
+
* Expects that the project and resource have already been defined in Transifex, and that
|
6
|
+
* the person running the script has the the TX_TOKEN environment variable set to an api
|
7
|
+
* token that has developer access.
|
8
|
+
*/
|
9
|
+
import fs from 'fs'
|
10
|
+
import path from 'path'
|
11
|
+
import locales, { localeMap } from '../src/supported-locales.mjs'
|
12
|
+
import { poolMap } from './lib/concurrent.mts'
|
13
|
+
import { TransifexStringsKeyValueJson } from './lib/transifex-formats.mts'
|
14
|
+
import { txPull } from './lib/transifex.mts'
|
15
|
+
import { TransifexEditorStrings, validateTranslations } from './lib/validate.mts'
|
16
|
+
|
17
|
+
const args = process.argv.slice(2)
|
18
|
+
|
19
|
+
const usage = `
|
20
|
+
Pull supported language translations from Transifex. Usage:
|
21
|
+
node tx-pull-editor.js tx-project tx-resource path
|
22
|
+
tx-project: project on Transifex (e.g., scratch-editor)
|
23
|
+
tx-resource: resource within the project (e.g., interface)
|
24
|
+
path: where to put the downloaded json files
|
25
|
+
NOTE: TX_TOKEN environment variable needs to be set with a Transifex API token. See
|
26
|
+
the Localization page on the GUI wiki for information about setting up Transifex.
|
27
|
+
`
|
28
|
+
|
29
|
+
// Fail immediately if the TX_TOKEN is not defined
|
30
|
+
if (!process.env.TX_TOKEN || args.length < 3) {
|
31
|
+
process.stdout.write(usage)
|
32
|
+
process.exit(1)
|
33
|
+
}
|
34
|
+
|
35
|
+
// Globals
|
36
|
+
const PROJECT = args[0]
|
37
|
+
const RESOURCE = args[1]
|
38
|
+
const OUTPUT_DIR = path.resolve(args[2])
|
39
|
+
const MODE = 'reviewed'
|
40
|
+
const CONCURRENCY_LIMIT = 36
|
41
|
+
|
42
|
+
const getLocaleData = async function (locale: string) {
|
43
|
+
const txLocale = localeMap[locale] || locale
|
44
|
+
const data = (await txPull(PROJECT, RESOURCE, txLocale, MODE)) as TransifexEditorStrings
|
45
|
+
return {
|
46
|
+
locale: locale,
|
47
|
+
translations: data,
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
const pullTranslations = async function () {
|
52
|
+
const values = await poolMap(Object.keys(locales), CONCURRENCY_LIMIT, getLocaleData)
|
53
|
+
const source = values.find(elt => elt.locale === 'en')?.translations
|
54
|
+
if (!source) {
|
55
|
+
throw new Error('Could not find source strings')
|
56
|
+
}
|
57
|
+
values.forEach(translation => {
|
58
|
+
validateTranslations({ locale: translation.locale, translations: translation.translations }, source)
|
59
|
+
// if translation has message & description, we only want the message
|
60
|
+
const txs: TransifexStringsKeyValueJson = {}
|
61
|
+
for (const key of Object.keys(translation.translations)) {
|
62
|
+
const tx = translation.translations[key]
|
63
|
+
if (typeof tx === 'string') {
|
64
|
+
txs[key] = tx
|
65
|
+
} else {
|
66
|
+
txs[key] = tx.message
|
67
|
+
}
|
68
|
+
}
|
69
|
+
const file = JSON.stringify(txs, null, 4)
|
70
|
+
fs.writeFileSync(`${OUTPUT_DIR}/${translation.locale}.json`, file)
|
71
|
+
})
|
72
|
+
}
|
73
|
+
|
74
|
+
await pullTranslations()
|
@@ -1,9 +1,9 @@
|
|
1
|
-
#!/usr/bin/env
|
2
|
-
|
1
|
+
#!/usr/bin/env tsx
|
3
2
|
/**
|
4
3
|
* @file
|
5
4
|
* Script to pull scratch-help translations from transifex and push to FreshDesk.
|
6
5
|
*/
|
6
|
+
import { getInputs, saveItem, localizeFolder } from './lib/help-utils.mts'
|
7
7
|
|
8
8
|
const args = process.argv.slice(2)
|
9
9
|
const usage = `
|
@@ -21,14 +21,6 @@ if (!process.env.TX_TOKEN || !process.env.FRESHDESK_TOKEN || args.length > 0) {
|
|
21
21
|
process.exit(1)
|
22
22
|
}
|
23
23
|
|
24
|
-
const {
|
25
|
-
|
26
|
-
|
27
|
-
.then(([languages, folders]) => {
|
28
|
-
process.stdout.write('Processing articles pulled from Transifex\n')
|
29
|
-
return folders.map(item => saveItem(item, languages, localizeFolder))
|
30
|
-
})
|
31
|
-
.catch(e => {
|
32
|
-
process.stdout.write(`Error: ${e.message}\n`)
|
33
|
-
process.exitCode = 1 // not ok
|
34
|
-
})
|
24
|
+
const { languages, folders } = await getInputs()
|
25
|
+
console.log('Processing articles pulled from Transifex')
|
26
|
+
await Promise.all(folders.map(item => saveItem(item, languages, localizeFolder)))
|
@@ -1,9 +1,9 @@
|
|
1
|
-
#!/usr/bin/env
|
2
|
-
|
1
|
+
#!/usr/bin/env tsx
|
3
2
|
/**
|
4
3
|
* @file
|
5
4
|
* Script to pull scratch-help translations from transifex and push to FreshDesk.
|
6
5
|
*/
|
6
|
+
import { getInputs, saveItem, localizeNames } from './lib/help-utils.mts'
|
7
7
|
|
8
8
|
const args = process.argv.slice(2)
|
9
9
|
|
@@ -22,14 +22,6 @@ if (!process.env.TX_TOKEN || !process.env.FRESHDESK_TOKEN || args.length > 0) {
|
|
22
22
|
process.exit(1)
|
23
23
|
}
|
24
24
|
|
25
|
-
const {
|
26
|
-
|
27
|
-
|
28
|
-
.then(([languages, , names]) => {
|
29
|
-
process.stdout.write('Process Category and Folder Names pulled from Transifex\n')
|
30
|
-
return names.map(item => saveItem(item, languages, localizeNames))
|
31
|
-
})
|
32
|
-
.catch(e => {
|
33
|
-
process.stdout.write(`Error: ${e.message}\n`)
|
34
|
-
process.exitCode = 1 // not ok
|
35
|
-
})
|
25
|
+
const { languages, names } = await getInputs()
|
26
|
+
console.log('Process Category and Folder Names pulled from Transifex')
|
27
|
+
await Promise.all(names.map(item => saveItem(item, languages, localizeNames)))
|
@@ -1,9 +1,9 @@
|
|
1
|
-
#!/usr/bin/env
|
2
|
-
|
1
|
+
#!/usr/bin/env tsx
|
3
2
|
/**
|
4
3
|
* @file
|
5
4
|
* Script to pull scratch-help translations from transifex and push to FreshDesk.
|
6
5
|
*/
|
6
|
+
import { getInputs, saveItem, localizeFolder, debugFolder } from './lib/help-utils.mts'
|
7
7
|
|
8
8
|
const args = process.argv.slice(2)
|
9
9
|
const usage = `
|
@@ -21,8 +21,6 @@ if (!process.env.TX_TOKEN || !process.env.FRESHDESK_TOKEN || args.length === 0)
|
|
21
21
|
process.exit(1)
|
22
22
|
}
|
23
23
|
|
24
|
-
const { getInputs, saveItem, localizeFolder, debugFolder } = require('./help-utils.js')
|
25
|
-
|
26
24
|
let locale = args[0]
|
27
25
|
let debug = false
|
28
26
|
if (locale === '-d') {
|
@@ -31,12 +29,6 @@ if (locale === '-d') {
|
|
31
29
|
}
|
32
30
|
const saveFn = debug ? debugFolder : localizeFolder
|
33
31
|
|
34
|
-
getInputs()
|
35
|
-
|
36
|
-
|
37
|
-
return folders.map(item => saveItem(item, [locale], saveFn))
|
38
|
-
})
|
39
|
-
.catch(e => {
|
40
|
-
process.stdout.write(`Error: ${e.message}\n`)
|
41
|
-
process.exitCode = 1 // not ok
|
42
|
-
})
|
32
|
+
const { folders } = await getInputs()
|
33
|
+
console.log('Processing articles pulled from Transifex')
|
34
|
+
await Promise.all(folders.map(item => saveItem(item, [locale], saveFn)))
|
@@ -1,4 +1,4 @@
|
|
1
|
-
#!/usr/bin/env
|
1
|
+
#!/usr/bin/env tsx
|
2
2
|
/**
|
3
3
|
* @file
|
4
4
|
* Script to pull www translations from transifex for all resources.
|
@@ -7,20 +7,12 @@
|
|
7
7
|
* token that has developer access.
|
8
8
|
*/
|
9
9
|
import fs from 'fs/promises'
|
10
|
-
import mkdirp from 'mkdirp'
|
10
|
+
import { mkdirp } from 'mkdirp'
|
11
11
|
import path from 'path'
|
12
|
-
import { batchMap } from '../lib/batch.js'
|
13
|
-
import { ProgressLogger } from '../lib/progress-logger.mjs'
|
14
|
-
import { txPull, txResources } from '../lib/transifex.js'
|
15
12
|
import locales, { localeMap } from '../src/supported-locales.mjs'
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
* Script to pull www translations from transifex for all resources.
|
20
|
-
* Expects that the project and that the person running the script
|
21
|
-
* has the the TX_TOKEN environment variable set to an api
|
22
|
-
* token that has developer access.
|
23
|
-
*/
|
13
|
+
import { poolMap } from './lib/concurrent.mts'
|
14
|
+
import { ProgressLogger } from './lib/progress-logger.mjs'
|
15
|
+
import { txPull, txResources } from './lib/transifex.mts'
|
24
16
|
|
25
17
|
const args = process.argv.slice(2)
|
26
18
|
|
@@ -49,7 +41,7 @@ const CONCURRENCY_LIMIT = 36
|
|
49
41
|
|
50
42
|
const lang = args.length === 2 ? args[1] : ''
|
51
43
|
|
52
|
-
const getLocaleData = async function (item) {
|
44
|
+
const getLocaleData = async function (item: { locale: string; resource: string }) {
|
53
45
|
const locale = item.locale
|
54
46
|
const resource = item.resource
|
55
47
|
const txLocale = localeMap[locale] || locale
|
@@ -69,7 +61,7 @@ const getLocaleData = async function (item) {
|
|
69
61
|
fileName,
|
70
62
|
}
|
71
63
|
} catch (e) {
|
72
|
-
e.cause = {
|
64
|
+
;(e as Error).cause = {
|
73
65
|
resource,
|
74
66
|
locale,
|
75
67
|
translations,
|
@@ -80,7 +72,7 @@ const getLocaleData = async function (item) {
|
|
80
72
|
}
|
81
73
|
}
|
82
74
|
|
83
|
-
const expandResourceFiles = resources => {
|
75
|
+
const expandResourceFiles = (resources: string[]) => {
|
84
76
|
const items = []
|
85
77
|
for (const resource of resources) {
|
86
78
|
if (lang) {
|
@@ -100,18 +92,13 @@ const pullTranslations = async function () {
|
|
100
92
|
|
101
93
|
const progress = new ProgressLogger(allFiles.length)
|
102
94
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
})
|
111
|
-
} catch (err) {
|
112
|
-
console.error(err)
|
113
|
-
process.exit(1)
|
114
|
-
}
|
95
|
+
await poolMap(allFiles, CONCURRENCY_LIMIT, async item => {
|
96
|
+
try {
|
97
|
+
await getLocaleData(item)
|
98
|
+
} finally {
|
99
|
+
progress.increment()
|
100
|
+
}
|
101
|
+
})
|
115
102
|
}
|
116
103
|
|
117
|
-
pullTranslations()
|
104
|
+
await pullTranslations()
|