Skip to content
Closed
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
1 change: 1 addition & 0 deletions .claude/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
9 changes: 8 additions & 1 deletion entrypoints/spte.content.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,14 @@ export function checkTranslation(ctx, translation, oldStatus, newStatus) {

let textWithoutTags = text.replaceAll(/&lt;.*?(?<!\/)&gt;/gmi, '');
for (const rule of rules) {
text = text.replace(rule.regex, (string, offset, fullString) => {
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 <span ...> (attributs entre
// guillemets doubles) : ignorer tout match à l'intérieur de ce balisage déjà posé, sinon il est
// corrompu par un second <span> imbriqué dans ses propres attributs.
Expand Down
36 changes: 36 additions & 0 deletions tests/spte.content.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
23 changes: 19 additions & 4 deletions utils/regex.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
rgxOpenParenthesis,
rgxOpenBrace,
rgxEllipsis,
rgxAsciiEllipsis,
rgxPeriod,
rgxComma,
rgxCloseHook,
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
16 changes: 15 additions & 1 deletion utils/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ export const rgxOpenParenthesis = new RegExp(`(?<![ ]|^|<br>|<br/>|<br />)\\${da
export const rgxOpenBrace = new RegExp(`(?<! |\\${data.openBrace}|^)\\${data.openBrace}(?!\\${data.openBrace})|\\${data.openBrace}(?=[ |\u00a0])(?![ \u00a0][a-zA-Z0-9]+\\${data.closeBrace})`, 'gmi');

// https://github.com/Association-WPFR/SPTE/wiki/rgxEllipsis
export const rgxEllipsis = new RegExp(`(?<=[ |\u00a0])\\${data.ellipsis}|\\${data.ellipsis}(?=[a-zÀ-ú0-9]| $|\u00a0$)|\\.\\.\\.`, 'gmi');
export const rgxEllipsis = new RegExp(`(?<=[ |\u00a0])\\${data.ellipsis}|\\${data.ellipsis}(?=[a-zÀ-ú0-9]| $|\u00a0$)`, 'gmi');

// Trois points ASCII employés à la place du caractère points de suspension (U+2026). Règle distincte de
// rgxEllipsis, qui porte sur l'espacement : le message affiché doit désigner le caractère à utiliser. Voir issue #29.
export const rgxAsciiEllipsis = /\.\.\./gm;

// https://github.com/Association-WPFR/SPTE/wiki/rgxPeriod
// La 2e alternative (point collé entre 2 mots, ex: "mot.mot") ne se déclenche jamais : son lookbehind
Expand Down Expand Up @@ -300,6 +304,16 @@ export const rules = [
counter: 0,
regex: rgxEllipsis,
},
{
id: 'asciiEllipsis',
name: 'trois points',
title: charTitle,
message: 'Trois points au lieu du caractère points de suspension (…)',
severity: 'toVerify',
cssClass: charClass,
counter: 0,
regex: rgxAsciiEllipsis,
},
{
id: 'period',
name: 'point',
Expand Down
Loading