Skip to content

Commit 9b94bf0

Browse files
docs-botCopilotheiskr
authored
Convert legacy \{\% note/warning/tip/danger %} Liquid blocks to GFM alerts in translations (#63093)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: heiskr <1221423+heiskr@users.noreply.github.com> Co-authored-by: Kevin Heis <heiskr@users.noreply.github.com>
1 parent 31dde23 commit 9b94bf0

2 files changed

Lines changed: 168 additions & 37 deletions

File tree

‎src/languages/lib/correct-translation-content.ts‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2721,6 +2721,14 @@ export function correctTranslatedContentStrings(
27212721
content = content.replaceAll('{%endraw %}', '{% endraw %}')
27222722
content = content.replaceAll('{%endraw -%}', '{% endraw -%}')
27232723

2724+
// `{% note %}` / `{% warning %}` / `{% tip %}` / `{% danger %}` were removed
2725+
// from the Liquid renderer (replaced by GFM alert blockquotes, see
2726+
// PR #62960 / commit 8b174bc4), but many translation files were forked
2727+
// before that change and still use the old tag syntax, which now fails
2728+
// with "tag not found" render errors. Strip the obsolete tags so the rest
2729+
// of the content renders.
2730+
content = stripLegacyAlertTags(content)
2731+
27242732
// Strip stray closing-only Liquid tags that have no matching opener anywhere
27252733
// in the content. Translators sometimes insert spurious closers (e.g. an
27262734
// extra `{% endif %}`) when they re-arrange paragraphs. We only remove a
@@ -2993,3 +3001,92 @@ function joinDanglingMarkers(content: string): string {
29933001

29943002
return out.join('\n')
29953003
}
3004+
3005+
/**
3006+
* Remove the obsolete `{% note %}` / `{% warning %}` / `{% tip %}` /
3007+
* `{% danger %}` Liquid tags (and their closers) from translated content.
3008+
*
3009+
* These tags were removed from the renderer in favour of GFM alert
3010+
* blockquotes, so any leftover occurrence now fails to render. Stripping
3011+
* them leaves the surrounding text intact.
3012+
*
3013+
* Skips YAML frontmatter, fenced code blocks, `{% raw %}` blocks, and inline
3014+
* code spans, where the tags are literal examples rather than markup.
3015+
*/
3016+
function stripLegacyAlertTags(content: string): string {
3017+
const tagPattern = /\{%-?\s*(?:end)?(?:note|warning|tip|danger)\s*-?%\}[ \t]*/g
3018+
const lines = content.split('\n')
3019+
const out: string[] = []
3020+
let inFence = false
3021+
let fenceChar = ''
3022+
let fenceLen = 0
3023+
let inRaw = false
3024+
let inFrontmatter = lines[0] === '---'
3025+
3026+
const stripOutsideInlineCode = (line: string): string =>
3027+
line
3028+
.split('`')
3029+
.map((segment, index) => (index % 2 === 0 ? segment.replace(tagPattern, '') : segment))
3030+
.join('`')
3031+
3032+
for (let i = 0; i < lines.length; i++) {
3033+
const line = lines[i]
3034+
3035+
if (inFrontmatter) {
3036+
if (i > 0 && (line === '---' || line === '...')) inFrontmatter = false
3037+
out.push(line)
3038+
continue
3039+
}
3040+
3041+
const fenceMatch = line.match(/^[ \t]*(`{3,}|~{3,})/)
3042+
if (fenceMatch) {
3043+
const marker = fenceMatch[1]
3044+
if (!inFence) {
3045+
inFence = true
3046+
fenceChar = marker[0]
3047+
fenceLen = marker.length
3048+
} else if (marker[0] === fenceChar && marker.length >= fenceLen) {
3049+
inFence = false
3050+
fenceChar = ''
3051+
fenceLen = 0
3052+
}
3053+
out.push(line)
3054+
continue
3055+
}
3056+
3057+
if (inFence) {
3058+
out.push(line)
3059+
continue
3060+
}
3061+
3062+
if (inRaw) {
3063+
if (/\{%-?\s*endraw\s*-?%\}/.test(line)) inRaw = false
3064+
out.push(line)
3065+
continue
3066+
}
3067+
if (/\{%-?\s*raw\s*-?%\}/.test(line) && !/\{%-?\s*endraw\s*-?%\}/.test(line)) {
3068+
inRaw = true
3069+
out.push(line)
3070+
continue
3071+
}
3072+
3073+
const withoutTags = stripOutsideInlineCode(line)
3074+
if (withoutTags === line) {
3075+
out.push(line)
3076+
continue
3077+
}
3078+
const stripped = withoutTags.replace(/[ \t]+$/, '')
3079+
3080+
// The line contained only the tag: drop it, and collapse the surrounding
3081+
// blank lines into one so the alert body keeps its original spacing.
3082+
if (stripped === '') {
3083+
const previousIsBlank = out.length === 0 || out[out.length - 1] === ''
3084+
if (previousIsBlank && lines[i + 1] === '') i++
3085+
continue
3086+
}
3087+
3088+
out.push(stripped)
3089+
}
3090+
3091+
return out.join('\n')
3092+
}

‎src/languages/tests/correct-translation-content.ts‎

Lines changed: 71 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ describe('correctTranslatedContentStrings', () => {
127127
})
128128

129129
test('fixes translated note block tags', () => {
130-
expect(fix('{% nota %}', 'es')).toBe('{% note %}')
131-
expect(fix('{%- nota %}', 'es')).toBe('{%- note %}')
132-
expect(fix('{%- nota -%}', 'es')).toBe('{%- note -%}')
130+
expect(fix('{% nota %}', 'es')).toBe('')
131+
expect(fix('{%- nota %}', 'es')).toBe('')
132+
expect(fix('{%- nota -%}', 'es')).toBe('')
133133
})
134134

135135
test('fixes otra → else', () => {
@@ -238,8 +238,8 @@ describe('correctTranslatedContentStrings', () => {
238238
})
239239

240240
test('fixes note keyword', () => {
241-
expect(fix('{% メモ %}', 'ja')).toBe('{% note %}')
242-
expect(fix('{%- メモ %}', 'ja')).toBe('{%- note %}')
241+
expect(fix('{% メモ %}', 'ja')).toBe('')
242+
expect(fix('{%- メモ %}', 'ja')).toBe('')
243243
})
244244

245245
test('fixes Japanese or (または) in ifversion tags', () => {
@@ -557,8 +557,8 @@ describe('correctTranslatedContentStrings', () => {
557557
})
558558

559559
test('fixes observação → note', () => {
560-
expect(fix('{% observação %}', 'pt')).toBe('{% note %}')
561-
expect(fix('{%- observação %}', 'pt')).toBe('{%- note %}')
560+
expect(fix('{% observação %}', 'pt')).toBe('')
561+
expect(fix('{%- observação %}', 'pt')).toBe('')
562562
})
563563

564564
test('fixes comentário → comment', () => {
@@ -567,8 +567,8 @@ describe('correctTranslatedContentStrings', () => {
567567
})
568568

569569
test('fixes nota de fim → endnote', () => {
570-
expect(fix('{% nota de fim %}', 'pt')).toBe('{% endnote %}')
571-
expect(fix('{%- nota de fim %}', 'pt')).toBe('{%- endnote %}')
570+
expect(fix('{% nota de fim %}', 'pt')).toBe('')
571+
expect(fix('{%- nota de fim %}', 'pt')).toBe('')
572572
})
573573

574574
test('fixes Dados variables → data variables', () => {
@@ -912,8 +912,8 @@ describe('correctTranslatedContentStrings', () => {
912912
test('fixes other translated keywords', () => {
913913
expect(fix('{% конечным %}', 'ru')).toBe('{% endif %}')
914914
expect(fix('{%- конечным %}', 'ru')).toBe('{%- endif %}')
915-
expect(fix('{% примечание %}', 'ru')).toBe('{% note %}')
916-
expect(fix('{%- примечание %}', 'ru')).toBe('{%- note %}')
915+
expect(fix('{% примечание %}', 'ru')).toBe('')
916+
expect(fix('{%- примечание %}', 'ru')).toBe('')
917917
expect(fix('{% конечных головщиков %}', 'ru')).toBe('{% endrowheaders %}')
918918
expect(fix('{% эндкёрл %}', 'ru')).toBe('{% endcurl %}')
919919
expect(fix('{%- эндкёрл %}', 'ru')).toBe('{%- endcurl %}')
@@ -1098,15 +1098,15 @@ describe('correctTranslatedContentStrings', () => {
10981098
})
10991099

11001100
test('fixes translated block tags', () => {
1101-
expect(fix('{% remarque %}', 'fr')).toBe('{% note %}')
1102-
expect(fix('{%- remarque %}', 'fr')).toBe('{%- note %}')
1103-
expect(fix('{%- remarque -%}', 'fr')).toBe('{%- note -%}')
1104-
expect(fix('{% avertissement %}', 'fr')).toBe('{% warning %}')
1105-
expect(fix('{%- avertissement %}', 'fr')).toBe('{%- warning %}')
1106-
expect(fix('{%- avertissement -%}', 'fr')).toBe('{%- warning -%}')
1107-
expect(fix('{% conseil %}', 'fr')).toBe('{% tip %}')
1108-
expect(fix('{%- conseil %}', 'fr')).toBe('{%- tip %}')
1109-
expect(fix('{%- conseil -%}', 'fr')).toBe('{%- tip -%}')
1101+
expect(fix('{% remarque %}', 'fr')).toBe('')
1102+
expect(fix('{%- remarque %}', 'fr')).toBe('')
1103+
expect(fix('{%- remarque -%}', 'fr')).toBe('')
1104+
expect(fix('{% avertissement %}', 'fr')).toBe('')
1105+
expect(fix('{%- avertissement %}', 'fr')).toBe('')
1106+
expect(fix('{%- avertissement -%}', 'fr')).toBe('')
1107+
expect(fix('{% conseil %}', 'fr')).toBe('')
1108+
expect(fix('{%- conseil %}', 'fr')).toBe('')
1109+
expect(fix('{%- conseil -%}', 'fr')).toBe('')
11101110
})
11111111

11121112
test('removes orphaned endif when no matching ifversion/elsif opener exists', () => {
@@ -1136,8 +1136,8 @@ describe('correctTranslatedContentStrings', () => {
11361136
})
11371137

11381138
test('fixes note de fin → endnote', () => {
1139-
expect(fix('{% note de fin %}', 'fr')).toBe('{% endnote %}')
1140-
expect(fix('{%- note de fin %}', 'fr')).toBe('{%- endnote %}')
1139+
expect(fix('{% note de fin %}', 'fr')).toBe('')
1140+
expect(fix('{%- note de fin %}', 'fr')).toBe('')
11411141
})
11421142

11431143
test('fixes éclipse → eclipse platform tag', () => {
@@ -1282,8 +1282,8 @@ describe('correctTranslatedContentStrings', () => {
12821282
test('fixes translated keywords', () => {
12831283
expect(fix('{% 기타 %}', 'ko')).toBe('{% else %}')
12841284
expect(fix('{%- 기타 %}', 'ko')).toBe('{%- else %}')
1285-
expect(fix('{% 참고 %}', 'ko')).toBe('{% note %}')
1286-
expect(fix('{%- 참고 %}', 'ko')).toBe('{%- note %}')
1285+
expect(fix('{% 참고 %}', 'ko')).toBe('')
1286+
expect(fix('{%- 참고 %}', 'ko')).toBe('')
12871287
expect(fix('{% 원시 %}', 'ko')).toBe('{% raw %}')
12881288
expect(fix('{%- 원시 %}', 'ko')).toBe('{%- raw %}')
12891289
})
@@ -1338,8 +1338,8 @@ describe('correctTranslatedContentStrings', () => {
13381338
})
13391339

13401340
test('fixes 주석 끝 → endnote', () => {
1341-
expect(fix('{% 주석 끝 %}', 'ko')).toBe('{% endnote %}')
1342-
expect(fix('{%- 주석 끝 %}', 'ko')).toBe('{%- endnote %}')
1341+
expect(fix('{% 주석 끝 %}', 'ko')).toBe('')
1342+
expect(fix('{%- 주석 끝 %}', 'ko')).toBe('')
13431343
})
13441344

13451345
test('fixes capitalized Variables → data variables', () => {
@@ -1405,15 +1405,15 @@ describe('correctTranslatedContentStrings', () => {
14051405
})
14061406

14071407
test('fixes translated block tags', () => {
1408-
expect(fix('{% Hinweis %}', 'de')).toBe('{% note %}')
1409-
expect(fix('{%- Hinweis %}', 'de')).toBe('{%- note %}')
1410-
expect(fix('{%- Hinweis -%}', 'de')).toBe('{%- note -%}')
1411-
expect(fix('{% Warnung %}', 'de')).toBe('{% warning %}')
1412-
expect(fix('{%- Warnung %}', 'de')).toBe('{%- warning %}')
1413-
expect(fix('{%- Warnung -%}', 'de')).toBe('{%- warning -%}')
1414-
expect(fix('{% Tipp %}', 'de')).toBe('{% tip %}')
1415-
expect(fix('{%- Tipp %}', 'de')).toBe('{%- tip %}')
1416-
expect(fix('{%- Tipp -%}', 'de')).toBe('{%- tip -%}')
1408+
expect(fix('{% Hinweis %}', 'de')).toBe('')
1409+
expect(fix('{%- Hinweis %}', 'de')).toBe('')
1410+
expect(fix('{%- Hinweis -%}', 'de')).toBe('')
1411+
expect(fix('{% Warnung %}', 'de')).toBe('')
1412+
expect(fix('{%- Warnung %}', 'de')).toBe('')
1413+
expect(fix('{%- Warnung -%}', 'de')).toBe('')
1414+
expect(fix('{% Tipp %}', 'de')).toBe('')
1415+
expect(fix('{%- Tipp %}', 'de')).toBe('')
1416+
expect(fix('{%- Tipp -%}', 'de')).toBe('')
14171417
})
14181418

14191419
test('fixes capitalized Codespaces platform tag', () => {
@@ -1515,8 +1515,8 @@ describe('correctTranslatedContentStrings', () => {
15151515
})
15161516

15171517
test('fixes Endnotiz → endnote', () => {
1518-
expect(fix('{% Endnotiz %}', 'de')).toBe('{% endnote %}')
1519-
expect(fix('{%- Endnotiz %}', 'de')).toBe('{%- endnote %}')
1518+
expect(fix('{% Endnotiz %}', 'de')).toBe('')
1519+
expect(fix('{%- Endnotiz %}', 'de')).toBe('')
15201520
})
15211521

15221522
test('fixes endifen → endif (via generic)', () => {
@@ -3010,4 +3010,38 @@ Para más información, consulta "[AUTOTITLE](/path)".
30103010
expect(output).toContain('GitHub нижнего колонтитула, для всех пользователей')
30113011
})
30123012
})
3013+
3014+
describe('universal: strips legacy {% note/warning/tip/danger %} tags', () => {
3015+
test('strips a simple flush-left {% note %} block', () => {
3016+
const broken = '{% note %}\n\n**Note:** Some note text.\n\n{% endnote %}\n'
3017+
expect(fix(broken, 'es')).toBe('**Note:** Some note text.\n')
3018+
})
3019+
3020+
test('strips {% warning %}, {% tip %}, and {% danger %} tags', () => {
3021+
expect(fix('{% warning %}\nBe careful.\n{% endwarning %}', 'ja')).toBe('Be careful.')
3022+
expect(fix('{% tip %}\nHelpful hint.\n{% endtip %}', 'de')).toBe('Helpful hint.')
3023+
expect(fix('{% danger %}\nDangerous.\n{% enddanger %}', 'fr')).toBe('Dangerous.')
3024+
})
3025+
3026+
test('strips a {% note %} block indented inside a list item', () => {
3027+
const broken = '- Item text.\n\n {% note %}\n\n Nested note text.\n\n {% endnote %}\n'
3028+
expect(fix(broken, 'pt')).toBe('- Item text.\n\n Nested note text.\n')
3029+
})
3030+
3031+
test('leaves already-converted GFM alerts unchanged', () => {
3032+
const correct = '> [!NOTE]\n> Already converted note.'
3033+
expect(fix(correct, 'ko')).toBe(correct)
3034+
})
3035+
3036+
test('strips tags that share a line with other Liquid tags', () => {
3037+
const broken = '{% ifversion fpt %} {% note %}\n\nText.\n\n{% endnote %} {% endif %}'
3038+
expect(fix(broken, 'zh')).toBe('{% ifversion fpt %}\n\nText.\n\n{% endif %}')
3039+
})
3040+
3041+
test('leaves legacy tags inside fenced code blocks and inline code alone', () => {
3042+
const example = '```\n{% note %}\nExample.\n{% endnote %}\n```\n'
3043+
expect(fix(example, 'es')).toBe(example)
3044+
expect(fix('Use `{% note %}` here.', 'es')).toBe('Use `{% note %}` here.')
3045+
})
3046+
})
30133047
})

0 commit comments

Comments
 (0)