diff --git a/.claude/ARCHITECTURE.md b/.claude/ARCHITECTURE.md index 9ef26c3..9d978a2 100644 --- a/.claude/ARCHITECTURE.md +++ b/.claude/ARCHITECTURE.md @@ -79,3 +79,4 @@ Contexte donné aux sessions Claude Code sur ce dépôt : - `npm run lint:amo` (`addons-linter` sur le build Firefox) ne peut pas descendre à 0 warning `UNSAFE_VAR_ASSIGNMENT` : contrairement à `eslint-plugin-no-unsanitized`, `addons-linter` ne reconnaît pas `DOMPurify.sanitize()` comme un assainissement valide et flague même le code interne de DOMPurify une fois bundlé. Attendu, à mentionner aux reviewers AMO si besoin — pas un bug à corriger ici. - `createDefaultSettings()` (`utils/settings.js`) initialise les réglages non personnalisés à `''` (chaîne vide), pas `undefined`. Un paramètre de fonction avec valeur par défaut (`function f(x = 'défaut')`) ne se déclenche que sur `undefined` — passer une valeur venant des réglages sans vérifier ce cas produit une valeur par défaut jamais appliquée, silencieusement (ex: CSS généré avec une valeur vide, ignoré par le navigateur sans erreur). Utiliser `||=` ou un coalescing explicite dans le corps de la fonction plutôt que compter sur les paramètres par défaut. - Les termes du glossaire officiel sont toujours stockés/comparés en minuscules (`.toLowerCase()` à l'ingestion du CSV, `entrypoints/spte.content.js`). Toute regex qui les matche contre le texte original (casse quelconque : majuscule initiale, tout en majuscules…) doit donc porter le flag `i`, sinon un mot capitalisé n'est pas repéré. Vérifié à jour sur les deux constructions concernées : `rgxBadWords` (`utils/rules.js:91`, déjà `gmi`) et `getGlossaryRegex()` qui la recombine (`entrypoints/spte.content.js:459`, `gmi` depuis la correction de l'issue #81/PR #82 — avant, le flag `i` manquait sur cette recombinaison précise). Si une future regex glossaire est ajoutée ailleurs, lui appliquer le même flag. +- Les fonctions de remplacement passées à `String.prototype.replace()` dans `checkTranslation()` (`entrypoints/spte.content.js`) reçoivent un argument par groupe capturant de la regex, avant la position et la chaîne d'origine (puis un objet si la regex a des groupes nommés). Lire ces deux valeurs à une position fixe (`(match, offset, string)`) ne fonctionne que si aucune règle n'a de groupe capturant : `rgxPeriod` (`utils/rules.js`) en contient un, ce qui interrompait l'analyse de la page dès qu'une traduction contenait « ... » en 3.1.0. Les lire à partir de la fin de la liste des arguments, comme le fait désormais `checkTranslation()`, ou n'utiliser que des groupes non capturants `(?:…)` dans les règles. diff --git a/entrypoints/spte.content.js b/entrypoints/spte.content.js index bf16237..991f86c 100644 --- a/entrypoints/spte.content.js +++ b/entrypoints/spte.content.js @@ -81,7 +81,14 @@ export function checkTranslation(ctx, translation, oldStatus, newStatus) { let textWithoutTags = text.replaceAll(/<.*?(? { + text = text.replace(rule.regex, (string, ...replaceArgs) => { + // String.prototype.replace() transmet un argument par groupe capturant avant la position et la chaîne + // d'origine, puis un objet de groupes nommés le cas échéant (rgxPeriod contient un groupe capturant). + // La position et la chaîne sont donc lues à partir de la fin de la liste des arguments. + const positionalArgs = typeof replaceArgs.at(-1) === 'object' ? replaceArgs.slice(0, -1) : replaceArgs; + const offset = /** @type {number} */ (positionalArgs.at(-2)); + const fullString = /** @type {string} */ (positionalArgs.at(-1)); + // Un rule précédent dans cette même passe peut avoir injecté un (attributs entre // guillemets doubles) : ignorer tout match à l'intérieur de ce balisage déjà posé, sinon il est // corrompu par un second imbriqué dans ses propres attributs. diff --git a/tests/spte.content.test.js b/tests/spte.content.test.js index bd586d5..65ea232 100644 --- a/tests/spte.content.test.js +++ b/tests/spte.content.test.js @@ -345,4 +345,40 @@ describe('checkTranslation', () => { checkTranslation({ projectName: 'Plugin' }, translated, 'untranslated', 'current'); expect(badWords.counter).toBe(0); }); + + // Le groupe capturant de rgxPeriod décale les arguments transmis par replace() (régression de la 3.1.0). + it('détecte trois points ASCII sans lever d\'exception', () => { + const translated = document.querySelector('#preview-1-1 .translation-text'); + translated.innerHTML = 'Chargement...'; + const asciiEllipsis = rules.find((rule) => rule.id === 'asciiEllipsis'); + const ellipsis = rules.find((rule) => rule.id === 'ellipsis'); + + expect(() => checkTranslation({ projectName: '' }, translated, 'untranslated', 'current')).not.toThrow(); + + expect(asciiEllipsis.counter).toBe(1); + expect(ellipsis.counter).toBe(0); + expect(document.querySelector('#preview-1-1 .translation-text').textContent).toBe('Chargement...'); + }); + + // Issue #29 : l'infobulle doit désigner le caractère à utiliser, et non un problème d'espacement. + it('affiche pour trois points ASCII un message qui désigne le caractère points de suspension', () => { + const translated = document.querySelector('#preview-1-1 .translation-text'); + translated.innerHTML = 'Chargement...'; + + checkTranslation({ projectName: '' }, translated, 'untranslated', 'current'); + + const warning = document.querySelector('#preview-1-1 .sp-warning--char'); + expect(warning.getAttribute('data-message')).toContain('Trois points au lieu du caractère points de suspension (…)'); + expect(warning.getAttribute('data-message')).not.toContain('Précédé par une espace'); + }); + + it('détecte un point suivi d\'une espace finale sans lever d\'exception', () => { + const translated = document.querySelector('#preview-1-1 .translation-text'); + translated.innerHTML = 'Fin de phrase. '; + const period = rules.find((rule) => rule.id === 'period'); + + expect(() => checkTranslation({ projectName: '' }, translated, 'untranslated', 'current')).not.toThrow(); + + expect(period.counter).toBe(1); + }); }); diff --git a/utils/regex.test.js b/utils/regex.test.js index b6f851f..6de5754 100644 --- a/utils/regex.test.js +++ b/utils/regex.test.js @@ -9,6 +9,7 @@ import { rgxOpenParenthesis, rgxOpenBrace, rgxEllipsis, + rgxAsciiEllipsis, rgxPeriod, rgxComma, rgxCloseHook, @@ -276,10 +277,8 @@ describe('rgxEllipsis', () => { it('ignore des points de suspension suivis d\'une espace', () => { expect(matches(rgxEllipsis, 'et… puis')).toEqual([]); }); - // Issue #29 : trois points ASCII successifs devraient être remplacés par le caractère - // unique « … » et doivent donc être détectés au même titre. - it('détecte trois points ASCII successifs', () => { - expect(matches(rgxEllipsis, 'et...puis')).toEqual(['...']); + it('ne détecte pas trois points ASCII, traités par rgxAsciiEllipsis', () => { + expect(matches(rgxEllipsis, 'et...puis')).toEqual([]); }); it('détecte des points de suspension précédés d’une espace', () => { expect(matches(rgxEllipsis, 'mot …!')).toHaveLength(1); @@ -297,6 +296,22 @@ describe('rgxEllipsis', () => { }); }); +// Issue #29 : trois points ASCII successifs doivent être remplacés par le caractère unique « … ». +describe('rgxAsciiEllipsis', () => { + it('détecte trois points ASCII successifs', () => { + expect(matches(rgxAsciiEllipsis, 'et...puis')).toEqual(['...']); + }); + it('détecte trois points ASCII en fin de chaîne', () => { + expect(matches(rgxAsciiEllipsis, 'Chargement...')).toEqual(['...']); + }); + it('ne détecte pas le caractère points de suspension', () => { + expect(matches(rgxAsciiEllipsis, 'Chargement…')).toEqual([]); + }); + it('ne détecte pas les deux points d\'un chemin relatif', () => { + expect(matches(rgxAsciiEllipsis, 'Copiez le fichier dans ../wp-content/')).toEqual([]); + }); +}); + describe('rgxPeriod', () => { // La branche censée détecter "mot.mot" (point collé entre deux minuscules) ne se déclenche // jamais en pratique — son lookbehind négatif matche toujours une chaîne vide. Comportement diff --git a/utils/rules.js b/utils/rules.js index c6560c7..be8fc01 100644 --- a/utils/rules.js +++ b/utils/rules.js @@ -122,7 +122,11 @@ export const rgxOpenParenthesis = new RegExp(`(?|
|
)\\${da export const rgxOpenBrace = new RegExp(`(?