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
@@ -1,54 +0,0 @@
1
- name: Daily TX Pull
2
-
3
- on:
4
- workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
5
- schedule:
6
- # daily-tx-pull (e.g., daily at 3 AM UTC)
7
- - cron: '0 3 * * *'
8
-
9
- concurrency:
10
- group: '${{ github.workflow }}'
11
- cancel-in-progress: true
12
-
13
- permissions:
14
- contents: write # publish a GitHub release
15
- pages: write # deploy to GitHub Pages
16
- issues: write # comment on released issues
17
- pull-requests: write # comment on released pull requests
18
-
19
- jobs:
20
- daily-tx-pull:
21
- runs-on: ubuntu-latest
22
-
23
- env:
24
- # Organization-wide secrets
25
- TX_TOKEN: ${{ secrets.TX_TOKEN }}
26
-
27
- steps:
28
- - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
29
- - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
30
- with:
31
- cache: 'npm'
32
- node-version-file: '.nvmrc'
33
-
34
- - name: Install dependencies
35
- run: npm ci
36
-
37
- - name: Pull editor and www translations
38
- run: |
39
- npm run pull:editor
40
- npm run pull:www
41
- npm run test
42
-
43
- - name: Commit translation updates
44
- id: commit
45
- run: |
46
- git config --global user.email $(git log --pretty=format:"%ae" -n1)
47
- git config --global user.name $(git log --pretty=format:"%an" -n1)
48
- git add .
49
- if git diff --cached --exit-code --quiet; then
50
- echo "Nothing to commit."
51
- else
52
- git commit -m "fix: pull new editor translations from Transifex"
53
- git push
54
- fi
@@ -1,31 +0,0 @@
1
- name: 'Signature Assistant'
2
- on:
3
- issue_comment:
4
- types: [created]
5
- pull_request_target:
6
- types: [opened, closed, synchronize]
7
-
8
- permissions:
9
- actions: write
10
- contents: read
11
- pull-requests: write
12
- statuses: write
13
-
14
- jobs:
15
- CLA-Assistant:
16
- runs-on: ubuntu-latest
17
- steps:
18
- - name: 'CLA Assistant'
19
- if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
20
- uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
21
- env:
22
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23
- # the below token should have repo scope and must be manually added by you in the repository's secrets
24
- PERSONAL_ACCESS_TOKEN: ${{ secrets.GHA_AGREEMENTS_PAT }}
25
- with:
26
- remote-organization-name: 'scratchfoundation'
27
- remote-repository-name: 'scratch-agreements'
28
- path-to-signatures: 'signatures/version1/cla.json'
29
- path-to-document: 'https://github.com/scratchfoundation/scratch-agreements/blob/main/CLA.md'
30
- branch: 'main'
31
- allowlist: semantic-release-bot,*[bot]
package/lib/batch.js DELETED
@@ -1,15 +0,0 @@
1
- /**
2
- * Maps each value of an array into an async function, and returns an array of the results
3
- * @param {Array} arr - array of values
4
- * @param {number} batchSize - number of calls to `func` to do at one time
5
- * @param {Function} func - async function to apply to all items in `arr`. Function should take one argument.
6
- * @returns {Promise<Array>} - results of `func` applied to each item in `arr`
7
- */
8
- exports.batchMap = async (arr, batchSize, func) => {
9
- const results = []
10
- for (let i = 0; i < arr.length; i += batchSize) {
11
- const result = await Promise.all(arr.slice(i, i + batchSize).map(func))
12
- results.push(...result)
13
- }
14
- return results
15
- }
package/lib/transifex.js DELETED
@@ -1,242 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * @file
5
- * Utilities for interfacing with Transifex API 3.
6
- */
7
-
8
- const transifexApi = require('@transifex/api').transifexApi
9
- const download = require('download')
10
-
11
- /**
12
- * @import {Collection, JsonApiResource} from '@transifex/api';
13
- */
14
-
15
- const ORG_NAME = 'llk'
16
- const SOURCE_LOCALE = 'en'
17
-
18
- try {
19
- transifexApi.setup({
20
- auth: process.env.TX_TOKEN,
21
- })
22
- } catch (err) {
23
- if (!process.env.TX_TOKEN) {
24
- throw new Error('TX_TOKEN is not defined.')
25
- }
26
- throw err
27
- }
28
-
29
- /*
30
- * The Transifex JS API wraps the Transifex JSON API, and is built around the concept of a `Collection`.
31
- * A `Collection` begins as a URL builder: methods like `filter` and `sort` add query parameters to the URL.
32
- * The `download` method doesn't actually download anything: it returns the built URL. It seems to be intended
33
- * primarily for internal use, but shows up in the documentation despite not being advertised in the .d.ts file.
34
- * The `download` method is mainly used to skip the `fetch` method in favor of downloading the resource yourself.
35
- * The `fetch` method sends a request to the URL and returns a promise that resolves to the first page of results.
36
- * If there's only one page of results, the `data` property of the collection object will be an array of all results.
37
- * However, if there are multiple pages of results, the `data` property will only contain the first page of results.
38
- * Previous versions of this code would unsafely assume that the `data` property contained all results.
39
- * The `all` method returns an async iterator that yields all results, fetching additional pages as needed.
40
- */
41
-
42
- /**
43
- * Collects all resources from all pages of a potentially-paginated Transifex collection.
44
- * It's not necessary, but also not harmful, to call `fetch()` on the collection before calling this function.
45
- * @param {Collection} collection A collection of Transifex resources.
46
- * @returns {Promise<JsonApiResource[]>} An array of all resources in the collection.
47
- */
48
- const collectAll = async function (collection) {
49
- await collection.fetch() // fetch the first page if it hasn't already been fetched
50
- const collected = []
51
- for await (const item of collection.all()) {
52
- collected.push(item)
53
- }
54
- return collected
55
- }
56
-
57
- /**
58
- * Creates a download event for a specific project, resource, and locale.
59
- * Returns the URL to download the resource.
60
- * @param {string} projectSlug - project slug (for example, "scratch-editor")
61
- * @param {string} resourceSlug - resource slug (for example, "blocks")
62
- * @param {string} localeCode - language code (for example, "ko")
63
- * @param {string} mode - translation status of strings to include
64
- * @returns {Promise<string>} - URL to download the resource
65
- */
66
- const getResourceLocation = async function (projectSlug, resourceSlug, localeCode, mode = 'default') {
67
- const resource = {
68
- data: {
69
- id: `o:${ORG_NAME}:p:${projectSlug}:r:${resourceSlug}`,
70
- type: 'resources',
71
- },
72
- }
73
-
74
- // if locale is English, create a download event of the source file
75
- if (localeCode === SOURCE_LOCALE) {
76
- return await transifexApi.ResourceStringsAsyncDownload.download({
77
- resource,
78
- })
79
- }
80
-
81
- const language = {
82
- data: {
83
- id: `l:${localeCode}`,
84
- type: 'languages',
85
- },
86
- }
87
-
88
- // if locale is not English, create a download event of the translation file
89
- return await transifexApi.ResourceTranslationsAsyncDownload.download({
90
- mode,
91
- resource,
92
- language,
93
- })
94
- }
95
-
96
- /**
97
- * Pulls a translation json from transifex, for a specific project, resource, and locale.
98
- * @param {string} project - project slug (for example, "scratch-editor")
99
- * @param {string} resource - resource slug (for example, "blocks")
100
- * @param {string} locale - language code (for example, "ko")
101
- * @param {string} mode - translation status of strings to include
102
- * @returns {Promise<object>} - JSON object of translated resource strings (or, of the original resource
103
- * strings, if the local is the source language)
104
- */
105
- const txPull = async function (project, resource, locale, mode = 'default') {
106
- let buffer
107
- try {
108
- const url = await getResourceLocation(project, resource, locale, mode)
109
- for (let i = 0; i < 5; i++) {
110
- if (i > 0) {
111
- console.log(`Retrying txPull download after ${i} failed attempt(s)`)
112
- }
113
- try {
114
- buffer = await download(url) // might throw(?)
115
- break
116
- } catch (e) {
117
- console.error(e, { project, resource, locale, buffer })
118
- }
119
- }
120
- if (!buffer) {
121
- throw Error(`txPull download failed after 5 retries: ${url}`)
122
- }
123
- buffer = buffer.toString()
124
- return JSON.parse(buffer)
125
- } catch (e) {
126
- e.cause = {
127
- project,
128
- resource,
129
- locale,
130
- buffer,
131
- }
132
- throw e
133
- }
134
- }
135
-
136
- /**
137
- * Given a project, returns a list of the slugs of all resources in the project
138
- * @param {string} project - project slug (for example, "scratch-website")
139
- * @returns {Promise<Array>} - array of strings, slugs identifying each resource in the project
140
- */
141
- const txResources = async function (project) {
142
- const resources = transifexApi.Resource.filter({
143
- project: `o:${ORG_NAME}:p:${project}`,
144
- })
145
-
146
- const resourcesData = await collectAll(resources)
147
-
148
- const slugs = resourcesData.map(
149
- r =>
150
- // r.id is a longer id string, like "o:llk:p:scratch-website:r:about-l10njson"
151
- // We just want the slug that comes after ":r:" ("about-l10njson")
152
- r.id.split(':r:')[1],
153
- )
154
- return slugs
155
- }
156
-
157
- /**
158
- * @param {string} project - project slug (for example)
159
- * @returns {Promise<JsonApiResource[]>} - array of resource objects
160
- */
161
- const txResourcesObjects = async function (project) {
162
- const resources = transifexApi.Resource.filter({
163
- project: `o:${ORG_NAME}:p:${project}`,
164
- })
165
-
166
- return collectAll(resources)
167
- }
168
-
169
- /**
170
- * Gets available languages for a project
171
- * @param {string} slug - project slug (for example, "scratch-editor")
172
- * @returns {Promise<string[]>} - list of language codes
173
- */
174
- const txAvailableLanguages = async function (slug) {
175
- const project = await transifexApi.Project.get({
176
- organization: `o:${ORG_NAME}`,
177
- slug: slug,
178
- })
179
-
180
- const languages = await project.fetch('languages')
181
- const languagesData = await collectAll(languages)
182
- return languagesData.map(l => l.attributes.code)
183
- }
184
-
185
- /**
186
- * Uploads English source strings to a resource in transifex
187
- * @param {string} project - project slug (for example, "scratch-editor")
188
- * @param {string} resource - resource slug (for example, "blocks")
189
- * @param {object} sourceStrings - json of source strings
190
- */
191
- const txPush = async function (project, resource, sourceStrings) {
192
- const resourceObj = {
193
- data: {
194
- id: `o:${ORG_NAME}:p:${project}:r:${resource}`,
195
- type: 'resources',
196
- },
197
- }
198
-
199
- await transifexApi.ResourceStringsAsyncUpload.upload({
200
- resource: resourceObj,
201
- content: JSON.stringify(sourceStrings),
202
- })
203
- }
204
-
205
- /**
206
- * Creates a new resource, and then uploads source strings to it if they are provided
207
- * @param {string} project - project slug (for example, "scratch-editor")
208
- * @param {object} resource - object of resource information
209
- * @param {string} resource.slug - resource slug (for example, "blocks")
210
- * @param {string} resource.name - human-readable name for the resource
211
- * @param {string} resource.i18nType - i18n format id
212
- * @param {object} resource.sourceStrings - json object of source strings
213
- */
214
- const txCreateResource = async function (project, { slug, name, i18nType, sourceStrings }) {
215
- const i18nFormat = {
216
- data: {
217
- id: i18nType || 'KEYVALUEJSON',
218
- type: 'i18n_formats',
219
- },
220
- }
221
-
222
- const projectObj = {
223
- data: {
224
- id: `o:${ORG_NAME}:p:${project}`,
225
- type: 'projects',
226
- },
227
- }
228
-
229
- await transifexApi.Resource.create({
230
- attributes: { slug: slug, name: name },
231
- relationships: {
232
- i18n_format: i18nFormat,
233
- project: projectObj,
234
- },
235
- })
236
-
237
- if (sourceStrings) {
238
- await txPush(project, slug, sourceStrings)
239
- }
240
- }
241
-
242
- module.exports = { txPull, txPush, txResources, txResourcesObjects, txCreateResource, txAvailableLanguages }
package/lib/validate.mjs DELETED
@@ -1,48 +0,0 @@
1
- import assert from 'assert'
2
- import parse from 'format-message-parse'
3
-
4
- // filter placeholders out of a message
5
- // parse('a message with a {value} and {count, plural, one {one} other {more}}.')
6
- // returns an array:
7
- // [ 'a message with a ',
8
- // [ 'value' ],
9
- // ' and ',
10
- // [ 'count', 'plural', 0, { one: [Array], other: [Array] } ],
11
- // '.'
12
- // ]
13
- // placeholders are always an array, so filter for array elements to find the placeholders
14
- const placeholders = message =>
15
- // this will throw an error if the message is not valid ICU
16
- // single quote (as in French l'année) messes up the parse and is not
17
- // relevant for this check, so strip them out
18
- parse(message.replace(/'/g, '')).filter(item => Array.isArray(item))
19
-
20
- const validMessage = (message, source) => {
21
- const transPlaceholders = placeholders(message.toString())
22
- const srcPlaceholders = placeholders(source.toString())
23
- // different number of placeholders
24
- if (transPlaceholders.length !== srcPlaceholders.length) {
25
- return false
26
- }
27
- // TODO: Add checking to make sure placeholders in source have not been translated
28
- // TODO: Add validation of scratch-blocks placeholders
29
- return true
30
- }
31
-
32
- const validateTranslations = (translation, source) => {
33
- const locale = translation.locale
34
- const translations = translation.translations
35
- const transKeys = Object.keys(translations)
36
- const sourceKeys = Object.keys(source)
37
- assert.strictEqual(transKeys.length, sourceKeys.length, `locale ${locale} has a different number of message keys`)
38
- transKeys.map(item => assert(sourceKeys.includes(item), `locale ${locale} has key ${item} not in the source`))
39
- sourceKeys.map(item => assert(transKeys.includes(item), `locale ${locale} is missing key ${item}`))
40
- sourceKeys.map(item =>
41
- assert(
42
- validMessage(translations[item], source[item]),
43
- `locale ${locale}: "${translations[item]}" is not a valid translation for "${source[item]}"`,
44
- ),
45
- )
46
- }
47
-
48
- export { validateTranslations, validMessage }
@@ -1,149 +0,0 @@
1
- // interface to FreshDesk Solutions (knowledge base) api
2
-
3
- const fetch = require('node-fetch')
4
- class FreshdeskApi {
5
- constructor(baseUrl, apiKey) {
6
- this.baseUrl = baseUrl
7
- this._auth = 'Basic ' + new Buffer(`${apiKey}:X`).toString('base64')
8
- this.defaultHeaders = {
9
- 'Content-Type': 'application/json',
10
- Authorization: this._auth,
11
- }
12
- this.rateLimited = false
13
- }
14
-
15
- /**
16
- * Checks the status of a response. If status is not ok, or the body is not json raise exception
17
- * @param {object} res The response object
18
- * @returns {object} the response if it is ok
19
- */
20
- checkStatus(res) {
21
- if (res.ok) {
22
- if (res.headers.get('content-type').indexOf('application/json') !== -1) {
23
- return res
24
- }
25
- throw new Error(`response not json: ${res.headers.get('content-type')}`)
26
- }
27
- const err = new Error(`response ${res.statusText}`)
28
- err.code = res.status
29
- if (res.status === 429) {
30
- err.retryAfter = res.headers.get('Retry-After')
31
- }
32
- throw err
33
- }
34
-
35
- listCategories() {
36
- return fetch(`${this.baseUrl}/api/v2/solutions/categories`, { headers: this.defaultHeaders })
37
- .then(this.checkStatus)
38
- .then(res => res.json())
39
- }
40
-
41
- listFolders(category) {
42
- return fetch(`${this.baseUrl}/api/v2/solutions/categories/${category.id}/folders`, {
43
- headers: this.defaultHeaders,
44
- })
45
- .then(this.checkStatus)
46
- .then(res => res.json())
47
- }
48
-
49
- listArticles(folder) {
50
- return fetch(`${this.baseUrl}/api/v2/solutions/folders/${folder.id}/articles`, { headers: this.defaultHeaders })
51
- .then(this.checkStatus)
52
- .then(res => res.json())
53
- }
54
-
55
- updateCategoryTranslation(id, locale, body) {
56
- if (this.rateLimited) {
57
- process.stdout.write(`Rate limited, skipping id: ${id} for ${locale}\n`)
58
- return -1
59
- }
60
- return fetch(`${this.baseUrl}/api/v2/solutions/categories/${id}/${locale}`, {
61
- method: 'put',
62
- body: JSON.stringify(body),
63
- headers: this.defaultHeaders,
64
- })
65
- .then(this.checkStatus)
66
- .then(res => res.json())
67
- .catch(err => {
68
- if (err.code === 404) {
69
- // not found, try create instead
70
- return fetch(`${this.baseUrl}/api/v2/solutions/categories/${id}/${locale}`, {
71
- method: 'post',
72
- body: JSON.stringify(body),
73
- headers: this.defaultHeaders,
74
- })
75
- .then(this.checkStatus)
76
- .then(res => res.json())
77
- }
78
- if (err.code === 429) {
79
- this.rateLimited = true
80
- }
81
- process.stdout.write(`Error processing id ${id} for locale ${locale}: ${err.message}\n`)
82
- throw err
83
- })
84
- }
85
-
86
- updateFolderTranslation(id, locale, body) {
87
- if (this.rateLimited) {
88
- process.stdout.write(`Rate limited, skipping id: ${id} for ${locale}\n`)
89
- return -1
90
- }
91
- return fetch(`${this.baseUrl}/api/v2/solutions/folders/${id}/${locale}`, {
92
- method: 'put',
93
- body: JSON.stringify(body),
94
- headers: this.defaultHeaders,
95
- })
96
- .then(this.checkStatus)
97
- .then(res => res.json())
98
- .catch(err => {
99
- if (err.code === 404) {
100
- // not found, try create instead
101
- return fetch(`${this.baseUrl}/api/v2/solutions/folders/${id}/${locale}`, {
102
- method: 'post',
103
- body: JSON.stringify(body),
104
- headers: this.defaultHeaders,
105
- })
106
- .then(this.checkStatus)
107
- .then(res => res.json())
108
- }
109
- if (err.code === 429) {
110
- this.rateLimited = true
111
- }
112
- process.stdout.write(`Error processing id ${id} for locale ${locale}: ${err.message}\n`)
113
- throw err
114
- })
115
- }
116
-
117
- updateArticleTranslation(id, locale, body) {
118
- if (this.rateLimited) {
119
- process.stdout.write(`Rate limited, skipping id: ${id} for ${locale}\n`)
120
- return -1
121
- }
122
- return fetch(`${this.baseUrl}/api/v2/solutions/articles/${id}/${locale}`, {
123
- method: 'put',
124
- body: JSON.stringify(body),
125
- headers: this.defaultHeaders,
126
- })
127
- .then(this.checkStatus)
128
- .then(res => res.json())
129
- .catch(err => {
130
- if (err.code === 404) {
131
- // not found, try create instead
132
- return fetch(`${this.baseUrl}/api/v2/solutions/articles/${id}/${locale}`, {
133
- method: 'post',
134
- body: JSON.stringify(body),
135
- headers: this.defaultHeaders,
136
- })
137
- .then(this.checkStatus)
138
- .then(res => res.json())
139
- }
140
- if (err.code === 429) {
141
- this.rateLimited = true
142
- }
143
- process.stdout.write(`Error processing id ${id} for locale ${locale}: ${err.message}\n`)
144
- throw err
145
- })
146
- }
147
- }
148
-
149
- module.exports = FreshdeskApi