scratch-l10n 5.0.309 → 6.0.1

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/CHANGELOG.md +57 -0
  2. package/README.md +6 -0
  3. package/dist/l10n.js +3 -0
  4. package/dist/l10n.js.map +1 -1
  5. package/dist/localeData.js +3 -0
  6. package/dist/localeData.js.map +1 -1
  7. package/dist/supportedLocales.js +3 -0
  8. package/dist/supportedLocales.js.map +1 -1
  9. package/package.json +23 -21
  10. package/scripts/{build-data.mjs → build-data.mts} +15 -6
  11. package/scripts/{build-i18n-src.js → build-i18n-src.mts} +16 -11
  12. package/scripts/lib/concurrent.mts +37 -0
  13. package/scripts/lib/freshdesk-api.mts +322 -0
  14. package/scripts/lib/help-utils.mts +221 -0
  15. package/{lib/progress-logger.mjs → scripts/lib/progress-logger.mts} +10 -5
  16. package/scripts/lib/transifex-formats.mts +53 -0
  17. package/scripts/lib/transifex-objects.mts +143 -0
  18. package/scripts/lib/transifex.mts +284 -0
  19. package/scripts/lib/validate.mts +107 -0
  20. package/scripts/tsconfig.json +20 -0
  21. package/scripts/tx-pull-editor.mts +74 -0
  22. package/scripts/{tx-pull-help-articles.js → tx-pull-help-articles.mts} +5 -13
  23. package/scripts/{tx-pull-help-names.js → tx-pull-help-names.mts} +5 -13
  24. package/scripts/{tx-pull-locale-articles.js → tx-pull-locale-articles.mts} +5 -13
  25. package/scripts/{tx-pull-www.mjs → tx-pull-www.mts} +16 -29
  26. package/scripts/{tx-push-help.mjs → tx-push-help.mts} +39 -37
  27. package/scripts/{tx-push-src.js → tx-push-src.mts} +13 -20
  28. package/scripts/update-translations.sh +2 -2
  29. package/scripts/{validate-extension-inputs.mjs → validate-extension-inputs.mts} +20 -10
  30. package/scripts/{validate-translations.mjs → validate-translations.mts} +7 -12
  31. package/scripts/{validate-www.mjs → validate-www.mts} +15 -13
  32. package/src/supported-locales.mjs +3 -0
  33. package/www/scratch-website.about-l10njson/nn.json +1 -1
  34. package/www/scratch-website.splash-l10njson/mi.json +2 -2
  35. package/www/scratch-website.teacher-faq-l10njson/cy.json +1 -1
  36. package/.github/PULL_REQUEST_TEMPLATE.md +0 -75
  37. package/.github/workflows/ci-cd.yml +0 -55
  38. package/.github/workflows/commitlint.yml +0 -12
  39. package/.github/workflows/daily-help-update.yml +0 -40
  40. package/.github/workflows/daily-tx-pull.yml +0 -54
  41. package/.github/workflows/signature-assistant.yml +0 -31
  42. package/lib/batch.js +0 -15
  43. package/lib/transifex.js +0 -242
  44. package/lib/validate.mjs +0 -48
  45. package/scripts/freshdesk-api.js +0 -149
  46. package/scripts/help-utils.js +0 -190
  47. package/scripts/tx-pull-editor.mjs +0 -83
@@ -0,0 +1,221 @@
1
+ /**
2
+ * @file
3
+ * Helper functions for syncing Freshdesk knowledge base articles with Transifex
4
+ */
5
+ import { promises as fsPromises } from 'fs'
6
+ import { mkdirp } from 'mkdirp'
7
+ import FreshdeskApi, { FreshdeskArticleCreate, FreshdeskArticleStatus } from './freshdesk-api.mts'
8
+ import { TransifexStringKeyValueJson, TransifexStringsKeyValueJson, TransifexStrings } from './transifex-formats.mts'
9
+ import { TransifexResourceObject } from './transifex-objects.mts'
10
+ import { txPull, txResourcesObjects, txAvailableLanguages } from './transifex.mts'
11
+
12
+ const FD = new FreshdeskApi('https://mitscratch.freshdesk.com', process.env.FRESHDESK_TOKEN ?? '')
13
+ const TX_PROJECT = 'scratch-help'
14
+
15
+ const freshdeskLocale = (locale: string): string => {
16
+ // map between Transifex locale and Freshdesk. Two letter codes are usually fine
17
+ const localeMap: Record<string, string> = {
18
+ es_419: 'es-LA',
19
+ ja: 'ja-JP',
20
+ 'ja-Hira': 'ja-JP',
21
+ lv: 'lv-LV',
22
+ nb: 'nb-NO',
23
+ nn: 'nb-NO',
24
+ pt: 'pt-PT',
25
+ pt_BR: 'pt-BR',
26
+ ru: 'ru-RU',
27
+ sv: 'sv-SE',
28
+ zh_CN: 'zh-CN',
29
+ zh_TW: 'zh-TW',
30
+ }
31
+ return localeMap[locale] || locale
32
+ }
33
+
34
+ /**
35
+ * Parse a string into an integer.
36
+ * If converting the integer back to a string does not result in the same string, throw.
37
+ * @param str - The (allegedly) numeric string to parse
38
+ * @param radix - Interpret the string as a number in this base. For example, use 10 for decimal values.
39
+ * @returns The numeric value of the string
40
+ */
41
+ const parseIntOrThrow = (str: string, radix: number) => {
42
+ const num = parseInt(str, radix)
43
+ if (str != num.toString(radix)) {
44
+ throw new Error(`Could not parse int safely: ${str}`)
45
+ }
46
+ return num
47
+ }
48
+
49
+ /**
50
+ * Pull metadata from Transifex for the scratch-help project
51
+ * @returns Promise for a results object containing:
52
+ * languages - array of supported languages
53
+ * folders - array of tx resources corresponding to Freshdesk folders
54
+ * names - array of tx resources corresponding to the Freshdesk metadata
55
+ */
56
+ export const getInputs = async () => {
57
+ const resourcesPromise = txResourcesObjects(TX_PROJECT)
58
+ const languagesPromise = txAvailableLanguages(TX_PROJECT)
59
+
60
+ // there are three types of resources differentiated by the file type
61
+ const foldersPromise = resourcesPromise.then(resources =>
62
+ resources.filter(resource => resource.attributes.i18n_type === 'STRUCTURED_JSON'),
63
+ )
64
+ const namesPromise = resourcesPromise.then(resources =>
65
+ resources.filter(resource => resource.attributes.i18n_type === 'KEYVALUEJSON'),
66
+ )
67
+ // ignore the yaml type because it's not possible to update via API
68
+
69
+ const [languages, folders, names] = await Promise.all([languagesPromise, foldersPromise, namesPromise])
70
+
71
+ return {
72
+ languages,
73
+ folders,
74
+ names,
75
+ }
76
+ }
77
+
78
+ /**
79
+ * internal function to serialize saving category and folder name translations to avoid Freshdesk rate limit
80
+ * @param strings - the string data pulled from Transifex
81
+ * @param resource - the `attributes` property of the resource object which contains these strings
82
+ * @param locale - the Transifex locale code corresponding to these strings
83
+ */
84
+ const serializeNameSave = async (
85
+ strings: TransifexStringsKeyValueJson,
86
+ resource: TransifexResourceObject,
87
+ locale: string,
88
+ ): Promise<void> => {
89
+ for (const [key, value] of Object.entries(strings)) {
90
+ // key is of the form <name>_<id>
91
+ const words = key.split('_')
92
+ const id = parseIntOrThrow(words[words.length - 1], 10)
93
+ let status
94
+ if (resource.attributes.name === 'categoryNames_json') {
95
+ status = await FD.updateCategoryTranslation(id, freshdeskLocale(locale), { name: value })
96
+ }
97
+ if (resource.attributes.name === 'folderNames_json') {
98
+ status = await FD.updateFolderTranslation(id, freshdeskLocale(locale), { name: value })
99
+ }
100
+ if (status === -1) {
101
+ process.exitCode = 1
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * We use this specific structure in the `STRUCTUREDJSON` resources associated with our Freshdesk folders.
108
+ * This should be compatible with (and stricter than) `TransifexStringStructuredJson`.
109
+ */
110
+ interface FreshdeskFolderInTransifex {
111
+ title: { string: string }
112
+ description: { string: string }
113
+ tags: { string: string }
114
+ }
115
+
116
+ /**
117
+ * Internal function serialize Freshdesk requests to avoid getting rate limited
118
+ * @param json object with keys corresponding to article ids
119
+ * @param locale language code
120
+ * @returns a numeric status code
121
+ */
122
+ const serializeFolderSave = async (json: TransifexStrings<FreshdeskFolderInTransifex>, locale: string) => {
123
+ for (const [idString, value] of Object.entries(json)) {
124
+ const id = parseIntOrThrow(idString, 10)
125
+ const body: FreshdeskArticleCreate = {
126
+ title: value.title.string,
127
+ description: value.description.string,
128
+ status: FreshdeskArticleStatus.published,
129
+ }
130
+ if (Object.prototype.hasOwnProperty.call(value, 'tags')) {
131
+ const tags = value.tags.string.split(',')
132
+ const validTags = tags.filter(tag => tag.length < 33)
133
+ if (validTags.length !== tags.length) {
134
+ process.stdout.write(`Warning: tags too long in ${id} for ${locale}\n`)
135
+ }
136
+ body.tags = validTags
137
+ }
138
+ const status = await FD.updateArticleTranslation(id, freshdeskLocale(locale), body)
139
+ if (status === -1) {
140
+ // eslint-disable-next-line require-atomic-updates -- `process` will not change across `await`
141
+ process.exitCode = 1
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Process Transifex resource corresponding to a Knowledge base folder on Freshdesk
148
+ * @param folderAttributes Transifex resource json corresponding to a KB folder
149
+ * @param locale locale to pull and submit to Freshdesk
150
+ */
151
+ export const localizeFolder = async (folderAttributes: TransifexResourceObject, locale: string) => {
152
+ try {
153
+ const data = await txPull<FreshdeskFolderInTransifex>(
154
+ TX_PROJECT,
155
+ folderAttributes.attributes.slug,
156
+ locale,
157
+ 'default',
158
+ )
159
+ await serializeFolderSave(data, locale)
160
+ } catch (e) {
161
+ process.stdout.write(`Error processing ${folderAttributes.attributes.slug}, ${locale}: ${(e as Error).message}\n`)
162
+ process.exitCode = 1 // not ok
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Save Transifex resource corresponding to a Knowledge base folder locally for debugging
168
+ * @param folderAttributes Transifex resource json corresponding to a KB folder
169
+ * @param locale locale to pull and save
170
+ */
171
+ export const debugFolder = async (folderAttributes: TransifexResourceObject, locale: string) => {
172
+ await mkdirp('tmpDebug')
173
+ await txPull(TX_PROJECT, folderAttributes.attributes.slug, locale, 'default')
174
+ .then(data =>
175
+ fsPromises.writeFile(
176
+ `tmpDebug/${folderAttributes.attributes.slug}_${locale}.json`,
177
+ JSON.stringify(data, null, 2),
178
+ ),
179
+ )
180
+ .catch(e => {
181
+ process.stdout.write(
182
+ `Error processing ${folderAttributes.attributes.slug}, ${locale}: ${(e as Error).message}\n`,
183
+ )
184
+ process.exitCode = 1 // not ok
185
+ })
186
+ }
187
+
188
+ /**
189
+ * Process KEYVALUEJSON resources from scratch-help on transifex
190
+ * Category and Folder names are stored as plain json
191
+ * @param resource Transifex resource json for either CategoryNames or FolderNames
192
+ * @param locale locale to pull and submit to Freshdesk
193
+ */
194
+ export const localizeNames = async (resource: TransifexResourceObject, locale: string): Promise<void> => {
195
+ await txPull<TransifexStringKeyValueJson>(TX_PROJECT, resource.attributes.slug, locale, 'default')
196
+ .then(data => serializeNameSave(data, resource, locale))
197
+ .catch(e => {
198
+ process.stdout.write(`Error saving ${resource.attributes.slug}, ${locale}: ${(e as Error).message}\n`)
199
+ process.exitCode = 1 // not ok
200
+ })
201
+ }
202
+
203
+ const BATCH_SIZE = 2
204
+
205
+ type SaveFn = (item: TransifexResourceObject, language: string) => Promise<void>
206
+
207
+ /**
208
+ * save resource items in batches to reduce rate limiting errors
209
+ * @param item Transifex resource json, used for 'slug'
210
+ * @param languages Array of languages to save
211
+ * @param saveFn Async function to use to save the item
212
+ */
213
+ export const saveItem = async (item: TransifexResourceObject, languages: string[], saveFn: SaveFn) => {
214
+ const saveLanguages = languages.filter(l => l !== 'en') // exclude English from update
215
+ for (let i = 0; i < saveLanguages.length; i += BATCH_SIZE) {
216
+ await Promise.all(saveLanguages.slice(i, i + BATCH_SIZE).map(l => saveFn(item, l))).catch(err => {
217
+ process.stdout.write(`Error saving item:${(err as Error).message}\n${JSON.stringify(item, null, 2)}\n`)
218
+ process.exitCode = 1 // not ok
219
+ })
220
+ }
221
+ }
@@ -2,19 +2,24 @@
2
2
  * Helper class to log progress.
3
3
  */
4
4
  export class ProgressLogger {
5
+ total?: number
6
+ completed: number
7
+ percent?: number
8
+
5
9
  /**
6
- * @param {number} [total] Optional: expected total number of items to process.
10
+ * @param [total] - Optional: expected total number of items to process.
7
11
  */
8
- constructor(total) {
12
+ constructor(total?: number) {
9
13
  this.total = total
10
14
  this.completed = 0
15
+ this.percent = 0
11
16
  }
12
17
 
13
18
  /**
14
19
  * Set the expected total number of items to process.
15
- * @param {number} total Total number of items to process.
20
+ * @param total - Total number of items to process.
16
21
  */
17
- setTotal(total) {
22
+ setTotal(total: number) {
18
23
  if (this.total !== total) {
19
24
  this.total = total
20
25
  delete this.percent
@@ -25,7 +30,7 @@ export class ProgressLogger {
25
30
  * Increment the number of items processed and log progress.
26
31
  * If a total is set, progress is logged as a percentage and only when the percentage changes.
27
32
  * If no total is set, progress is logged as a count.
28
- * @param {number} [count] Number of items processed.
33
+ * @param [count] - Number of items processed.
29
34
  */
30
35
  increment(count = 1) {
31
36
  this.completed += count
@@ -0,0 +1,53 @@
1
+ /**
2
+ * A set of strings from a Transifex resource.
3
+ */
4
+ export type TransifexStrings<T> = Record<string, T>
5
+
6
+ /**
7
+ * A single string from a resource that uses the "CHROME" file type.
8
+ * Scratch: used for most (but not all) Scratch Editor resources.
9
+ */
10
+ export interface TransifexStringChrome {
11
+ /** The source or translation text */
12
+ message: string
13
+ /** Description or context information for translators */
14
+ description?: string
15
+ }
16
+
17
+ /**
18
+ * A set of strings from a resource that uses the "CHROME" file type.
19
+ * Scratch: used for most (but not all) Scratch Editor resources.
20
+ */
21
+ export type TransifexStringsChrome = TransifexStrings<TransifexStringChrome>
22
+
23
+ /**
24
+ * A single string from a resource that uses the "KEYVALUEJSON" file type.
25
+ * Scratch: used for the Scratch Website project, the Scratch Editor blocks resource, and some Scratch Help resources.
26
+ */
27
+ export type TransifexStringKeyValueJson = string
28
+
29
+ /**
30
+ * A set of strings from a resource that uses the "KEYVALUEJSON" file type.
31
+ * Scratch: used for the Scratch Website project, the Scratch Editor blocks resource, and some Scratch Help resources.
32
+ */
33
+ export type TransifexStringsKeyValueJson = TransifexStrings<TransifexStringKeyValueJson>
34
+
35
+ /**
36
+ * A single string from a resource that uses the "STRUCTUREDJSON" file type.
37
+ * Scratch: used for most (but not all) Scratch Help resources.
38
+ */
39
+ export interface TransifexStringStructuredJson {
40
+ string?: string
41
+ context?: string
42
+ developer_comment?: string
43
+ character_limit?: number
44
+ plurals?: object
45
+
46
+ [key: string]: TransifexStringStructuredJson | string | number | object | undefined
47
+ }
48
+
49
+ /**
50
+ * A set of strings from a resource that uses the "STRUCTUREDJSON" file type.
51
+ * Scratch: used for most (but not all) Scratch Help resources.
52
+ */
53
+ export type TransifexStringsStructuredJson = TransifexStrings<TransifexStringStructuredJson>
@@ -0,0 +1,143 @@
1
+ import { JsonApiResource } from '@transifex/api'
2
+
3
+ // Writing these types is very manual, so I've only written the ones used by our scripts. The types are adapted from
4
+ // the documentation of the Transifex API, usually from the description of the `data` field in a 200 response to some
5
+ // "list" or "get details" kind of call.
6
+
7
+ /**
8
+ * Properties common to all Transifex API objects. I chose the generic term "Object" for what JSON API calls a
9
+ * "Resource" since "Resource" has a specific meaning in a Transifex context.
10
+ */
11
+ export interface TransifexObject extends JsonApiResource {
12
+ /** The type of object. Use this to determine the specific type to use for this object. */
13
+ type: string
14
+ /** Unique identifier for this object. */
15
+ id: string
16
+ /** The attributes of this object. */
17
+ attributes: Record<string, unknown>
18
+ /** The relationships of this object. */
19
+ relationships: Record<string, unknown>
20
+ /** The URL links of the object. */
21
+ links: Record<string, string>
22
+ }
23
+
24
+ /**
25
+ * Transifex object representing a Language.
26
+ * @see https://developers.transifex.com/reference/get_languages-language-id
27
+ */
28
+ export interface TransifexLanguageObject extends TransifexObject {
29
+ /** The type of the resource.*/
30
+ type: 'languages'
31
+ /** Language identifier. Example: `l:en_US` */
32
+ id: `l:${string}`
33
+ attributes: {
34
+ /** The language code as defined in CLDR. Example: `en`. */
35
+ code: string
36
+ /** The name of the language as defined in CLDR. Example: `English`. */
37
+ name: string
38
+ /** Whether the language is right-to-left. */
39
+ rtl: boolean
40
+ /** The language plural rule equation as defined in CLDR. Example: `(n != 1)`. */
41
+ plural_equation: string
42
+ /** Object of plural rules for Language as defined in CLDR. */
43
+ plural_rules: object
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Transifex object representing a Project.
49
+ * @see https://developers.transifex.com/reference/get_projects-project-id
50
+ */
51
+ export interface TransifexProjectObject extends TransifexObject {
52
+ /** The type of the resource.*/
53
+ type: 'projects'
54
+ /** Project identifier. Example: `o:org_slug:p:project_slug` */
55
+ id: `o:${string}:p:${string}`
56
+ attributes: {
57
+ /**
58
+ * If the project is archived or not.
59
+ * If a project is archived the pricing will be lower but no action will be available.
60
+ */
61
+ archived: boolean
62
+ /** The date and time the project was created. */
63
+ datetime_created: string
64
+ /** The date and time the project was last modified. */
65
+ datetime_modified: string
66
+ /** A description of the project. */
67
+ description: string
68
+ /** The homepage of the project. */
69
+ homepage_url: string
70
+ /**
71
+ * A web page containing documentation or instructions for translators, or localization tips for your
72
+ * community.
73
+ */
74
+ instructions_url: string
75
+ /** The license of the project. */
76
+ license: string
77
+ /** The URL of the project's logo. */
78
+ logo_url: string
79
+ /** A long description of the project. */
80
+ long_description: string
81
+ /** If the resources of the project will be filled up from a machine translation. */
82
+ machine_translation_fillup: boolean
83
+ /** The name of the project. */
84
+ name: string
85
+ /** Whether the project is private. A private project is visible only by you and your team. */
86
+ private: boolean
87
+ /** The URL of the public source code repository. */
88
+ repository_url: string
89
+ /** The slug of the project. Example: `project_slug`. */
90
+ slug: string
91
+ /** List of tags for the project. */
92
+ tags: string[]
93
+ /** If the resources of the project will be filled up from common translation memory. */
94
+ translation_memory_fillup: boolean
95
+ /** The type of the project. */
96
+ type: 'live' | 'file'
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Transifex object representing a Resource.
102
+ * @see https://developers.transifex.com/reference/get_resources-resource-id
103
+ */
104
+ export interface TransifexResourceObject extends TransifexObject {
105
+ /** The type of the resource.*/
106
+ type: 'resources'
107
+ /** Resource identifier. Example: `o:org_slug:p:project_slug:r:resource_slug` */
108
+ id: `o:${string}:p:${string}:r:${string}`
109
+ attributes: {
110
+ /** The slug of the resource. Example: `resource_slug`. */
111
+ slug: string
112
+ /** The name of the resource. */
113
+ name: string
114
+ /** The priority of the resource. */
115
+ priority: 'normal' | 'high' | 'urgent'
116
+ /** The format of the resource. Example: `STRUCTURED_JSON`. */
117
+ i18n_type: string
118
+ /** File format type version. */
119
+ i18n_version: number
120
+ /** Whether the resource should accept translations or not. */
121
+ accept_translations: boolean
122
+ /** The number of strings in the resource content. */
123
+ string_count: number
124
+ /** The number of words in the resource content. */
125
+ word_count: string
126
+ /** The date and time the resource was created. */
127
+ datetime_created: string
128
+ /** The date and time the resource was last modified. */
129
+ datetime_modified: string
130
+ /** List of categories to associate similar resources. */
131
+ categories: string[]
132
+ /** Options that determine how the resource will be parsed and compiled. */
133
+ i18n_options: Record<string, unknown>
134
+ /** A (public) URL to provide an MP4 video file for subtitle translation. */
135
+ mp4_url: string
136
+ /** A (public) URL to provide an OGG video file for subtitle translation. */
137
+ ogg_url: string
138
+ /** A (public) URL to provide a YouTube video file for subtitle translation. */
139
+ youtube_url: string
140
+ /** A (public) URL to provide a WEBM video file for subtitle translation. */
141
+ webm_url: string
142
+ }
143
+ }