Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 119 additions & 5 deletions scripts/normalize-xlf.prebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,35 @@ const TEST_LANGUAGES = [
// hardcoded list to drift out of sync.
const PRINT_VIEW_SOURCE_FILE = './src/assets/print-view/fetch-orcid.js'

// Sentinels used to carry XLIFF <x/> placeholder elements through the xml2js
// Builder, which escapes any markup it finds in a text node. We emit the
// sentinels as plain text and swap them for real elements in formatXmlOutput().
const PH_START = '@@XPH_START@@'
const PH_MID = '@@XPH_MID@@'
const PH_END = '@@XPH_END@@'

// Matches a $localize template placeholder: `${expression}:NAME:`.
// NAME deliberately accepts any case: fetch-orcid.js uses camelCase names, and
// $localize matches placeholder names verbatim.
const LOCALIZE_PLACEHOLDER_RE = /\$\{([^}]*)\}:([A-Za-z0-9_]+):/g

// Non-global twin of the above, for stateless `.test()` checks (a shared /g
// regex carries lastIndex between calls).
const HAS_LOCALIZE_PLACEHOLDER_RE = new RegExp(LOCALIZE_PLACEHOLDER_RE.source)

interface PrintViewUnit {
id: string
source: string
}

function escapeXmlAttr(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}

function parsePrintViewUnits(path: string): PrintViewUnit[] {
const js = fs.readFileSync(path, 'utf8')
// Matches $localize tagged templates of the form `:@@printView.someId:Source text`
Expand All @@ -33,11 +57,18 @@ function parsePrintViewUnits(path: string): PrintViewUnit[] {
continue
}
seen.add(id)
// Convert $localize template placeholders `${expression}:NAME:` into the
// canonical $localize message form `{$NAME}` so the XLF source matches the
// runtime message and stays stable regardless of the JS expression.
// Convert $localize template placeholders `${expression}:NAME:` into XLIFF
// <x id="NAME"/> elements (emitted as sentinels here, see formatXmlOutput).
// Plain-text forms such as `{$NAME}` do NOT work: Xliff1TranslationParser
// only recognises real <x/> elements, so a text placeholder is parsed as
// literal content and the message ends up with zero placeholders — the
// translation is then silently truncated at the first `$`.
const source = normalizeText(
rawSource.replace(/\$\{[^}]*\}:([A-Z0-9_]+):/g, '{$$$1}')
rawSource.replace(
LOCALIZE_PLACEHOLDER_RE,
(_match, expression: string, name: string) =>
`${PH_START}${name}${PH_MID}\${${expression}}${PH_END}`
)
)
units.push({ id, source })
}
Expand All @@ -54,6 +85,13 @@ function normalizeText(input: string): string {

function formatXmlOutput(xml: string): string {
let out = xml
// Restore XLIFF placeholder elements that were emitted as sentinels so the
// xml2js Builder would not escape them into plain text.
out = out.replace(
new RegExp(`${PH_START}(.*?)${PH_MID}(.*?)${PH_END}`, 'g'),
(_match, name: string, equivText: string) =>
`<x id="${name}" equiv-text="${escapeXmlAttr(equivText)}"/>`
)
// Match tx pull header formatting (xml decl + <xliff ...> on same line)
out = out.replace(
/<xliff[^>]*>/,
Expand Down Expand Up @@ -97,6 +135,69 @@ function generateTestingLanguages(baseData: any) {
})
}

/** Extracts the ordered <x id="..."/> names inside an XLF element body. */
function placeholderIds(fragment: string): string[] {
return Array.from(fragment.matchAll(/<x\s+id="([^"]+)"/g)).map((m) => m[1])
}

/**
* Warns when a translated printView.* target has lost one of the placeholders
* its source declares.
*
* Such a target still inlines cleanly — @angular/localize reports no error — but
* renders with the value silently dropped, e.g. a peer review count vanishing
* from the heading. That silence is what let this class of bug ship before, so
* surface it at build time. Read-only: translations are never rewritten here.
*/
function checkLocalePlaceholders(localeDir: string) {
const files = fs
.readdirSync(localeDir)
.filter((name) => name.startsWith('messages.') && name.endsWith('.xlf'))
// The generated test languages replace every target with a marker, so their
// placeholders are legitimately absent.
.filter(
(name) =>
!TEST_LANGUAGES.some(({ code }) => name === `messages.${code}.xlf`)
)

files.forEach((name) => {
const xlf = fs.readFileSync(`${localeDir}/${name}`, 'utf8')

// Transifex holds the <x/> form of these strings, so a pull should never
// bring back the legacy `${expression}:name:` text. If it does, the source
// there has gone stale and every locale would render the heading truncated
// at the first `$` with no other symptom — fail rather than ship that.
if (HAS_LOCALIZE_PLACEHOLDER_RE.test(xlf)) {
throw new Error(
`[normalize-xlf] ${name} contains legacy raw placeholder syntax ` +
`(\${expression}:name:). Push the current messages.xlf to Transifex ` +
`so translations come back with <x id="..."/> placeholders.`
)
}

Array.from(
xlf.matchAll(/<trans-unit id="(printView\.[^"]*)"[\s\S]*?<\/trans-unit>/g)
).forEach(([unit, id]) => {
const target = unit.match(/<target[^>]*>([\s\S]*?)<\/target>/)
if (!target) {
return
}
const expected = placeholderIds(
unit.match(/<source[^>]*>([\s\S]*?)<\/source>/)?.[1] ?? ''
)
const actual = placeholderIds(target[1])
const missing = expected.filter((p) => !actual.includes(p))
if (missing.length) {
console.warn(
`[normalize-xlf] ${name}: "${id}" target is missing placeholder(s) ${missing.join(
', '
)} — their values will be dropped from the rendered string.`
)
}
})
})
}

function normalizeXlf12Sources(path: string) {
const xml = fs.readFileSync(path, 'utf8')
parseString(xml, (error, data) => {
Expand All @@ -111,11 +212,24 @@ function normalizeXlf12Sources(path: string) {
throw new Error('No trans-units found in XLF 1.2 file')
}

const printViewUnits = parsePrintViewUnits(PRINT_VIEW_SOURCE_FILE)
const printViewById = new Map(printViewUnits.map((u) => [u.id, u]))

transUnits.forEach((tu) => {
// Provide a stable key for Transifex (some parsers rely on resname over id/source)
if (tu?.$?.id) {
tu.$.resname = tu.$.id
}
const generated = tu?.$?.id ? printViewById.get(tu.$.id) : undefined
if (generated) {
// fetch-orcid.js is the source of truth for printView.* units, so
// always regenerate the source rather than keeping what is on disk.
// Re-parsing a unit that already contains <x/> elements yields mixed
// content, which the Builder cannot round-trip; replacing the whole
// node with a freshly built string keeps placeholders correct.
tu.source = [generated.source]
return
}
const src = tu?.source?.[0]
if (typeof src === 'string') {
tu.source[0] = normalizeText(src)
Expand All @@ -127,7 +241,6 @@ function normalizeXlf12Sources(path: string) {
// will also stamp X / LR / RL for these strings, and so Transifex can
// discover them for real locales. We use a stable @@-prefixed id so
// $localize can match by explicit id.
const printViewUnits = parsePrintViewUnits(PRINT_VIEW_SOURCE_FILE)
const existingIds = new Set(transUnits.map((tu: any) => tu?.$?.id))
const printViewBody = fileNode?.body?.[0]
printViewUnits.forEach(({ id, source }) => {
Expand All @@ -145,3 +258,4 @@ function normalizeXlf12Sources(path: string) {
}

normalizeXlf12Sources('./src/locale/messages.xlf')
checkLocalePlaceholders('./src/locale')
4 changes: 2 additions & 2 deletions src/locale/messages.ar.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21237,8 +21237,8 @@
<target>ممول من</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>مراجعة الأقران (${reviewsCount}:reviewCount: مراجعة لـ ${publicationsCount}:publicationCount: منشور/منحة)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>مراجعة الأقران (<x id="reviewCount" equiv-text="${reviewsCount}"/> مراجعة لـ <x id="publicationCount" equiv-text="${publicationsCount}"/> منشور/منحة)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
2 changes: 1 addition & 1 deletion src/locale/messages.ca.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21057,7 +21057,7 @@
<target>Funded by</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
4 changes: 2 additions & 2 deletions src/locale/messages.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21238,8 +21238,8 @@
<target>Financováno</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>Posudek oponenta (${reviewsCount}:reviewCount: posudek na ${publicationsCount}:publicationCount: publikace/granty)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>Posudek oponenta (<x id="reviewCount" equiv-text="${reviewsCount}"/> posudek na <x id="publicationCount" equiv-text="${publicationsCount}"/> publikace/granty)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
4 changes: 2 additions & 2 deletions src/locale/messages.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21238,8 +21238,8 @@
<target>Finanziert durch</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>Peer-Review (${reviewsCount}:reviewCount: Reviews für ${publicationsCount}:publicationCount: Veröffentlichungen/Förderungen)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>Peer-Review (<x id="reviewCount" equiv-text="${reviewsCount}"/> Reviews für <x id="publicationCount" equiv-text="${publicationsCount}"/> Veröffentlichungen/Förderungen)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
4 changes: 2 additions & 2 deletions src/locale/messages.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21238,8 +21238,8 @@
<target>Financiado por</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>Revisión por pares (${reviewsCount}:reviewCount: revisiones de ${publicationsCount}:publicationCount: publicaciones/concesiones)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>Revisión por pares (<x id="reviewCount" equiv-text="${reviewsCount}"/> revisiones de <x id="publicationCount" equiv-text="${publicationsCount}"/> publicaciones/concesiones)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
4 changes: 2 additions & 2 deletions src/locale/messages.fr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21238,8 +21238,8 @@
<target>Financé par</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>Évaluation par les pairs (${reviewsCount}:reviewCount: évaluations pour ${publicationsCount}:publicationCount: publications/subventions)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>Évaluation par les pairs (<x id="reviewCount" equiv-text="${reviewsCount}"/> évaluations pour <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/subventions)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
4 changes: 2 additions & 2 deletions src/locale/messages.it.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21238,8 +21238,8 @@
<target>Finanziato da</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>Revisione paritaria (${reviewsCount}:reviewCount: revisioni per ${publicationsCount}:publicationCount: pubblicazioni/borse)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>Revisione paritaria (<x id="reviewCount" equiv-text="${reviewsCount}"/> revisioni per <x id="publicationCount" equiv-text="${publicationsCount}"/> pubblicazioni/borse)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
4 changes: 2 additions & 2 deletions src/locale/messages.ja.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21238,8 +21238,8 @@
<target>資金の提供:</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>ピアレビュー (レビュー ${reviewsCount}:reviewCount: 件、出版/助成 ${publicationsCount}:publicationCount: 件)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>ピアレビュー (レビュー <x id="reviewCount" equiv-text="${reviewsCount}"/> 件、出版/助成 <x id="publicationCount" equiv-text="${publicationsCount}"/> 件)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
4 changes: 2 additions & 2 deletions src/locale/messages.ko.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21238,8 +21238,8 @@
<target>자금 제공 주체</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>동료 검토 (${reviewsCount}:reviewCount:건의 검토 - ${publicationsCount}:publicationCount:건의 발행물/보조금)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>동료 검토 (<x id="reviewCount" equiv-text="${reviewsCount}"/>건의 검토 - <x id="publicationCount" equiv-text="${publicationsCount}"/>건의 발행물/보조금)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
2 changes: 1 addition & 1 deletion src/locale/messages.lr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21181,7 +21181,7 @@
<target>LR</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>LR</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
Expand Down
4 changes: 2 additions & 2 deletions src/locale/messages.pl_PL.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21238,8 +21238,8 @@
<target>Finansowane przez</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>Recenzja naukowa (recenzje (${reviewsCount}:reviewCount:) dla następującej liczby publikacji/grantów: ${publicationsCount}:publicationCount:)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>Recenzja naukowa (recenzje (<x id="reviewCount" equiv-text="${reviewsCount}"/>) dla następującej liczby publikacji/grantów: <x id="publicationCount" equiv-text="${publicationsCount}"/>)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
4 changes: 2 additions & 2 deletions src/locale/messages.pt.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21238,8 +21238,8 @@
<target>Financiado por</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<target>Revisão por pares (${reviewsCount}:reviewCount: revisões para ${publicationsCount}:publicationCount: publicações/bolsas)</target>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>Revisão por pares (<x id="reviewCount" equiv-text="${reviewsCount}"/> revisões para <x id="publicationCount" equiv-text="${publicationsCount}"/> publicações/bolsas)</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
<source>Afghanistan</source>
Expand Down
2 changes: 1 addition & 1 deletion src/locale/messages.rl.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -21181,7 +21181,7 @@
<target>RL</target>
</trans-unit>
<trans-unit id="printView.peerReviewSummary" datatype="html" resname="printView.peerReviewSummary">
<source>Peer review (${reviewsCount}:reviewCount: reviews for ${publicationsCount}:publicationCount: publications/grants)</source>
<source>Peer review (<x id="reviewCount" equiv-text="${reviewsCount}"/> reviews for <x id="publicationCount" equiv-text="${publicationsCount}"/> publications/grants)</source>
<target>RL</target>
</trans-unit>
<trans-unit id="printView.AF" datatype="html" resname="printView.AF">
Expand Down
Loading
Loading