scratch-l10n 6.0.75 → 6.1.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 +12 -0
- package/package.json +1 -1
- package/scripts/lib/pull-and-validate.mts +142 -0
- package/scripts/lib/validate.mts +44 -17
- package/scripts/tx-pull-editor.mts +13 -35
- package/scripts/tx-pull-www.mts +23 -60
- package/scripts/validate-translations.mts +4 -5
- package/scripts/validate-www.mts +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See
|
|
4
4
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [6.1.0](https://github.com/scratchfoundation/scratch-l10n/compare/v6.0.75...v6.1.0) (2025-10-30)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* apply suggestions from code review ([19fb2c4](https://github.com/scratchfoundation/scratch-l10n/commit/19fb2c424025360955d64b2dec84b22c3bf9c19d))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* continue after validation error ([08be90e](https://github.com/scratchfoundation/scratch-l10n/commit/08be90e3dc362fb0104e29cb96fcd60e8497b6a8))
|
|
17
|
+
|
|
6
18
|
## [6.0.75](https://github.com/scratchfoundation/scratch-l10n/compare/v6.0.74...v6.0.75) (2025-10-30)
|
|
7
19
|
|
|
8
20
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import locales, { localeMap } from '../../src/supported-locales.mjs'
|
|
2
|
+
import { poolMap } from './concurrent.mts'
|
|
3
|
+
import { ProgressLogger } from './progress-logger.mts'
|
|
4
|
+
import { TransifexStringsKeyValueJson } from './transifex-formats.mts'
|
|
5
|
+
import { txPull, txResources } from './transifex.mts'
|
|
6
|
+
import { filterInvalidTranslations, TransifexEditorString } from './validate.mts'
|
|
7
|
+
|
|
8
|
+
const CONCURRENCY_LIMIT = 36
|
|
9
|
+
const SOURCE_LOCALE = 'en' // TODO: don't hardcode this
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param resources - A list of Transifex resource names
|
|
13
|
+
* @param selectedLocales - A list of Scratch locale codes
|
|
14
|
+
* @returns The list of all resource/locale combinations to pull. The source locale is always excluded.
|
|
15
|
+
*/
|
|
16
|
+
const expandResourceFiles = (resources: string[], selectedLocales: string[]) => {
|
|
17
|
+
const files = []
|
|
18
|
+
for (const resource of resources) {
|
|
19
|
+
for (const locale of selectedLocales) {
|
|
20
|
+
if (locale === SOURCE_LOCALE) {
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
files.push({ resource: resource, locale: locale })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return files
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pull and validate a single Transifex "file" (resource + locale).
|
|
31
|
+
* @param o - The options for pulling and validating a single file.
|
|
32
|
+
* @param o.allStrings - All pulled strings so far, used to get source strings for validation
|
|
33
|
+
* @param o.project - The Transifex project to pull from
|
|
34
|
+
* @param o.resource - The Transifex resource to pull from
|
|
35
|
+
* @param o.locale - The locale to pull
|
|
36
|
+
* @param o.mode - The mode to use when pulling (e.g., "reviewed")
|
|
37
|
+
* @returns A list of messages about errors encountered during validation, if any.
|
|
38
|
+
*/
|
|
39
|
+
async function pullAndValidateFile({
|
|
40
|
+
allStrings,
|
|
41
|
+
project,
|
|
42
|
+
resource,
|
|
43
|
+
locale,
|
|
44
|
+
mode,
|
|
45
|
+
}: {
|
|
46
|
+
allStrings: Record<string, Record<string, TransifexStringsKeyValueJson>>
|
|
47
|
+
project: string
|
|
48
|
+
resource: string
|
|
49
|
+
locale: string
|
|
50
|
+
mode?: string
|
|
51
|
+
}) {
|
|
52
|
+
const messages: string[] = []
|
|
53
|
+
const txLocale = localeMap[locale] || locale
|
|
54
|
+
const fileContent = await txPull<TransifexEditorString>(project, resource, txLocale, mode)
|
|
55
|
+
|
|
56
|
+
// if fileContent has message & description, we only want the message
|
|
57
|
+
const translations: TransifexStringsKeyValueJson = {}
|
|
58
|
+
for (const key of Object.keys(fileContent)) {
|
|
59
|
+
const tx = fileContent[key]
|
|
60
|
+
if (typeof tx === 'string') {
|
|
61
|
+
translations[key] = tx
|
|
62
|
+
} else {
|
|
63
|
+
translations[key] = tx.message
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!(resource in allStrings)) {
|
|
68
|
+
allStrings[resource] = {}
|
|
69
|
+
}
|
|
70
|
+
allStrings[resource][locale] = translations
|
|
71
|
+
|
|
72
|
+
// may or may not be the same as `translations`
|
|
73
|
+
const sourceStrings = allStrings[resource][SOURCE_LOCALE]
|
|
74
|
+
|
|
75
|
+
// some of the validation checks may still be relevant even if locale === SOURCE_LOCALE
|
|
76
|
+
// console.log({ resource, locale, translations, sourceStrings })
|
|
77
|
+
messages.push(...filterInvalidTranslations(locale, translations, sourceStrings))
|
|
78
|
+
|
|
79
|
+
return messages
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Pull one or more resource(s) from a transifex project and validate the strings.
|
|
84
|
+
* Return any error messages from the validation process along with all valid strings.
|
|
85
|
+
* @param o - The options for pulling, validating, and saving translations.
|
|
86
|
+
* @param o.project - The Transifex project to pull translations from.
|
|
87
|
+
* @param o.mode - The mode to use when pulling translations (e.g., "reviewed").
|
|
88
|
+
* @param o.resources - The resources within the project to pull translations for.
|
|
89
|
+
* @param o.selectedLocales - The locales to pull translations for. Defaults to all supported locales.
|
|
90
|
+
* @returns Translation strings and a list of messages about errors encountered during validation, if any.
|
|
91
|
+
*/
|
|
92
|
+
export async function pullAndValidateProject({
|
|
93
|
+
project,
|
|
94
|
+
resources,
|
|
95
|
+
mode,
|
|
96
|
+
selectedLocales,
|
|
97
|
+
}: {
|
|
98
|
+
project: string
|
|
99
|
+
mode?: string
|
|
100
|
+
resources?: string[]
|
|
101
|
+
selectedLocales?: string | string[]
|
|
102
|
+
}) {
|
|
103
|
+
const selectedResources = resources ?? (await txResources(project))
|
|
104
|
+
selectedLocales =
|
|
105
|
+
typeof selectedLocales === 'string' ? [selectedLocales] : (selectedLocales ?? Object.keys(locales))
|
|
106
|
+
|
|
107
|
+
const files = expandResourceFiles(selectedResources, selectedLocales)
|
|
108
|
+
|
|
109
|
+
const allStrings: Record<string, Record<string, TransifexStringsKeyValueJson>> = {}
|
|
110
|
+
const messages: string[] = []
|
|
111
|
+
|
|
112
|
+
const progress = new ProgressLogger(selectedResources.length + files.length)
|
|
113
|
+
|
|
114
|
+
const handleFile = async (resource: string, locale: string) => {
|
|
115
|
+
try {
|
|
116
|
+
const fileMessages = await pullAndValidateFile({
|
|
117
|
+
allStrings,
|
|
118
|
+
project,
|
|
119
|
+
resource,
|
|
120
|
+
locale,
|
|
121
|
+
mode,
|
|
122
|
+
})
|
|
123
|
+
for (const message of fileMessages) {
|
|
124
|
+
// `message` already contains locale and/or string info if appropriate
|
|
125
|
+
messages.push(`resource ${resource} / ${message}`)
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
progress.increment()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Ensure source locale is available for validation
|
|
133
|
+
await poolMap(selectedResources, CONCURRENCY_LIMIT, async resource => handleFile(resource, SOURCE_LOCALE))
|
|
134
|
+
|
|
135
|
+
// Non-source locales
|
|
136
|
+
await poolMap(files, CONCURRENCY_LIMIT, async ({ resource, locale }) => handleFile(resource, locale))
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
allStrings,
|
|
140
|
+
messages,
|
|
141
|
+
}
|
|
142
|
+
}
|
package/scripts/lib/validate.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import assert from 'assert'
|
|
2
1
|
import parse from 'format-message-parse'
|
|
3
2
|
import {
|
|
4
3
|
TransifexStringChrome,
|
|
@@ -84,24 +83,52 @@ export const validMessage = (message: TransifexEditorString, source: TransifexEd
|
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @param
|
|
86
|
+
* Validate and filter translations.
|
|
87
|
+
* WARNING: Modifies the translations object in place by replacing invalid translations with source strings.
|
|
88
|
+
* @param locale - the Transifex locale, for error reporting
|
|
89
|
+
* @param translations - the translations to validate and filter
|
|
90
90
|
* @param source - the source strings for the translations
|
|
91
|
+
* @returns a list of messages about errors encountered during validation. Every removed translation will have a
|
|
92
|
+
* message. Some messages may not correspond to removed translations (e.g., when the number of keys differ).
|
|
91
93
|
*/
|
|
92
|
-
export const
|
|
93
|
-
|
|
94
|
+
export const filterInvalidTranslations = (
|
|
95
|
+
locale: string,
|
|
96
|
+
translations: TransifexEditorStrings,
|
|
94
97
|
source: TransifexEditorStrings,
|
|
95
|
-
) => {
|
|
96
|
-
const
|
|
98
|
+
): string[] => {
|
|
99
|
+
const messages: string[] = []
|
|
100
|
+
|
|
97
101
|
const sourceKeys = Object.keys(source)
|
|
98
|
-
|
|
99
|
-
transKeys
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
102
|
+
|
|
103
|
+
const transKeys = Object.keys(translations).filter(item => {
|
|
104
|
+
if (!sourceKeys.includes(item)) {
|
|
105
|
+
messages.push(`locale ${locale} has key ${item} not in the source`)
|
|
106
|
+
delete translations[item]
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
return true
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
sourceKeys.forEach(item => {
|
|
113
|
+
if (!transKeys.includes(item)) {
|
|
114
|
+
messages.push(`locale ${locale} is missing key ${item}`)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
transKeys.forEach(item => {
|
|
119
|
+
if (!validMessage(translations[item], source[item])) {
|
|
120
|
+
messages.push(
|
|
121
|
+
[
|
|
122
|
+
`locale ${locale} / item ${item}: message validation failed:`,
|
|
123
|
+
` msg: ${getMessageText(translations[item])}`,
|
|
124
|
+
` src: ${getMessageText(source[item])}`,
|
|
125
|
+
].join('\n'),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
// fall back to source message
|
|
129
|
+
translations[item] = source[item]
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return messages
|
|
107
134
|
}
|
|
@@ -8,11 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'fs'
|
|
10
10
|
import path from 'path'
|
|
11
|
-
import
|
|
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'
|
|
11
|
+
import { pullAndValidateProject } from './lib/pull-and-validate.mts'
|
|
16
12
|
|
|
17
13
|
const args = process.argv.slice(2)
|
|
18
14
|
|
|
@@ -37,38 +33,20 @@ const PROJECT = args[0]
|
|
|
37
33
|
const RESOURCE = args[1]
|
|
38
34
|
const OUTPUT_DIR = path.resolve(args[2])
|
|
39
35
|
const MODE = 'reviewed'
|
|
40
|
-
const CONCURRENCY_LIMIT = 36
|
|
41
36
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
translations: data,
|
|
48
|
-
}
|
|
49
|
-
}
|
|
37
|
+
const validationResults = await pullAndValidateProject({
|
|
38
|
+
project: PROJECT,
|
|
39
|
+
resources: [RESOURCE],
|
|
40
|
+
mode: MODE,
|
|
41
|
+
})
|
|
50
42
|
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
throw new Error('Could not find source strings')
|
|
43
|
+
for (const resource of Object.values(validationResults.allStrings)) {
|
|
44
|
+
for (const [locale, strings] of Object.entries(resource)) {
|
|
45
|
+
const file = JSON.stringify(strings, null, 4)
|
|
46
|
+
fs.writeFileSync(`${OUTPUT_DIR}/${locale}.json`, file)
|
|
56
47
|
}
|
|
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
48
|
}
|
|
73
49
|
|
|
74
|
-
|
|
50
|
+
if (validationResults.messages.length > 0) {
|
|
51
|
+
console.error(validationResults.messages.join('\n\n'))
|
|
52
|
+
}
|
package/scripts/tx-pull-www.mts
CHANGED
|
@@ -9,10 +9,7 @@
|
|
|
9
9
|
import fs from 'fs/promises'
|
|
10
10
|
import { mkdirp } from 'mkdirp'
|
|
11
11
|
import path from 'path'
|
|
12
|
-
import
|
|
13
|
-
import { poolMap } from './lib/concurrent.mts'
|
|
14
|
-
import { ProgressLogger } from './lib/progress-logger.mjs'
|
|
15
|
-
import { txPull, txResources } from './lib/transifex.mts'
|
|
12
|
+
import { pullAndValidateProject } from './lib/pull-and-validate.mts'
|
|
16
13
|
|
|
17
14
|
const args = process.argv.slice(2)
|
|
18
15
|
|
|
@@ -37,68 +34,34 @@ if (!process.env.TX_TOKEN || args.length < 1) {
|
|
|
37
34
|
const PROJECT = 'scratch-website'
|
|
38
35
|
const OUTPUT_DIR = path.resolve(args[0])
|
|
39
36
|
// const MODE = {mode: 'reviewed'}; // default is everything for www
|
|
40
|
-
const CONCURRENCY_LIMIT = 36
|
|
41
37
|
|
|
42
|
-
const lang = args.length === 2 ? args[1] :
|
|
38
|
+
const lang = args.length === 2 ? args[1] : undefined
|
|
39
|
+
const validationResults = await pullAndValidateProject({
|
|
40
|
+
project: PROJECT,
|
|
41
|
+
selectedLocales: lang,
|
|
42
|
+
})
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
const locale
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
for (const [resourceName, resource] of Object.entries(validationResults.allStrings)) {
|
|
45
|
+
for (const [locale, translations] of Object.entries(resource)) {
|
|
46
|
+
const txOutdir = `${OUTPUT_DIR}/${PROJECT}.${resourceName}`
|
|
47
|
+
const fileName = `${txOutdir}/${locale}.json`
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
resource,
|
|
60
|
-
locale,
|
|
61
|
-
fileName,
|
|
62
|
-
}
|
|
63
|
-
} catch (e) {
|
|
64
|
-
;(e as Error).cause = {
|
|
65
|
-
resource,
|
|
66
|
-
locale,
|
|
67
|
-
translations,
|
|
68
|
-
txOutdir,
|
|
69
|
-
fileName,
|
|
70
|
-
}
|
|
71
|
-
throw e
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const expandResourceFiles = (resources: string[]) => {
|
|
76
|
-
const items = []
|
|
77
|
-
for (const resource of resources) {
|
|
78
|
-
if (lang) {
|
|
79
|
-
items.push({ resource: resource, locale: lang })
|
|
80
|
-
} else {
|
|
81
|
-
for (const locale of Object.keys(locales)) {
|
|
82
|
-
items.push({ resource: resource, locale: locale })
|
|
49
|
+
try {
|
|
50
|
+
mkdirp.sync(txOutdir)
|
|
51
|
+
await fs.writeFile(fileName, JSON.stringify(translations, null, 4))
|
|
52
|
+
} catch (e) {
|
|
53
|
+
;(e as Error).cause = {
|
|
54
|
+
resourceName,
|
|
55
|
+
locale,
|
|
56
|
+
translations,
|
|
57
|
+
txOutdir,
|
|
58
|
+
fileName,
|
|
83
59
|
}
|
|
60
|
+
throw e
|
|
84
61
|
}
|
|
85
62
|
}
|
|
86
|
-
return items
|
|
87
63
|
}
|
|
88
64
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const allFiles = expandResourceFiles(resources)
|
|
92
|
-
|
|
93
|
-
const progress = new ProgressLogger(allFiles.length)
|
|
94
|
-
|
|
95
|
-
await poolMap(allFiles, CONCURRENCY_LIMIT, async item => {
|
|
96
|
-
try {
|
|
97
|
-
await getLocaleData(item)
|
|
98
|
-
} finally {
|
|
99
|
-
progress.increment()
|
|
100
|
-
}
|
|
101
|
-
})
|
|
65
|
+
if (validationResults.messages.length > 0) {
|
|
66
|
+
console.error(validationResults.messages.join('\n\n'))
|
|
102
67
|
}
|
|
103
|
-
|
|
104
|
-
await pullTranslations()
|
|
@@ -7,7 +7,7 @@ import async from 'async'
|
|
|
7
7
|
import fs from 'fs'
|
|
8
8
|
import path from 'path'
|
|
9
9
|
import locales from '../src/supported-locales.mjs'
|
|
10
|
-
import {
|
|
10
|
+
import { filterInvalidTranslations, TransifexEditorStrings } from './lib/validate.mts'
|
|
11
11
|
|
|
12
12
|
const args = process.argv.slice(2)
|
|
13
13
|
const usage = `
|
|
@@ -31,11 +31,10 @@ const validate = (locale: string, callback: async.ErrorCallback) => {
|
|
|
31
31
|
if (err) callback(err)
|
|
32
32
|
// let this throw an error if invalid json
|
|
33
33
|
const strings = JSON.parse(data) as TransifexEditorStrings
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const messages = filterInvalidTranslations(locale, strings, source)
|
|
35
|
+
if (messages.length > 0) {
|
|
36
|
+
callback(new Error(`Locale ${locale} has validation errors:\n${messages.join('\n')}`))
|
|
37
37
|
}
|
|
38
|
-
validateTranslations(translations, source)
|
|
39
38
|
})
|
|
40
39
|
}
|
|
41
40
|
|
package/scripts/validate-www.mts
CHANGED
|
@@ -9,7 +9,7 @@ import glob from 'glob'
|
|
|
9
9
|
import path from 'path'
|
|
10
10
|
import locales from '../src/supported-locales.mjs'
|
|
11
11
|
import { TransifexStringsKeyValueJson, TransifexStrings } from './lib/transifex-formats.mts'
|
|
12
|
-
import {
|
|
12
|
+
import { filterInvalidTranslations } from './lib/validate.mts'
|
|
13
13
|
|
|
14
14
|
const args = process.argv.slice(2)
|
|
15
15
|
const usage = `
|
|
@@ -37,11 +37,10 @@ const validate = (localeData: LocaleData, callback: async.ErrorCallback) => {
|
|
|
37
37
|
if (err) callback(err)
|
|
38
38
|
// let this throw an error if invalid json
|
|
39
39
|
const strings = JSON.parse(data) as TransifexStringsKeyValueJson
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const messages = filterInvalidTranslations(localeData.locale, strings, localeData.sourceData)
|
|
41
|
+
if (messages.length > 0) {
|
|
42
|
+
callback(new Error(`Locale ${localeData.locale} has validation errors:\n${messages.join('\n')}`))
|
|
43
43
|
}
|
|
44
|
-
validateTranslations(translations, localeData.sourceData)
|
|
45
44
|
})
|
|
46
45
|
}
|
|
47
46
|
|