From 089f85fd48c3a85e82ddfb25382d0a6857643b7e Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:07:44 -0700 Subject: [PATCH] fix(translations): add new Liquid corruption fixes for fr/es/pt + generic endprompt (#61615) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 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 --- .../lib/correct-translation-content.ts | 42 ++++++++++++ .../tests/correct-translation-content.ts | 68 +++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts index a933f4951dfa..5fb13fb1e567 100644 --- a/src/languages/lib/correct-translation-content.ts +++ b/src/languages/lib/correct-translation-content.ts @@ -177,6 +177,9 @@ export function correctTranslatedContentStrings( // `{% icono "X" ... %}` — "icono" = "icon" = octicon content = content.replaceAll('{% icono ', '{% octicon ') content = content.replaceAll('{%- icono ', '{%- octicon ') + // `{% alto "X" ... %}` — "alto" used as alias for octicon (observed in billing reusable) + content = content.replaceAll('{% alto ', '{% octicon ') + content = content.replaceAll('{%- alto ', '{%- octicon ') // `{% octicon "bombilla" %}` — Spanish "bombilla" = "light-bulb" (translated octicon name) content = content.replaceAll('{% octicon "bombilla"', '{% octicon "light-bulb"') content = content.replaceAll('{%- octicon "bombilla"', '{%- octicon "light-bulb"') @@ -643,6 +646,12 @@ export function correctTranslatedContentStrings( /\{%(-?)\s*(fpt|ghec|ghes)\s+ifversion\s*%\}/g, '{%$1 ifversion $2 %}', ) + // Multi-plan word-order swap: `{% ghes ifversion ou ghec %}` → `{% ifversion ghes or ghec %}` + // Handles the combination of word-order inversion AND Portuguese "ou" for "or". + content = content.replace( + /\{%(-?)\s*(fpt|ghec|ghes|ghae)\s+ifversion\s+(?:ou|or)\s+(fpt|ghec|ghes|ghae)\s*(-?)%\}/g, + '{%$1 ifversion $2 or $3 $4%}', + ) // With extra "de" word: `{% ghes de ifversion %}` → `{% ifversion ghes %}` content = content.replace( /\{%(-?)\s*(fpt|ghec|ghes)\s+de\s+ifversion\s*%\}/g, @@ -1329,6 +1338,30 @@ export function correctTranslatedContentStrings( /\{%(-?)\s*des(?:\s+[^{}%\n]+?)?\s+variables\.([A-Za-z0-9._-]+)(\s*-?%\})/g, '{%$1 data variables.$2$3', ) + // `{% modules réutilisables.X %}` — French "modules réutilisables" = "reusable modules" + // used in place of `{% data reusables.X %}`. + content = content.replaceAll('{% modules réutilisables.', '{% data reusables.') + content = content.replaceAll('{%- modules réutilisables.', '{%- data reusables.') + // `{% flux de travail variables.X %}` — French "flux de travail" = "workflow" was + // mistakenly substituted for the "data" keyword in data variable references. + content = content.replaceAll('{% flux de travail variables.', '{% data variables.') + content = content.replaceAll('{%- flux de travail variables.', '{%- data variables.') + // `{% invite %}` / `{%- invite %}` — French "invite" = "prompt"; translator used the + // French word as the tag opener for the `{% prompt %}` block tag. + content = content.replaceAll('{% invite %}', '{% prompt %}') + content = content.replaceAll('{%- invite %}', '{%- prompt %}') + content = content.replaceAll('{% invite -%}', '{% prompt -%}') + content = content.replaceAll('{%- invite -%}', '{%- prompt -%}') + // `{% collaborateurs invités ifversion %}` — French translation of + // `{% ifversion guest-collaborators %}` with both word-order swap and full translation. + content = content.replaceAll( + '{% collaborateurs invités ifversion %}', + '{% ifversion guest-collaborators %}', + ) + content = content.replaceAll( + '{%- collaborateurs invités ifversion %}', + '{%- ifversion guest-collaborators %}', + ) } if (context.code === 'ko') { @@ -1678,6 +1711,15 @@ export function correctTranslatedContentStrings( // --- Generic fixes (all languages) --- + // [copilot/tutorials/learn-a-new-language] The `${numCats}` JS template literal inside + // a backtick code span confused translators and caused the closing `{% endprompt %}` to + // be dropped from the JavaScript-conditional-example prompt block. Fix by appending + // `{% endprompt %}` to the line that contains the distinctive code. + content = content.replace( + /(\* \{%[- ]prompt [-]?%\}(?![^\n]*\{%-?\s*endprompt\s*-?%\})[^\n]*'cat is' : 'cats are'\} hungry\.[^\n]*(?:\?|?)[^\n]*)(\n|$)/g, + '$1{% endprompt %}$2', + ) + // Inside ANY Liquid tag `{% ... %}` (including `{% octicon ... %}`, // `{% data ... %}`, `{% assign ... %}` etc.), normalize typographic // quotation marks back to ASCII straight quotes. Translators diff --git a/src/languages/tests/correct-translation-content.ts b/src/languages/tests/correct-translation-content.ts index 8cf46829143e..cea31c79d81a 100644 --- a/src/languages/tests/correct-translation-content.ts +++ b/src/languages/tests/correct-translation-content.ts @@ -124,6 +124,13 @@ describe('correctTranslatedContentStrings', () => { expect(fix('{%- icono "check" %}', 'es')).toBe('{%- octicon "check" %}') }) + test('fixes alto → octicon', () => { + expect(fix('{% alto "link-external":16 aria-label="link-external" %}', 'es')).toBe( + '{% octicon "link-external":16 aria-label="link-external" %}', + ) + expect(fix('{%- alto "check" %}', 'es')).toBe('{%- octicon "check" %}') + }) + test('fixes octicon "bombilla" → octicon "light-bulb"', () => { expect(fix('{% octicon "bombilla" aria-label="The light-bulb icon" %}', 'es')).toBe( '{% octicon "light-bulb" aria-label="The light-bulb icon" %}', @@ -424,6 +431,14 @@ describe('correctTranslatedContentStrings', () => { expect(fix('{% if condition ou other %}', 'pt')).toBe('{% if condition or other %}') }) + test('fixes multi-plan word-order swap with ou (ghes ifversion ou ghec)', () => { + // `{% ghes ifversion ou ghec %}` — word-order swap + Portuguese "ou" for "or" + expect(fix('{% ghes ifversion ou ghec %}', 'pt')).toBe('{% ifversion ghes or ghec %}') + expect(fix('{%- ghes ifversion ou ghec %}', 'pt')).toBe('{%- ifversion ghes or ghec %}') + expect(fix('{% fpt ifversion ou ghec %}', 'pt')).toBe('{% ifversion fpt or ghec %}') + expect(fix('{% ghec ifversion ou ghes %}', 'pt')).toBe('{% ifversion ghec or ghes %}') + }) + test('fixes fully translated reutilizáveis reusables path', () => { // `reutilizáveis` is Portuguese for "reusables" expect(fix('{% dados reutilizáveis.repositórios.reaction_list %}', 'pt')).toBe( @@ -956,6 +971,41 @@ describe('correctTranslatedContentStrings', () => { fix('{% données réutilisables propriétés-personnalisées valeurs-requises %}', 'fr'), ).toBe('{% data reusables.organizations.custom-properties-required-values %}') }) + + test('fixes modules réutilisables → data reusables', () => { + expect(fix('{% modules réutilisables.enterprise_migrations.ready-to-import %}', 'fr')).toBe( + '{% data reusables.enterprise_migrations.ready-to-import %}', + ) + expect(fix('{%- modules réutilisables.foo.bar %}', 'fr')).toBe( + '{%- data reusables.foo.bar %}', + ) + }) + + test('fixes flux de travail variables → data variables', () => { + // `{% flux de travail variables.` — French "flux de travail" (workflow) mistakenly + // used as the Liquid tag name instead of "data". + expect(fix('{% flux de travail variables.product.prodname_actions %}', 'fr')).toBe( + '{% data variables.product.prodname_actions %}', + ) + expect(fix('{%- flux de travail variables.copilot.foo %}', 'fr')).toBe( + '{%- data variables.copilot.foo %}', + ) + }) + + test('fixes invite → prompt', () => { + expect(fix('{% invite %}', 'fr')).toBe('{% prompt %}') + expect(fix('{%- invite %}', 'fr')).toBe('{%- prompt %}') + expect(fix('{% invite -%}', 'fr')).toBe('{% prompt -%}') + }) + + test('fixes collaborateurs invités ifversion → ifversion guest-collaborators', () => { + expect(fix('{% collaborateurs invités ifversion %}', 'fr')).toBe( + '{% ifversion guest-collaborators %}', + ) + expect(fix('{%- collaborateurs invités ifversion %}', 'fr')).toBe( + '{%- ifversion guest-collaborators %}', + ) + }) }) // ─── KOREAN (ko) ────────────────────────────────────────────────── @@ -1606,6 +1656,24 @@ describe('correctTranslatedContentStrings', () => { ) }) + test('fixes missing endprompt on the JS-numCats line (all translation languages)', () => { + // The `${}` template literal inside a backtick confused translators and they dropped + // `{% endprompt %}` from the line. Fix is applied universally across all languages. + const input = + "* {% prompt %}How do I write `The ${'cat is' : 'cats are'} hungry.`?{% endprompt %}\n" + + "* {% prompt %}In JS I'd write: `The ${'cat is' : 'cats are'} hungry.`. ¿How in NEW-LANGUAGE?\n" + + '* {% prompt %}Next question?{% endprompt %}' + const output = + "* {% prompt %}How do I write `The ${'cat is' : 'cats are'} hungry.`?{% endprompt %}\n" + + "* {% prompt %}In JS I'd write: `The ${'cat is' : 'cats are'} hungry.`. ¿How in NEW-LANGUAGE?{% endprompt %}\n" + + '* {% prompt %}Next question?{% endprompt %}' + expect(fix(input, 'es')).toBe(output) + expect(fix(input, 'pt')).toBe(output) + expect(fix(input, 'zh')).toBe(output) + expect(fix(input, 'de')).toBe(output) + expect(fix(input, 'fr')).toBe(output) + }) + test('recovers linebreaks from English', () => { const en = '{% endif %}\nSome text' const translated = '{% endif %} Some text'