scratch-l10n 6.1.103 → 6.1.105
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 +4 -4
- package/scripts/lib/annotations.mts +55 -0
- package/scripts/lib/errors.mts +48 -0
- package/scripts/lib/help-utils.mts +55 -11
- package/scripts/lib/transifex.mts +57 -1
- package/scripts/lib/warnings.mts +10 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scratch-l10n",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.105",
|
|
4
4
|
"description": "Localization for the Scratch 3.0 components",
|
|
5
5
|
"main": "./dist/l10n.js",
|
|
6
6
|
"browser": "./src/index.mjs",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"lodash.defaultsdeep": "4.6.1",
|
|
56
56
|
"mkdirp": "3.0.1",
|
|
57
57
|
"transifex": "1.6.6",
|
|
58
|
-
"tsx": "4.23.
|
|
58
|
+
"tsx": "4.23.8"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@babel/cli": "7.29.7",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"eslint": "9.39.5",
|
|
77
77
|
"eslint-config-scratch": "11.0.53",
|
|
78
78
|
"format-message-cli": "6.2.4",
|
|
79
|
-
"globals": "17.
|
|
79
|
+
"globals": "17.9.0",
|
|
80
80
|
"husky": "8.0.3",
|
|
81
81
|
"jshint": "2.13.6",
|
|
82
82
|
"json": "9.0.6",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"prettier": "3.9.6",
|
|
85
85
|
"rimraf": "2.7.1",
|
|
86
86
|
"scratch-semantic-release-config": "4.0.1",
|
|
87
|
-
"semantic-release": "25.0.
|
|
87
|
+
"semantic-release": "25.0.9",
|
|
88
88
|
"webpack": "4.47.0",
|
|
89
89
|
"webpack-cli": "3.3.12"
|
|
90
90
|
},
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* Emit GitHub Actions workflow annotations for problems found during a run, so they surface at the
|
|
4
|
+
* top of the run page (and inline in the log) instead of only in the raw output. A no-op outside
|
|
5
|
+
* GitHub Actions, so local runs stay quiet.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** True when running inside a GitHub Actions runner, where `::error::`/`::warning::` are meaningful. */
|
|
9
|
+
const IN_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === 'true'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* GitHub surfaces only about the first ten annotations of each level per step in the run UI. A
|
|
13
|
+
* first-time full sync can fail on many items; emitting hundreds would be truncated there anyway and
|
|
14
|
+
* just spams the log, so cap what we emit and note once when the cap is reached. The full, unbounded
|
|
15
|
+
* list still goes to stderr and the failure summary.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_ANNOTATIONS_PER_LEVEL = 10
|
|
18
|
+
|
|
19
|
+
type AnnotationLevel = 'error' | 'warning' | 'notice'
|
|
20
|
+
|
|
21
|
+
const emittedByLevel: Record<AnnotationLevel, number> = { error: 0, warning: 0, notice: 0 }
|
|
22
|
+
|
|
23
|
+
// In a workflow command's message, only `%`, CR and LF are special and must be encoded. The order
|
|
24
|
+
// matters: encode `%` first so the escapes we add are not themselves re-encoded.
|
|
25
|
+
const escapeData = (value: string): string => value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A')
|
|
26
|
+
|
|
27
|
+
// A property value (such as `title`) additionally encodes `:` and `,`.
|
|
28
|
+
const escapeProperty = (value: string): string => escapeData(value).replace(/:/g, '%3A').replace(/,/g, '%2C')
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Emit a single GitHub Actions annotation. No-op outside Actions and after the per-level display cap
|
|
32
|
+
* is reached (the full list still reaches stderr and the failure summary).
|
|
33
|
+
* @param annotation - the annotation to emit
|
|
34
|
+
* @param annotation.level - annotation severity (`error`, `warning`, or `notice`)
|
|
35
|
+
* @param annotation.message - the annotation body; may contain newlines and a URL
|
|
36
|
+
* @param annotation.title - optional short bold header (for example, the failing item)
|
|
37
|
+
*/
|
|
38
|
+
export const emitAnnotation = (annotation: { level: AnnotationLevel; message: string; title?: string }): void => {
|
|
39
|
+
const { level, message, title } = annotation
|
|
40
|
+
if (!IN_GITHUB_ACTIONS) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
emittedByLevel[level]++
|
|
44
|
+
if (emittedByLevel[level] > MAX_ANNOTATIONS_PER_LEVEL) {
|
|
45
|
+
if (emittedByLevel[level] === MAX_ANNOTATIONS_PER_LEVEL + 1) {
|
|
46
|
+
// Say so once; the rest are in the log and the summary rather than the annotations UI.
|
|
47
|
+
console.log(
|
|
48
|
+
`Reached the GitHub annotation display limit for ${level}s; further ${level}s appear in the log only.`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
const titlePart = title ? ` title=${escapeProperty(title)}` : ''
|
|
54
|
+
process.stdout.write(`::${level}${titlePart}::${escapeData(message)}\n`)
|
|
55
|
+
}
|
package/scripts/lib/errors.mts
CHANGED
|
@@ -10,3 +10,51 @@
|
|
|
10
10
|
* @returns the error message, or a string representation of a non-Error throw
|
|
11
11
|
*/
|
|
12
12
|
export const messageOf = (err: unknown): string => (err instanceof Error ? err.message : String(err))
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Describe where a `JSON.parse` failed, for a diagnostic message. V8 reports the byte offset (and, on
|
|
16
|
+
* newer Node, the line and column) in the error message; combined with the original text this pins the
|
|
17
|
+
* problem to a snippet and, for a flat `{"key": "value"}` file, the key whose value is malformed.
|
|
18
|
+
* Best-effort: returns undefined when the error is not a positioned JSON parse error or the text can't
|
|
19
|
+
* be read, so callers fall back to the plain message.
|
|
20
|
+
* @param error - the caught value (expected to be a `SyntaxError` from `JSON.parse`)
|
|
21
|
+
* @param text - the exact string that was parsed
|
|
22
|
+
* @returns a one-line description (location, nearby key, snippet), or undefined
|
|
23
|
+
*/
|
|
24
|
+
export const describeJsonParseError = (error: unknown, text: string): string | undefined => {
|
|
25
|
+
if (!(error instanceof SyntaxError) || typeof text !== 'string') {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
const posMatch = /in JSON at position (\d+)/.exec(error.message)
|
|
29
|
+
if (!posMatch) {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
const pos = Number(posMatch[1])
|
|
33
|
+
if (!Number.isFinite(pos)) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
const lineColMatch = /\(line (\d+) column (\d+)\)/.exec(error.message)
|
|
37
|
+
const where = lineColMatch ? `line ${lineColMatch[1]}, column ${lineColMatch[2]}` : `position ${pos}`
|
|
38
|
+
// Recover the enclosing key of a flat KEYVALUEJSON object: the value that failed to parse is
|
|
39
|
+
// preceded by `"<key>":`, so scan back from the error position to that key. Heuristic (a colon or
|
|
40
|
+
// quote inside an earlier string can fool it), so it is reported as "near key", not an exact locator.
|
|
41
|
+
let nearKey: string | undefined
|
|
42
|
+
const before = text.slice(0, pos)
|
|
43
|
+
const colon = before.lastIndexOf(':')
|
|
44
|
+
if (colon > 0) {
|
|
45
|
+
const keyClose = before.lastIndexOf('"', colon)
|
|
46
|
+
if (keyClose > 0) {
|
|
47
|
+
const keyOpen = before.lastIndexOf('"', keyClose - 1)
|
|
48
|
+
if (keyOpen >= 0) {
|
|
49
|
+
nearKey = before.slice(keyOpen + 1, keyClose)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
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
|
+
const keyPart = nearKey ? ` near key "${nearKey}"` : ''
|
|
59
|
+
return `malformed JSON at ${where}${keyPart}: …${snippet}…`
|
|
60
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { promises as fsPromises } from 'fs'
|
|
6
6
|
import { mkdirp } from 'mkdirp'
|
|
7
|
-
import {
|
|
7
|
+
import { emitAnnotation } from './annotations.mts'
|
|
8
|
+
import { describeJsonParseError, messageOf } from './errors.mts'
|
|
8
9
|
import FreshdeskApi, {
|
|
9
10
|
FreshdeskArticleCreate,
|
|
10
11
|
FreshdeskArticleStatus,
|
|
@@ -14,7 +15,15 @@ import FreshdeskApi, {
|
|
|
14
15
|
import { loadBaseline, saveBaseline, SyncBaseline } from './sync-baseline.mts'
|
|
15
16
|
import { TransifexStringKeyValueJson, TransifexStringsKeyValueJson, TransifexStrings } from './transifex-formats.mts'
|
|
16
17
|
import { TransifexResourceObject } from './transifex-objects.mts'
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
txPull,
|
|
20
|
+
txResourcesObjects,
|
|
21
|
+
txAvailableLanguages,
|
|
22
|
+
txResourceLanguageStats,
|
|
23
|
+
transifexEditorLink,
|
|
24
|
+
txPullErrorCause,
|
|
25
|
+
SOURCE_LOCALE,
|
|
26
|
+
} from './transifex.mts'
|
|
18
27
|
import { emitWarning } from './warnings.mts'
|
|
19
28
|
|
|
20
29
|
const FD = new FreshdeskApi('https://mitscratch.freshdesk.com', process.env.FRESHDESK_TOKEN ?? '')
|
|
@@ -25,12 +34,30 @@ const TX_PROJECT = 'scratch-help'
|
|
|
25
34
|
// transparently by the client (paced requests, `Retry-After`-aware retries), so anything that reaches
|
|
26
35
|
// here is a genuine problem worth a human's attention. `reportFailures` prints a consolidated summary
|
|
27
36
|
// and fails the run at the end, so real errors are surfaced rather than lost in the log.
|
|
28
|
-
const failures: { context: string; message: string }[] = []
|
|
29
|
-
const recordFailure = (context: string, error: unknown): void => {
|
|
37
|
+
const failures: { context: string; message: string; link?: string }[] = []
|
|
38
|
+
const recordFailure = (context: string, error: unknown, link?: string): void => {
|
|
30
39
|
const message = messageOf(error)
|
|
40
|
+
// A txPull error carries the failing (resource, locale) — and, for a parse failure, the raw text —
|
|
41
|
+
// on its `cause`, so we can build a Transifex link and describe malformed JSON without the call site
|
|
42
|
+
// passing anything. An explicit link still wins: some callers know the fix lives on the source (`en`)
|
|
43
|
+
// rather than the translation locale the pull used.
|
|
44
|
+
const cause = txPullErrorCause(error)
|
|
45
|
+
const resolvedLink =
|
|
46
|
+
link ??
|
|
47
|
+
(cause
|
|
48
|
+
? transifexEditorLink({ project: cause.project, resource: cause.resource, lang: cause.locale })
|
|
49
|
+
: undefined)
|
|
50
|
+
const detail = cause?.buffer != null ? describeJsonParseError(error, cause.buffer) : undefined
|
|
51
|
+
const fullMessage = detail ? `${message} — ${detail}` : message
|
|
31
52
|
// Genuine failures go to stderr (like the consolidated summary), so they stand apart from normal output.
|
|
32
|
-
process.stderr.write(`Error: ${context}: ${
|
|
33
|
-
failures.push({ context, message })
|
|
53
|
+
process.stderr.write(`Error: ${context}: ${fullMessage}${resolvedLink ? ` (${resolvedLink})` : ''}\n`)
|
|
54
|
+
failures.push({ context, message: fullMessage, link: resolvedLink })
|
|
55
|
+
// Surface it on the run page too (no-op off CI). The full list still lives in stderr and the summary.
|
|
56
|
+
emitAnnotation({
|
|
57
|
+
level: 'error',
|
|
58
|
+
title: context,
|
|
59
|
+
message: resolvedLink ? `${fullMessage}\n${resolvedLink}` : fullMessage,
|
|
60
|
+
})
|
|
34
61
|
}
|
|
35
62
|
|
|
36
63
|
/**
|
|
@@ -43,7 +70,7 @@ export const reportFailures = (): void => {
|
|
|
43
70
|
}
|
|
44
71
|
console.error(`\n${failures.length} item(s) failed to sync to Freshdesk:`)
|
|
45
72
|
for (const failure of failures) {
|
|
46
|
-
console.error(` - ${failure.context}: ${failure.message}`)
|
|
73
|
+
console.error(` - ${failure.context}: ${failure.message}${failure.link ? ` (${failure.link})` : ''}`)
|
|
47
74
|
}
|
|
48
75
|
process.exitCode = 1
|
|
49
76
|
}
|
|
@@ -386,6 +413,8 @@ const serializeNameSave = async (
|
|
|
386
413
|
warnedKeys.add(warnedKey)
|
|
387
414
|
emitWarning(
|
|
388
415
|
`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
|
+
// The key is removed from the source, so point at the source language, not a translation.
|
|
417
|
+
transifexEditorLink({ project: TX_PROJECT, resource: resource.attributes.slug, lang: SOURCE_LOCALE }),
|
|
389
418
|
)
|
|
390
419
|
}
|
|
391
420
|
continue
|
|
@@ -409,7 +438,11 @@ const serializeNameSave = async (
|
|
|
409
438
|
} catch (error) {
|
|
410
439
|
// Record the failure so the job still reports a non-zero exit at the end, then move on to the
|
|
411
440
|
// next entry.
|
|
412
|
-
recordFailure(
|
|
441
|
+
recordFailure(
|
|
442
|
+
`${resource.attributes.name} entry "${key}" for ${locale}`,
|
|
443
|
+
error,
|
|
444
|
+
transifexEditorLink({ project: TX_PROJECT, resource: resource.attributes.slug, lang: locale }),
|
|
445
|
+
)
|
|
413
446
|
allOk = false
|
|
414
447
|
}
|
|
415
448
|
}
|
|
@@ -430,11 +463,13 @@ interface FreshdeskFolderInTransifex {
|
|
|
430
463
|
* Internal function serialize Freshdesk requests to avoid getting rate limited
|
|
431
464
|
* @param json object with keys corresponding to article ids
|
|
432
465
|
* @param locale language code
|
|
466
|
+
* @param resourceSlug Transifex resource slug for these articles, used to build editor links in warnings and failures
|
|
433
467
|
* @returns true if every article saved without a failure
|
|
434
468
|
*/
|
|
435
469
|
const serializeFolderSave = async (
|
|
436
470
|
json: TransifexStrings<FreshdeskFolderInTransifex>,
|
|
437
471
|
locale: string,
|
|
472
|
+
resourceSlug: string,
|
|
438
473
|
): Promise<boolean> => {
|
|
439
474
|
let allOk = true
|
|
440
475
|
for (const [idString, value] of Object.entries(json)) {
|
|
@@ -457,6 +492,7 @@ const serializeFolderSave = async (
|
|
|
457
492
|
`(${locale}); shorten them in Transifex: ${droppedTags
|
|
458
493
|
.map(tag => `"${tag}" (${tag.length} chars)`)
|
|
459
494
|
.join(', ')}.`,
|
|
495
|
+
transifexEditorLink({ project: TX_PROJECT, resource: resourceSlug, lang: locale }),
|
|
460
496
|
)
|
|
461
497
|
}
|
|
462
498
|
body.tags = validTags
|
|
@@ -470,7 +506,11 @@ const serializeFolderSave = async (
|
|
|
470
506
|
} catch (error) {
|
|
471
507
|
// Record the failure so the job still reports a non-zero exit at the end, then move on to the
|
|
472
508
|
// next article.
|
|
473
|
-
recordFailure(
|
|
509
|
+
recordFailure(
|
|
510
|
+
`article ${idString} for ${locale}`,
|
|
511
|
+
error,
|
|
512
|
+
transifexEditorLink({ project: TX_PROJECT, resource: resourceSlug, lang: locale }),
|
|
513
|
+
)
|
|
474
514
|
allOk = false
|
|
475
515
|
}
|
|
476
516
|
}
|
|
@@ -491,7 +531,7 @@ export const localizeFolder = async (folderAttributes: TransifexResourceObject,
|
|
|
491
531
|
locale,
|
|
492
532
|
'default',
|
|
493
533
|
)
|
|
494
|
-
return await serializeFolderSave(data, locale)
|
|
534
|
+
return await serializeFolderSave(data, locale, folderAttributes.attributes.slug)
|
|
495
535
|
} catch (e) {
|
|
496
536
|
recordFailure(`${folderAttributes.attributes.slug} for ${locale}`, e)
|
|
497
537
|
return false
|
|
@@ -579,7 +619,11 @@ export const saveItem = async (item: TransifexResourceObject, languages: string[
|
|
|
579
619
|
markSynced(slug, l)
|
|
580
620
|
}
|
|
581
621
|
} catch (err) {
|
|
582
|
-
recordFailure(
|
|
622
|
+
recordFailure(
|
|
623
|
+
`saving ${slug} for ${l}`,
|
|
624
|
+
err,
|
|
625
|
+
transifexEditorLink({ project: TX_PROJECT, resource: slug, lang: l }),
|
|
626
|
+
)
|
|
583
627
|
}
|
|
584
628
|
}),
|
|
585
629
|
)
|
|
@@ -12,7 +12,63 @@ import {
|
|
|
12
12
|
} from './transifex-objects.mts'
|
|
13
13
|
|
|
14
14
|
const ORG_NAME = 'llk'
|
|
15
|
-
const SOURCE_LOCALE = 'en'
|
|
15
|
+
export const SOURCE_LOCALE = 'en'
|
|
16
|
+
|
|
17
|
+
/**
|
|
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>]`.
|
|
20
|
+
* @param params - link target
|
|
21
|
+
* @param params.project - project slug (for example, `scratch-help`)
|
|
22
|
+
* @param params.resource - resource slug (for example, `Accessibility_4000040849_json`)
|
|
23
|
+
* @param params.lang - language code: the source ({@link SOURCE_LOCALE}) for a source-side fix, or the
|
|
24
|
+
* translation locale for a bad translation
|
|
25
|
+
* @param params.stringId - optional Transifex numeric string id to focus a single string; omitted, the
|
|
26
|
+
* link lands on the resource (with its first string shown)
|
|
27
|
+
* @returns the editor URL
|
|
28
|
+
*/
|
|
29
|
+
export const transifexEditorLink = (params: {
|
|
30
|
+
project: string
|
|
31
|
+
resource: string
|
|
32
|
+
lang: string
|
|
33
|
+
stringId?: string | number
|
|
34
|
+
}): string => {
|
|
35
|
+
const { project, resource, lang, stringId } = params
|
|
36
|
+
const base = `https://app.transifex.com/${ORG_NAME}/${project}/translate/#${lang}/${resource}`
|
|
37
|
+
return stringId == null ? base : `${base}/${stringId}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Shape attached to a {@link txPull} error's `cause`, carrying the context needed to locate and link
|
|
42
|
+
* the failing (resource, locale) in Transifex (and, for a parse failure, the raw file that failed).
|
|
43
|
+
*/
|
|
44
|
+
export interface TxPullErrorCause {
|
|
45
|
+
project: string
|
|
46
|
+
resource: string
|
|
47
|
+
locale: string
|
|
48
|
+
buffer: string | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read the {@link TxPullErrorCause} that {@link txPull} attaches to its errors, if present, so a
|
|
53
|
+
* failure handler can build a Transifex link (and read the raw buffer for a parse failure) without the
|
|
54
|
+
* call site threading that context through.
|
|
55
|
+
* @param error - the caught value
|
|
56
|
+
* @returns the cause, or undefined if the error did not come from txPull
|
|
57
|
+
*/
|
|
58
|
+
export const txPullErrorCause = (error: unknown): TxPullErrorCause | undefined => {
|
|
59
|
+
const cause = (error as { cause?: unknown } | null)?.cause
|
|
60
|
+
if (
|
|
61
|
+
cause &&
|
|
62
|
+
typeof cause === 'object' &&
|
|
63
|
+
typeof (cause as TxPullErrorCause).project === 'string' &&
|
|
64
|
+
typeof (cause as TxPullErrorCause).resource === 'string' &&
|
|
65
|
+
typeof (cause as TxPullErrorCause).locale === 'string' &&
|
|
66
|
+
(typeof (cause as TxPullErrorCause).buffer === 'string' || (cause as TxPullErrorCause).buffer === null)
|
|
67
|
+
) {
|
|
68
|
+
return cause as TxPullErrorCause
|
|
69
|
+
}
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
16
72
|
|
|
17
73
|
if (!process.env.TX_TOKEN) {
|
|
18
74
|
throw new Error('TX_TOKEN is not defined.')
|
package/scripts/lib/warnings.mts
CHANGED
|
@@ -3,24 +3,30 @@
|
|
|
3
3
|
* Shared helper for reporting non-fatal problems during help sync.
|
|
4
4
|
*/
|
|
5
5
|
import { appendFileSync } from 'fs'
|
|
6
|
+
import { emitAnnotation } from './annotations.mts'
|
|
6
7
|
import { messageOf } from './errors.mts'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Log a warning to the console and, when the `WARNINGS_FILE` environment variable is set, append it
|
|
10
11
|
* to that file. CI reads the file after the sync to surface warnings in the job summary and to send
|
|
11
12
|
* a notification, so a warning is the right tool for a problem worth a human's attention that should
|
|
12
|
-
* not fail the run (for example, a resource we deliberately skip).
|
|
13
|
+
* not fail the run (for example, a resource we deliberately skip). Also emits a GitHub Actions
|
|
14
|
+
* warning annotation (a no-op off CI) so the problem shows on the run page, not just in the log.
|
|
13
15
|
* @param warning - the warning message; a trailing newline is added when written to the file
|
|
16
|
+
* @param link - optional convenience URL (for example, a Transifex editor deep link) appended to the
|
|
17
|
+
* warning so a reader can jump straight to where the fix is made
|
|
14
18
|
*/
|
|
15
|
-
export const emitWarning = (warning: string): void => {
|
|
16
|
-
|
|
19
|
+
export const emitWarning = (warning: string, link?: string): void => {
|
|
20
|
+
const line = link ? `${warning} (${link})` : warning
|
|
21
|
+
console.warn(line)
|
|
17
22
|
if (process.env.WARNINGS_FILE) {
|
|
18
23
|
// The file write is best-effort: emitWarning is the non-fatal path (often called from a catch
|
|
19
24
|
// block), so a failed append must not turn a warning into a crash.
|
|
20
25
|
try {
|
|
21
|
-
appendFileSync(process.env.WARNINGS_FILE,
|
|
26
|
+
appendFileSync(process.env.WARNINGS_FILE, line + '\n')
|
|
22
27
|
} catch (error) {
|
|
23
28
|
console.warn(`Could not append to WARNINGS_FILE "${process.env.WARNINGS_FILE}": ${messageOf(error)}`)
|
|
24
29
|
}
|
|
25
30
|
}
|
|
31
|
+
emitAnnotation({ level: 'warning', message: line })
|
|
26
32
|
}
|