scratch-l10n 6.1.133 → 6.1.135
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/package.json +2 -2
- package/scripts/lib/errors.mts +29 -8
- package/scripts/lib/help-utils.mts +149 -24
- package/scripts/lib/transifex.mts +11 -2
- package/scripts/tx-pull-help-articles.mts +7 -1
- package/scripts/tx-pull-locale-articles.mts +10 -2
- package/www/scratch-website.cookies-l10njson/ab.json +1 -0
- package/www/scratch-website.cookies-l10njson/af.json +1 -0
- package/www/scratch-website.cookies-l10njson/am.json +1 -0
- package/www/scratch-website.cookies-l10njson/an.json +1 -0
- package/www/scratch-website.cookies-l10njson/ar.json +1 -0
- package/www/scratch-website.cookies-l10njson/ast.json +1 -0
- package/www/scratch-website.cookies-l10njson/az.json +1 -0
- package/www/scratch-website.cookies-l10njson/be.json +1 -0
- package/www/scratch-website.cookies-l10njson/bg.json +1 -0
- package/www/scratch-website.cookies-l10njson/bn.json +1 -0
- package/www/scratch-website.cookies-l10njson/ca.json +1 -0
- package/www/scratch-website.cookies-l10njson/ckb.json +1 -0
- package/www/scratch-website.cookies-l10njson/cs.json +1 -0
- package/www/scratch-website.cookies-l10njson/cy.json +1 -0
- package/www/scratch-website.cookies-l10njson/da.json +1 -0
- package/www/scratch-website.cookies-l10njson/de.json +1 -0
- package/www/scratch-website.cookies-l10njson/el.json +1 -0
- package/www/scratch-website.cookies-l10njson/en.json +1 -0
- package/www/scratch-website.cookies-l10njson/eo.json +1 -0
- package/www/scratch-website.cookies-l10njson/es-419.json +1 -0
- package/www/scratch-website.cookies-l10njson/es.json +1 -0
- package/www/scratch-website.cookies-l10njson/et.json +1 -0
- package/www/scratch-website.cookies-l10njson/eu.json +1 -0
- package/www/scratch-website.cookies-l10njson/fa.json +1 -0
- package/www/scratch-website.cookies-l10njson/fi.json +1 -0
- package/www/scratch-website.cookies-l10njson/fil.json +1 -0
- package/www/scratch-website.cookies-l10njson/fr.json +1 -0
- package/www/scratch-website.cookies-l10njson/fy.json +1 -0
- package/www/scratch-website.cookies-l10njson/ga.json +1 -0
- package/www/scratch-website.cookies-l10njson/gd.json +1 -0
- package/www/scratch-website.cookies-l10njson/gl.json +1 -0
- package/www/scratch-website.cookies-l10njson/ha.json +1 -0
- package/www/scratch-website.cookies-l10njson/he.json +1 -0
- package/www/scratch-website.cookies-l10njson/hi.json +1 -0
- package/www/scratch-website.cookies-l10njson/hr.json +1 -0
- package/www/scratch-website.cookies-l10njson/ht.json +1 -0
- package/www/scratch-website.cookies-l10njson/hu.json +1 -0
- package/www/scratch-website.cookies-l10njson/hy.json +1 -0
- package/www/scratch-website.cookies-l10njson/id.json +1 -0
- package/www/scratch-website.cookies-l10njson/is.json +1 -0
- package/www/scratch-website.cookies-l10njson/it.json +1 -0
- package/www/scratch-website.cookies-l10njson/ja-Hira.json +1 -0
- package/www/scratch-website.cookies-l10njson/ja.json +1 -0
- package/www/scratch-website.cookies-l10njson/ka.json +1 -0
- package/www/scratch-website.cookies-l10njson/kk.json +1 -0
- package/www/scratch-website.cookies-l10njson/km.json +1 -0
- package/www/scratch-website.cookies-l10njson/ko.json +1 -0
- package/www/scratch-website.cookies-l10njson/ku.json +1 -0
- package/www/scratch-website.cookies-l10njson/lt.json +1 -0
- package/www/scratch-website.cookies-l10njson/lv.json +1 -0
- package/www/scratch-website.cookies-l10njson/mi.json +1 -0
- package/www/scratch-website.cookies-l10njson/mn.json +1 -0
- package/www/scratch-website.cookies-l10njson/nb.json +1 -0
- package/www/scratch-website.cookies-l10njson/nl.json +1 -0
- package/www/scratch-website.cookies-l10njson/nn.json +1 -0
- package/www/scratch-website.cookies-l10njson/nso.json +1 -0
- package/www/scratch-website.cookies-l10njson/oc.json +1 -0
- package/www/scratch-website.cookies-l10njson/or.json +1 -0
- package/www/scratch-website.cookies-l10njson/pl.json +1 -0
- package/www/scratch-website.cookies-l10njson/pt-br.json +1 -0
- package/www/scratch-website.cookies-l10njson/pt.json +1 -0
- package/www/scratch-website.cookies-l10njson/qu.json +1 -0
- package/www/scratch-website.cookies-l10njson/rap.json +1 -0
- package/www/scratch-website.cookies-l10njson/ro.json +1 -0
- package/www/scratch-website.cookies-l10njson/ru.json +1 -0
- package/www/scratch-website.cookies-l10njson/sk.json +1 -0
- package/www/scratch-website.cookies-l10njson/sl.json +1 -0
- package/www/scratch-website.cookies-l10njson/sr.json +1 -0
- package/www/scratch-website.cookies-l10njson/sv.json +1 -0
- package/www/scratch-website.cookies-l10njson/sw.json +1 -0
- package/www/scratch-website.cookies-l10njson/th.json +1 -0
- package/www/scratch-website.cookies-l10njson/tn.json +1 -0
- package/www/scratch-website.cookies-l10njson/tr.json +1 -0
- package/www/scratch-website.cookies-l10njson/uk.json +1 -0
- package/www/scratch-website.cookies-l10njson/uz.json +1 -0
- package/www/scratch-website.cookies-l10njson/vi.json +1 -0
- package/www/scratch-website.cookies-l10njson/xh.json +1 -0
- package/www/scratch-website.cookies-l10njson/zh-cn.json +1 -0
- package/www/scratch-website.cookies-l10njson/zh-tw.json +1 -0
- package/www/scratch-website.cookies-l10njson/zu.json +1 -0
- package/www/scratch-website.splash-l10njson/ab.json +1 -1
- package/www/scratch-website.splash-l10njson/af.json +1 -1
- package/www/scratch-website.splash-l10njson/am.json +1 -1
- package/www/scratch-website.splash-l10njson/an.json +1 -1
- package/www/scratch-website.splash-l10njson/ar.json +1 -1
- package/www/scratch-website.splash-l10njson/ast.json +1 -1
- package/www/scratch-website.splash-l10njson/az.json +1 -1
- package/www/scratch-website.splash-l10njson/be.json +1 -1
- package/www/scratch-website.splash-l10njson/bg.json +1 -1
- package/www/scratch-website.splash-l10njson/bn.json +1 -1
- package/www/scratch-website.splash-l10njson/ca.json +1 -1
- package/www/scratch-website.splash-l10njson/ckb.json +1 -1
- package/www/scratch-website.splash-l10njson/cs.json +1 -1
- package/www/scratch-website.splash-l10njson/cy.json +1 -1
- package/www/scratch-website.splash-l10njson/da.json +1 -1
- package/www/scratch-website.splash-l10njson/de.json +1 -1
- package/www/scratch-website.splash-l10njson/el.json +1 -1
- package/www/scratch-website.splash-l10njson/en.json +1 -1
- package/www/scratch-website.splash-l10njson/eo.json +1 -1
- package/www/scratch-website.splash-l10njson/es-419.json +1 -1
- package/www/scratch-website.splash-l10njson/es.json +1 -1
- package/www/scratch-website.splash-l10njson/et.json +1 -1
- package/www/scratch-website.splash-l10njson/eu.json +1 -1
- package/www/scratch-website.splash-l10njson/fa.json +1 -1
- package/www/scratch-website.splash-l10njson/fi.json +1 -1
- package/www/scratch-website.splash-l10njson/fil.json +1 -1
- package/www/scratch-website.splash-l10njson/fr.json +1 -1
- package/www/scratch-website.splash-l10njson/fy.json +1 -1
- package/www/scratch-website.splash-l10njson/ga.json +1 -1
- package/www/scratch-website.splash-l10njson/gd.json +1 -1
- package/www/scratch-website.splash-l10njson/gl.json +1 -1
- package/www/scratch-website.splash-l10njson/ha.json +1 -1
- package/www/scratch-website.splash-l10njson/he.json +1 -1
- package/www/scratch-website.splash-l10njson/hi.json +1 -1
- package/www/scratch-website.splash-l10njson/hr.json +1 -1
- package/www/scratch-website.splash-l10njson/ht.json +1 -1
- package/www/scratch-website.splash-l10njson/hu.json +1 -1
- package/www/scratch-website.splash-l10njson/hy.json +1 -1
- package/www/scratch-website.splash-l10njson/id.json +1 -1
- package/www/scratch-website.splash-l10njson/is.json +1 -1
- package/www/scratch-website.splash-l10njson/it.json +1 -1
- package/www/scratch-website.splash-l10njson/ja-Hira.json +1 -1
- package/www/scratch-website.splash-l10njson/ja.json +1 -1
- package/www/scratch-website.splash-l10njson/ka.json +1 -1
- package/www/scratch-website.splash-l10njson/kk.json +1 -1
- package/www/scratch-website.splash-l10njson/km.json +1 -1
- package/www/scratch-website.splash-l10njson/ko.json +1 -1
- package/www/scratch-website.splash-l10njson/ku.json +1 -1
- package/www/scratch-website.splash-l10njson/lt.json +1 -1
- package/www/scratch-website.splash-l10njson/lv.json +1 -1
- package/www/scratch-website.splash-l10njson/mi.json +1 -1
- package/www/scratch-website.splash-l10njson/mn.json +1 -1
- package/www/scratch-website.splash-l10njson/nb.json +1 -1
- package/www/scratch-website.splash-l10njson/nl.json +1 -1
- package/www/scratch-website.splash-l10njson/nn.json +1 -1
- package/www/scratch-website.splash-l10njson/nso.json +1 -1
- package/www/scratch-website.splash-l10njson/oc.json +1 -1
- package/www/scratch-website.splash-l10njson/or.json +1 -1
- package/www/scratch-website.splash-l10njson/pl.json +1 -1
- package/www/scratch-website.splash-l10njson/pt-br.json +1 -1
- package/www/scratch-website.splash-l10njson/pt.json +1 -1
- package/www/scratch-website.splash-l10njson/qu.json +1 -1
- package/www/scratch-website.splash-l10njson/rap.json +1 -1
- package/www/scratch-website.splash-l10njson/ro.json +1 -1
- package/www/scratch-website.splash-l10njson/ru.json +1 -1
- package/www/scratch-website.splash-l10njson/sk.json +1 -1
- package/www/scratch-website.splash-l10njson/sl.json +1 -1
- package/www/scratch-website.splash-l10njson/sr.json +1 -1
- package/www/scratch-website.splash-l10njson/sv.json +1 -1
- package/www/scratch-website.splash-l10njson/sw.json +1 -1
- package/www/scratch-website.splash-l10njson/th.json +1 -1
- package/www/scratch-website.splash-l10njson/tn.json +1 -1
- package/www/scratch-website.splash-l10njson/tr.json +1 -1
- package/www/scratch-website.splash-l10njson/uk.json +1 -1
- package/www/scratch-website.splash-l10njson/uz.json +1 -1
- package/www/scratch-website.splash-l10njson/vi.json +1 -1
- package/www/scratch-website.splash-l10njson/xh.json +1 -1
- package/www/scratch-website.splash-l10njson/zh-cn.json +1 -1
- package/www/scratch-website.splash-l10njson/zh-tw.json +1 -1
- package/www/scratch-website.splash-l10njson/zu.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scratch-l10n",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.135",
|
|
4
4
|
"description": "Localization for the Scratch 3.0 components",
|
|
5
5
|
"main": "./dist/l10n.js",
|
|
6
6
|
"browser": "./src/index.mjs",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"jshint": "2.13.6",
|
|
82
82
|
"json": "9.0.6",
|
|
83
83
|
"jsonlint": "1.6.3",
|
|
84
|
-
"prettier": "3.9.
|
|
84
|
+
"prettier": "3.9.10",
|
|
85
85
|
"rimraf": "2.7.1",
|
|
86
86
|
"scratch-semantic-release-config": "4.0.1",
|
|
87
87
|
"semantic-release": "25.0.9",
|
package/scripts/lib/errors.mts
CHANGED
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export const messageOf = (err: unknown): string => (err instanceof Error ? err.message : String(err))
|
|
13
13
|
|
|
14
|
+
/** Where a JSON parse error falls, as named by a shape-aware locator for {@link describeJsonParseError}. */
|
|
15
|
+
export interface JsonEntryLocation {
|
|
16
|
+
/** Human-readable name of the entry, for the message (for example `key "123.description"`). */
|
|
17
|
+
label: string
|
|
18
|
+
/** The string key, when the locator could pin one, for linking straight to it. */
|
|
19
|
+
key?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* Describe where a `JSON.parse` failed, for a diagnostic message. V8 reports the byte offset (and, on
|
|
16
24
|
* newer Node, the line and column) in the error message; combined with the original text this pins the
|
|
@@ -19,9 +27,18 @@ export const messageOf = (err: unknown): string => (err instanceof Error ? err.m
|
|
|
19
27
|
* be read, so callers fall back to the plain message.
|
|
20
28
|
* @param error - the caught value (expected to be a `SyntaxError` from `JSON.parse`)
|
|
21
29
|
* @param text - the exact string that was parsed
|
|
22
|
-
* @
|
|
30
|
+
* @param locate - optional, for a caller that knows the file's shape: given the text before the error
|
|
31
|
+
* position, name the entry it falls in (a `label` for the message, and the string `key` when known).
|
|
32
|
+
* Used in place of the flat-file "near key" guess, which nested files with escaped quotes in their
|
|
33
|
+
* values easily fool.
|
|
34
|
+
* @returns a one-line `message` (location, entry or nearby key, snippet) plus the located string `key`
|
|
35
|
+
* if any, or undefined
|
|
23
36
|
*/
|
|
24
|
-
export const describeJsonParseError = (
|
|
37
|
+
export const describeJsonParseError = (
|
|
38
|
+
error: unknown,
|
|
39
|
+
text: string,
|
|
40
|
+
locate?: (before: string) => JsonEntryLocation | undefined,
|
|
41
|
+
): { message: string; key?: string } | undefined => {
|
|
25
42
|
if (!(error instanceof SyntaxError) || typeof text !== 'string') {
|
|
26
43
|
return undefined
|
|
27
44
|
}
|
|
@@ -35,6 +52,15 @@ export const describeJsonParseError = (error: unknown, text: string): string | u
|
|
|
35
52
|
}
|
|
36
53
|
const lineColMatch = /\(line (\d+) column (\d+)\)/.exec(error.message)
|
|
37
54
|
const where = lineColMatch ? `line ${lineColMatch[1]}, column ${lineColMatch[2]}` : `position ${pos}`
|
|
55
|
+
// A short, whitespace-collapsed window around the failure so the operator can eyeball the bad text.
|
|
56
|
+
const snippet = text
|
|
57
|
+
.slice(Math.max(0, pos - 30), pos + 30)
|
|
58
|
+
.replace(/\s+/g, ' ')
|
|
59
|
+
.trim()
|
|
60
|
+
const entry = locate?.(text.slice(0, pos))
|
|
61
|
+
if (entry) {
|
|
62
|
+
return { message: `malformed JSON in ${entry.label} (${where}): …${snippet}…`, key: entry.key }
|
|
63
|
+
}
|
|
38
64
|
// Recover the enclosing key of a flat KEYVALUEJSON object: the value that failed to parse is
|
|
39
65
|
// preceded by `"<key>":`, so scan back from the error position to that key. Heuristic (a colon or
|
|
40
66
|
// quote inside an earlier string can fool it), so it is reported as "near key", not an exact locator.
|
|
@@ -50,11 +76,6 @@ export const describeJsonParseError = (error: unknown, text: string): string | u
|
|
|
50
76
|
}
|
|
51
77
|
}
|
|
52
78
|
}
|
|
53
|
-
// A short, whitespace-collapsed window around the failure so the operator can eyeball the bad text.
|
|
54
|
-
const snippet = text
|
|
55
|
-
.slice(Math.max(0, pos - 30), pos + 30)
|
|
56
|
-
.replace(/\s+/g, ' ')
|
|
57
|
-
.trim()
|
|
58
79
|
const keyPart = nearKey ? ` near key "${nearKey}"` : ''
|
|
59
|
-
return `malformed JSON at ${where}${keyPart}: …${snippet}…`
|
|
80
|
+
return { message: `malformed JSON at ${where}${keyPart}: …${snippet}…` }
|
|
60
81
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { promises as fsPromises } from 'fs'
|
|
6
6
|
import { mkdirp } from 'mkdirp'
|
|
7
7
|
import { emitAnnotation } from './annotations.mts'
|
|
8
|
-
import { describeJsonParseError, messageOf } from './errors.mts'
|
|
8
|
+
import { describeJsonParseError, JsonEntryLocation, messageOf } from './errors.mts'
|
|
9
9
|
import FreshdeskApi, {
|
|
10
10
|
FreshdeskArticleCreate,
|
|
11
11
|
FreshdeskArticleStatus,
|
|
@@ -28,6 +28,8 @@ import { emitWarning } from './warnings.mts'
|
|
|
28
28
|
|
|
29
29
|
const FD = new FreshdeskApi('https://mitscratch.freshdesk.com', process.env.FRESHDESK_TOKEN ?? '')
|
|
30
30
|
const TX_PROJECT = 'scratch-help'
|
|
31
|
+
/** The Transifex resources that hold category and folder names; every other JSON resource holds articles. */
|
|
32
|
+
const NAMES_RESOURCES = ['categoryNames_json', 'folderNames_json']
|
|
31
33
|
|
|
32
34
|
// Collect per-item failures instead of aborting on the first one, so one bad translation or a single
|
|
33
35
|
// item the token cannot write still lets every other item sync. Freshdesk rate limiting is handled
|
|
@@ -35,20 +37,31 @@ const TX_PROJECT = 'scratch-help'
|
|
|
35
37
|
// here is a genuine problem worth a human's attention. `reportFailures` prints a consolidated summary
|
|
36
38
|
// and fails the run at the end, so real errors are surfaced rather than lost in the log.
|
|
37
39
|
const failures: { context: string; message: string; link?: string }[] = []
|
|
38
|
-
const recordFailure = (
|
|
40
|
+
const recordFailure = (
|
|
41
|
+
context: string,
|
|
42
|
+
error: unknown,
|
|
43
|
+
link?: string,
|
|
44
|
+
locate?: (before: string) => JsonEntryLocation | undefined,
|
|
45
|
+
): void => {
|
|
39
46
|
const message = messageOf(error)
|
|
40
47
|
// A txPull error carries the failing (resource, locale) — and, for a parse failure, the raw text —
|
|
41
48
|
// on its `cause`, so we can build a Transifex link and describe malformed JSON without the call site
|
|
42
|
-
// passing anything.
|
|
49
|
+
// passing anything. When the parse error can be pinned to one string, the link filters the editor to
|
|
50
|
+
// that string's key. An explicit link still wins: some callers know the fix lives on the source (`en`)
|
|
43
51
|
// rather than the translation locale the pull used.
|
|
44
52
|
const cause = txPullErrorCause(error)
|
|
53
|
+
const detail = cause?.buffer != null ? describeJsonParseError(error, cause.buffer, locate) : undefined
|
|
45
54
|
const resolvedLink =
|
|
46
55
|
link ??
|
|
47
56
|
(cause
|
|
48
|
-
? transifexEditorLink({
|
|
57
|
+
? transifexEditorLink({
|
|
58
|
+
project: cause.project,
|
|
59
|
+
resource: cause.resource,
|
|
60
|
+
lang: cause.locale,
|
|
61
|
+
query: detail?.key && `key:${detail.key}`,
|
|
62
|
+
})
|
|
49
63
|
: undefined)
|
|
50
|
-
const
|
|
51
|
-
const fullMessage = detail ? `${message} — ${detail}` : message
|
|
64
|
+
const fullMessage = detail ? `${message} — ${detail.message}` : message
|
|
52
65
|
// Genuine failures go to stderr (like the consolidated summary), so they stand apart from normal output.
|
|
53
66
|
process.stderr.write(`Error: ${context}: ${fullMessage}${resolvedLink ? ` (${resolvedLink})` : ''}\n`)
|
|
54
67
|
failures.push({ context, message: fullMessage, link: resolvedLink })
|
|
@@ -345,13 +358,16 @@ export const getInputs = async () => {
|
|
|
345
358
|
const resourcesPromise = txResourcesObjects(TX_PROJECT)
|
|
346
359
|
const languagesPromise = txAvailableLanguages(TX_PROJECT)
|
|
347
360
|
|
|
348
|
-
//
|
|
361
|
+
// Category and folder names live in two fixed KEYVALUEJSON resources; every other JSON resource holds a
|
|
362
|
+
// folder's articles. Classify by name rather than type: some article resources were created as
|
|
363
|
+
// KEYVALUEJSON by mistake, and they download in the same nested shape as the STRUCTURED_JSON ones.
|
|
364
|
+
const isNames = (resource: TransifexResourceObject) => NAMES_RESOURCES.includes(resource.attributes.slug)
|
|
365
|
+
const isJson = (resource: TransifexResourceObject) =>
|
|
366
|
+
resource.attributes.i18n_type === 'STRUCTURED_JSON' || resource.attributes.i18n_type === 'KEYVALUEJSON'
|
|
349
367
|
const foldersPromise = resourcesPromise.then(resources =>
|
|
350
|
-
resources.filter(resource => resource
|
|
351
|
-
)
|
|
352
|
-
const namesPromise = resourcesPromise.then(resources =>
|
|
353
|
-
resources.filter(resource => resource.attributes.i18n_type === 'KEYVALUEJSON'),
|
|
368
|
+
resources.filter(resource => isJson(resource) && !isNames(resource)),
|
|
354
369
|
)
|
|
370
|
+
const namesPromise = resourcesPromise.then(resources => resources.filter(isNames))
|
|
355
371
|
// ignore the yaml type because it's not possible to update via API
|
|
356
372
|
|
|
357
373
|
const [languages, folders, names] = await Promise.all([languagesPromise, foldersPromise, namesPromise])
|
|
@@ -383,6 +399,46 @@ export const getValidFreshdeskIds = async () => {
|
|
|
383
399
|
return { validCategoryIds, validFolderIds }
|
|
384
400
|
}
|
|
385
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Freshdesk folders whose article translations are deliberately not written to Freshdesk, even when they're
|
|
404
|
+
* translated in Transifex. Their articles are still pushed to Transifex as source.
|
|
405
|
+
*
|
|
406
|
+
* - 4000040867 "Legal Documents" (Terms of Service, Privacy Policy, and the like): we don't publish
|
|
407
|
+
* translations of legal documents for now. This decision may be revisited.
|
|
408
|
+
*/
|
|
409
|
+
const UNSYNCED_FOLDER_IDS = new Set([4000040867])
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Keep only the article resources whose translations should be written to Freshdesk: the folder must still
|
|
413
|
+
* exist, and must not be in {@link UNSYNCED_FOLDER_IDS}. The push only updates resources for current
|
|
414
|
+
* folders, so a resource left behind by a deleted folder keeps its old English source forever. Its articles
|
|
415
|
+
* may have moved to another folder, so pulling it would overwrite their live translations with translations
|
|
416
|
+
* of outdated content. Each such resource is reported once so someone can remove it.
|
|
417
|
+
* @param folders - article resources from {@link getInputs}, named `<folder name>_<folder id>_json`
|
|
418
|
+
* @param validFolderIds - set of Freshdesk folder IDs that currently exist
|
|
419
|
+
* @returns the resources whose translations should sync to Freshdesk
|
|
420
|
+
*/
|
|
421
|
+
export const foldersToSync = (folders: TransifexResourceObject[], validFolderIds: Set<number>) =>
|
|
422
|
+
folders.filter(resource => {
|
|
423
|
+
const slug = resource.attributes.slug
|
|
424
|
+
const match = /_(\d+)_json$/.exec(slug)
|
|
425
|
+
if (match && UNSYNCED_FOLDER_IDS.has(Number(match[1]))) {
|
|
426
|
+
// Intentional, so log it rather than raising a warning on every run.
|
|
427
|
+
console.log(`Not syncing Transifex resource "${slug}": its translations are excluded from Freshdesk.`)
|
|
428
|
+
return false
|
|
429
|
+
}
|
|
430
|
+
if (match && validFolderIds.has(Number(match[1]))) {
|
|
431
|
+
return true
|
|
432
|
+
}
|
|
433
|
+
emitWarning(
|
|
434
|
+
match
|
|
435
|
+
? `Skipping Transifex resource "${slug}": Freshdesk folder ${match[1]} no longer exists. Remove this resource from Transifex.`
|
|
436
|
+
: `Skipping Transifex resource "${slug}": its name doesn't end in a Freshdesk folder ID.`,
|
|
437
|
+
transifexEditorLink({ project: TX_PROJECT, resource: slug, lang: SOURCE_LOCALE }),
|
|
438
|
+
)
|
|
439
|
+
return false
|
|
440
|
+
})
|
|
441
|
+
|
|
386
442
|
/**
|
|
387
443
|
* internal function to serialize saving category and folder name translations to avoid Freshdesk rate limit
|
|
388
444
|
* @param strings - the string data pulled from Transifex
|
|
@@ -414,7 +470,12 @@ const serializeNameSave = async (
|
|
|
414
470
|
emitWarning(
|
|
415
471
|
`Warning: key "${key}" in Transifex resource "${resource.attributes.name}" refers to Freshdesk id ${id} which no longer exists. Remove this key from the Transifex resource.`,
|
|
416
472
|
// The key is removed from the source, so point at the source language, not a translation.
|
|
417
|
-
transifexEditorLink({
|
|
473
|
+
transifexEditorLink({
|
|
474
|
+
project: TX_PROJECT,
|
|
475
|
+
resource: resource.attributes.slug,
|
|
476
|
+
lang: SOURCE_LOCALE,
|
|
477
|
+
query: `key:${key}`,
|
|
478
|
+
}),
|
|
418
479
|
)
|
|
419
480
|
}
|
|
420
481
|
continue
|
|
@@ -441,7 +502,12 @@ const serializeNameSave = async (
|
|
|
441
502
|
recordFailure(
|
|
442
503
|
`${resource.attributes.name} entry "${key}" for ${locale}`,
|
|
443
504
|
error,
|
|
444
|
-
transifexEditorLink({
|
|
505
|
+
transifexEditorLink({
|
|
506
|
+
project: TX_PROJECT,
|
|
507
|
+
resource: resource.attributes.slug,
|
|
508
|
+
lang: locale,
|
|
509
|
+
query: `key:${key}`,
|
|
510
|
+
}),
|
|
445
511
|
)
|
|
446
512
|
allOk = false
|
|
447
513
|
}
|
|
@@ -459,6 +525,53 @@ interface FreshdeskFolderInTransifex {
|
|
|
459
525
|
tags: { string: string }
|
|
460
526
|
}
|
|
461
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Name the article (and field) that a position in a folder resource's raw JSON falls in, for a parse
|
|
530
|
+
* error. Matches only the structural `"<id>":{"title"…` and `"<field>":{"string":` openers, which don't
|
|
531
|
+
* occur inside the HTML values, so escaped or stray quotes in the content can't throw it off.
|
|
532
|
+
* @param before - the raw text up to the parse error position
|
|
533
|
+
* @returns the article's string key (e.g. `4000123456.description`) and a label for it, or undefined if no
|
|
534
|
+
* article opener precedes the position
|
|
535
|
+
*/
|
|
536
|
+
const locateArticle = (before: string): JsonEntryLocation | undefined => {
|
|
537
|
+
const article = [...before.matchAll(/"(\d+)"\s*:\s*\{\s*"(?:title|description|tags)"/g)].pop()
|
|
538
|
+
if (article?.index == null) {
|
|
539
|
+
return undefined
|
|
540
|
+
}
|
|
541
|
+
const field = [
|
|
542
|
+
...before.slice(article.index).matchAll(/[{,]\s*"(title|description|tags)"\s*:\s*\{\s*"string"\s*:/g),
|
|
543
|
+
].pop()
|
|
544
|
+
if (!field) {
|
|
545
|
+
return { label: `article ${article[1]}` }
|
|
546
|
+
}
|
|
547
|
+
// The Transifex string key is `<id>.<field>`, which the editor shows and can filter to.
|
|
548
|
+
const key = `${article[1]}.${field[1]}`
|
|
549
|
+
return { label: `key "${key}"`, key }
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Split a translated tags string into Freshdesk tags. The push joins each article's tags with ASCII commas,
|
|
554
|
+
* but translators often use their own language's comma, so also split on the Arabic (،), ideographic (、),
|
|
555
|
+
* and full-width (,) commas. Tags are trimmed, empty ones dropped, and duplicates that differ only in case
|
|
556
|
+
* removed, since Freshdesk rejects those.
|
|
557
|
+
* @param tags - the translated tags string
|
|
558
|
+
* @returns the tags to send to Freshdesk, in their original order
|
|
559
|
+
*/
|
|
560
|
+
export const splitTags = (tags: string): string[] => {
|
|
561
|
+
const seen = new Set<string>()
|
|
562
|
+
return tags
|
|
563
|
+
.split(/[,،、,]/)
|
|
564
|
+
.map(tag => tag.trim())
|
|
565
|
+
.filter(tag => {
|
|
566
|
+
const folded = tag.toLowerCase()
|
|
567
|
+
if (!tag || seen.has(folded)) {
|
|
568
|
+
return false
|
|
569
|
+
}
|
|
570
|
+
seen.add(folded)
|
|
571
|
+
return true
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
462
575
|
/**
|
|
463
576
|
* Internal function serialize Freshdesk requests to avoid getting rate limited
|
|
464
577
|
* @param json object with keys corresponding to article ids
|
|
@@ -482,7 +595,7 @@ const serializeFolderSave = async (
|
|
|
482
595
|
status: FreshdeskArticleStatus.published,
|
|
483
596
|
}
|
|
484
597
|
if (Object.prototype.hasOwnProperty.call(value, 'tags')) {
|
|
485
|
-
const tags = value.tags.string
|
|
598
|
+
const tags = splitTags(value.tags.string)
|
|
486
599
|
// Freshdesk rejects tags longer than 32 characters, so drop them rather than fail the write.
|
|
487
600
|
const validTags = tags.filter(tag => tag.length < 33)
|
|
488
601
|
const droppedTags = tags.filter(tag => tag.length >= 33)
|
|
@@ -492,7 +605,12 @@ const serializeFolderSave = async (
|
|
|
492
605
|
`(${locale}); shorten them in Transifex: ${droppedTags
|
|
493
606
|
.map(tag => `"${tag}" (${tag.length} chars)`)
|
|
494
607
|
.join(', ')}.`,
|
|
495
|
-
transifexEditorLink({
|
|
608
|
+
transifexEditorLink({
|
|
609
|
+
project: TX_PROJECT,
|
|
610
|
+
resource: resourceSlug,
|
|
611
|
+
lang: locale,
|
|
612
|
+
query: `key:${id}.tags`,
|
|
613
|
+
}),
|
|
496
614
|
)
|
|
497
615
|
}
|
|
498
616
|
body.tags = validTags
|
|
@@ -505,12 +623,19 @@ const serializeFolderSave = async (
|
|
|
505
623
|
}
|
|
506
624
|
} catch (error) {
|
|
507
625
|
// Record the failure so the job still reports a non-zero exit at the end, then move on to the
|
|
508
|
-
// next article.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
626
|
+
// next article. Freshdesk won't accept an article translation until its folder's name is
|
|
627
|
+
// translated too, so for that error point at the folder's entry in the names resource (keyed by
|
|
628
|
+
// this resource's slug without `_json`) instead of the article. Otherwise filter the editor to
|
|
629
|
+
// the article's strings (a `key:<id>` search matches its title, description, and tags).
|
|
630
|
+
const link = /"field"\s*:\s*"folder_name"/.test(messageOf(error))
|
|
631
|
+
? transifexEditorLink({
|
|
632
|
+
project: TX_PROJECT,
|
|
633
|
+
resource: 'folderNames_json',
|
|
634
|
+
lang: locale,
|
|
635
|
+
query: `key:${resourceSlug.replace(/_json$/, '')}`,
|
|
636
|
+
})
|
|
637
|
+
: transifexEditorLink({ project: TX_PROJECT, resource: resourceSlug, lang: locale, query: `key:${idString}` })
|
|
638
|
+
recordFailure(`article ${idString} for ${locale}`, error, link)
|
|
514
639
|
allOk = false
|
|
515
640
|
}
|
|
516
641
|
}
|
|
@@ -533,7 +658,7 @@ export const localizeFolder = async (folderAttributes: TransifexResourceObject,
|
|
|
533
658
|
)
|
|
534
659
|
return await serializeFolderSave(data, locale, folderAttributes.attributes.slug)
|
|
535
660
|
} catch (e) {
|
|
536
|
-
recordFailure(`${folderAttributes.attributes.slug} for ${locale}`, e)
|
|
661
|
+
recordFailure(`${folderAttributes.attributes.slug} for ${locale}`, e, undefined, locateArticle)
|
|
537
662
|
return false
|
|
538
663
|
}
|
|
539
664
|
}
|
|
@@ -561,8 +686,8 @@ export const debugFolder = async (folderAttributes: TransifexResourceObject, loc
|
|
|
561
686
|
}
|
|
562
687
|
|
|
563
688
|
/**
|
|
564
|
-
* Process
|
|
565
|
-
* Category and Folder names are stored as plain json
|
|
689
|
+
* Process the category and folder names resources from scratch-help on transifex
|
|
690
|
+
* Category and Folder names are stored as plain json (KEYVALUEJSON)
|
|
566
691
|
* @param resource Transifex resource json for either CategoryNames or FolderNames
|
|
567
692
|
* @param locale locale to pull and submit to Freshdesk
|
|
568
693
|
* @param validCategoryIds - set of Freshdesk category IDs that currently exist
|
|
@@ -16,7 +16,8 @@ export const SOURCE_LOCALE = 'en'
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Build a deep link to the Transifex online editor for a resource in one language, matching the URL
|
|
19
|
-
* the web app uses: `app.transifex.com/<org>/<project>/translate/#<lang>/<resource>[/<stringId>]
|
|
19
|
+
* the web app uses: `app.transifex.com/<org>/<project>/translate/#<lang>/<resource>[/<stringId>]`, or
|
|
20
|
+
* `…/<resource>?q=<query>` to open the editor with a search applied.
|
|
20
21
|
* @param params - link target
|
|
21
22
|
* @param params.project - project slug (for example, `scratch-help`)
|
|
22
23
|
* @param params.resource - resource slug (for example, `Accessibility_4000040849_json`)
|
|
@@ -24,6 +25,8 @@ export const SOURCE_LOCALE = 'en'
|
|
|
24
25
|
* translation locale for a bad translation
|
|
25
26
|
* @param params.stringId - optional Transifex numeric string id to focus a single string; omitted, the
|
|
26
27
|
* link lands on the resource (with its first string shown)
|
|
28
|
+
* @param params.query - optional editor search, as typed into its search box (for example,
|
|
29
|
+
* `key:4000156778.description` to filter to one string by its key); takes precedence over `stringId`
|
|
27
30
|
* @returns the editor URL
|
|
28
31
|
*/
|
|
29
32
|
export const transifexEditorLink = (params: {
|
|
@@ -31,9 +34,15 @@ export const transifexEditorLink = (params: {
|
|
|
31
34
|
resource: string
|
|
32
35
|
lang: string
|
|
33
36
|
stringId?: string | number
|
|
37
|
+
query?: string
|
|
34
38
|
}): string => {
|
|
35
|
-
const { project, resource, lang, stringId } = params
|
|
39
|
+
const { project, resource, lang, stringId, query } = params
|
|
36
40
|
const base = `https://app.transifex.com/${ORG_NAME}/${project}/translate/#${lang}/${resource}`
|
|
41
|
+
if (query != null) {
|
|
42
|
+
// No slash before `?q=`. The editor rewrites its address bar to `<resource>/?q=…`, but that form
|
|
43
|
+
// lands on the project overview (query ignored) when opened fresh.
|
|
44
|
+
return `${base}?q=${encodeURIComponent(query)}`
|
|
45
|
+
}
|
|
37
46
|
return stringId == null ? base : `${base}/${stringId}`
|
|
38
47
|
}
|
|
39
48
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import {
|
|
7
7
|
getInputs,
|
|
8
|
+
getValidFreshdeskIds,
|
|
9
|
+
foldersToSync,
|
|
8
10
|
saveItem,
|
|
9
11
|
localizeFolder,
|
|
10
12
|
initChangeDetection,
|
|
@@ -29,7 +31,11 @@ if (!process.env.TX_TOKEN || !process.env.FRESHDESK_TOKEN || args.length > 0) {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
await initChangeDetection()
|
|
32
|
-
const { languages, folders } = await
|
|
34
|
+
const [{ languages, folders: allFolders }, { validFolderIds }] = await Promise.all([
|
|
35
|
+
getInputs(),
|
|
36
|
+
getValidFreshdeskIds(),
|
|
37
|
+
])
|
|
38
|
+
const folders = foldersToSync(allFolders, validFolderIds)
|
|
33
39
|
reportSkippedLocales(languages)
|
|
34
40
|
console.log('Processing articles pulled from Transifex')
|
|
35
41
|
await Promise.all(folders.map(item => saveItem(item, languages, localizeFolder)))
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
* @file
|
|
4
4
|
* Script to pull scratch-help translations from transifex and push to FreshDesk.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getInputs,
|
|
8
|
+
getValidFreshdeskIds,
|
|
9
|
+
foldersToSync,
|
|
10
|
+
saveItem,
|
|
11
|
+
localizeFolder,
|
|
12
|
+
debugFolder,
|
|
13
|
+
} from './lib/help-utils.mts'
|
|
7
14
|
|
|
8
15
|
const args = process.argv.slice(2)
|
|
9
16
|
const usage = `
|
|
@@ -29,6 +36,7 @@ if (locale === '-d') {
|
|
|
29
36
|
}
|
|
30
37
|
const saveFn = debug ? debugFolder : localizeFolder
|
|
31
38
|
|
|
32
|
-
const { folders } = await getInputs()
|
|
39
|
+
const [{ folders: allFolders }, { validFolderIds }] = await Promise.all([getInputs(), getValidFreshdeskIds()])
|
|
40
|
+
const folders = foldersToSync(allFolders, validFolderIds)
|
|
33
41
|
console.log('Processing articles pulled from Transifex')
|
|
34
42
|
await Promise.all(folders.map(item => saveItem(item, [locale], saveFn)))
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "تُستخدم لحفظ تفضيلات البحث الحالية للمستخدم.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "تُستخدم لحفظ معلومات حول اللغة المفضلة للمستخدم.",
|
|
32
32
|
"cookies.functional.scratchTheme": "يُستخدم لحفظ معلومات حول وضع الألوان المفضل للمستخدم.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "يُستخدم لتتبع ما إذا كان المستخدم قد شاهد إشعار تحديث سياسة الخصوصية",
|
|
34
35
|
"cookies.functional.wistia": "يحتوي على طابع زمني لمحتوى الفيديو الخاص بموقع Scratch. يتيح ذلك للمستخدم استئناف المشاهدة دون الحاجة إلى البدء من جديد، إذا غادر المستخدم الفيديو أو موقع Scratch.",
|
|
35
36
|
"cookies.functional.minilogSettings": "غير مصنف",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Выкарыстоўваецца для захавання бягучых параметраў пошуку карыстальніка.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Выкарыстоўваецца для захавання інфармацыі аб мове, якую карыстальнік аддае перавагу.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Выкарыстоўваецца для захавання інфармацыі аб абраным каляровым рэжыме карыстальніка.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Выкарыстоўваецца для адсочвання таго, ці бачыў карыстальнік апавяшчэнне аб абнаўленні палітыкі прыватнасці",
|
|
34
35
|
"cookies.functional.wistia": "Змяшчае пазнаку часу для відэакантэнту сайта Scratch. Гэта дазваляе карыстальніку аднавіць прагляд без неабходнасці пачынаць спачатку, калі карыстальнік пакідае відэа ці вэб-сайт Scratch.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Некласіфікаваны",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "ব্যবহারকারীর বর্তমান সার্চ পছন্দ সংরক্ষণ করতে ব্যবহৃত হয়।",
|
|
31
31
|
"cookies.functional.scratchLanguage": "ব্যবহারকারীর পছন্দের ভাষা সম্পর্কে তথ্য সংরক্ষণ করতে ব্যবহৃত হয়।",
|
|
32
32
|
"cookies.functional.scratchTheme": "ব্যবহারকারীর পছন্দের রঙের মোড সম্পর্কে তথ্য সংরক্ষণ করতে ব্যবহৃত হয়।",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "ব্যবহারকারী একটি প্রাইভেসি নীতি আপডেট নোটিফিকেশন দেখেছে কিনা তা ট্র্যাক করতে ব্যবহৃত হয়",
|
|
34
35
|
"cookies.functional.wistia": "Scratch ওয়েবসাইটের ভিডিও-বিষয়বস্তুর জন্য একটি টাইমস্ট্যাম্প ধারণ করে। এটি ব্যবহারকারীকে ভিডিও বা Scratch ওয়েবসাইট ছেড়ে গেলে আবার প্রথম থেকে শুরু না করে দেখা চালিয়ে যেতে দেয়।",
|
|
35
36
|
"cookies.functional.minilogSettings": "শ্রেণীবিভক্ত করা হয়নি এমন",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Slouží k uložení uživatelových aktuálních vyhledávacích preferencí.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Slouží k ukládání informací o preferovaném jazyce uživatele.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Slouží k uložení informací o uživatelem preferovaném barevném režimu.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Používá se ke sledování, zda se uživateli zobrazilo oznámení o aktualizaci zásad ochrany osobních údajů",
|
|
34
35
|
"cookies.functional.wistia": "Obsahuje časové razítko pro videoobsah webových stránek Scratch. To umožňuje uživateli pokračovat ve sledování, aniž by musel začínat znovu, pokud uživatel opustí video nebo Scratch web.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Neklasifikováno",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Fe'i defnyddir i gadw dewis chwilio cyfredol y defnyddiwr.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Fe'i defnyddir i gadw gwybodaeth am ddewis iaith y defnyddiwr.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Mae'n cael ei ddefnyddio i gadw gwybodaeth am ddewis iaith y defnyddiwr.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Fe'i defnyddir i olrhain a yw'r defnyddiwr wedi gweld hysbysiad diweddaru polisi preifatrwydd",
|
|
34
35
|
"cookies.functional.wistia": "Yn cynnwys stamp amser ar gyfer cynnwys fideo Gwefan Scratch. Mae hyn yn caniatáu i'r defnyddiwr ailddechrau gwylio heb orfod dechrau drosodd, os yw'r defnyddiwr yn gadael y fideo neu Scratch Gwefan.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Annosbarthedig",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Wird verwendet, um die aktuellen Sucheinstellungen des Benutzers zu speichern.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Wird verwendet, um Informationen über die bevorzugte Sprache des Benutzers zu speichern.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Wird zum Speichern von Informationen über den bevorzugten Farbmodus des Benutzers verwendet.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Wird verwendet, um zu verfolgen, ob der Benutzer eine Benachrichtigung über eine Aktualisierung der Datenschutzrichtlinie gesehen hat",
|
|
34
35
|
"cookies.functional.wistia": "Enthält einen Zeitstempel für den Videoinhalt der Scratch-Website. Dadurch kann der Benutzer die Wiedergabe fortsetzen, ohne von vorne beginnen zu müssen, wenn er das Video oder die Scratch-Website verlässt.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Nicht klassifiziert",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"cookies.functional.exploreBy": "Used to save the user’s current search preference.",
|
|
31
31
|
"cookies.functional.scratchLanguage": "Used to save information about the user’s preferred language.",
|
|
32
32
|
"cookies.functional.scratchTheme": "Used to save information about the user’s preferred color mode.",
|
|
33
|
+
"cookies.functional.donateBannerAb": "Used to remember which homepage donate banner to show while we compare banner versions.",
|
|
33
34
|
"cookies.functional.scratchPolicySeen": "Used to track if the user has seen a privacy policy update notification",
|
|
34
35
|
"cookies.functional.wistia": "Contains a timestamp for the Scratch Website’s video-content. This allows the user to resume watching without having to start over, if the user leaves the video or Scratch Website.",
|
|
35
36
|
"cookies.functional.minilogSettings": "Unclassified",
|