Fix double-escaped quote regex in translation catalogs (Behat 4 BC break) - #64
YoannChabert wants to merge 2 commits into
Conversation
MinkContext's step definitions are declared as PHP single-quoted string attributes, e.g. '/^(?:|I )press "(?P<button>(?:[^"]|\\")*)"$/'. PHP decodes \\" to a single backslash followed by a quote before Behat uses that decoded string as the translation msgid. The i18n/*.xliff catalogs are raw XML CDATA, not PHP string literals, so they go through no such decoding. Every <source> entry that used \\" therefore held a different byte sequence (backslash-backslash-quote) than the actual msgid Behat looks up (backslash-quote), causing an exact-match failure in the translator for every step whose regex contains an escaped-quote alternative — the large majority of MinkContext's string-argument steps (press, follow, fill in, select, check, uncheck, attach file, should (not) see, field should (not) contain, etc.). Because the msgid never matched, Behat silently fell back to untranslated matching for these steps in every non-English locale, so translated feature files could not use them at all. Fix: strip the extra backslash so <source> exactly mirrors the PHP-decoded regex, and apply the same correction to <target> where present, across all 17 translation catalogs (all locales but English, which needs no catalog). Verified byte-for-byte against the actual attribute string via reflection for the i-press-button unit.
features/search.fr.feature mirrors search.feature but is written entirely in French (Gherkin keywords via '# language: fr', step text via the fr.xliff translations), exercising four of the steps affected by the previous double-escaping bug: "je suis sur", "je remplis ... avec ...", "je presse", "je devrais voir". Confirmed the regression this guards against: running this feature with a --dry-run against the pre-fix fr.xliff reports 6 of 8 steps as undefined (every affected step except "je suis sur", whose regex doesn't use the escaped-quote alternative). With the fix, all 8 steps are recognized, and the full suite passes against Wikipedia the same way the existing English suite does.
|
Added `features/search.fr.feature` — a French translation of the existing `search.feature`, exercising 4 of the affected steps (`je suis sur`, `je remplis ... avec ...`, `je presse`, `je devrais voir`). Confirmed the regression this guards against by running it with `--dry-run` against the pre-fix `fr.xliff`: (`je suis sur` is the one step here whose regex doesn't use the escaped-quote alternative, so it was already matching.) With the fix, all 8 steps are recognized and the suite passes for real against Wikipedia, the same way the existing English `search.feature` does in CI: |
|
Thank you for your contribution @YoannChabert. I've approved this PR. Any idea why inability to write |
|
It is directly tied to the Behat 4 migration specifically commit c0d7a83 ("Fix CI shell safety, widen Symfony constraints, add Behat 4 step attributes", 2026-06-12). Before that commit, steps were declared only via That commit added So depending on which Behat version reads the context:
So this almost certainly sat latent for years while everyone was on Behat 3, and only became an actual, user-facing regression once a project upgraded to Behat 4. |
|
Ah-ha. Then then it's BC break for Behat 4. Worth noting in the PR title/description and commit message so that release notes stay useful. |
|
Updated 👍🏼 |
|
Ran into this issue as well – but I think we need to fix it at the other end, see #65. The regexes have changed and are now too permissive. We need to fix the regex back to the original values, not make the translations follow the change. |
|
Agreed, #65 is the more fundamental fix, it addresses the actual regex over-permissiveness (which is a real matching bug, not just a translation lookup issue), and as a side effect it requires zero changes to any of the 18 I'll defer to #65 here, happy to close this PR once #65 is merged, since the catalog changes in this PR would become redundant (and would need reverting) once the regex itself is fixed upstream. |
Summary
Every non-English translation catalog under
i18n/*.xliff(17 files:cs,da,de,es,fr,hu,id,it,ja,nl,pl,pt,pt-br,ro,ru,sk,sv,zh-CN) has a<source>(and, where the same construct appears,<target>) entry that is a byte-for-byte mismatch with the actual regex Behat looks up as a translation key for every step whose pattern contains an escaped-quote alternative such as(?:[^"]|\\")*. In practice this is most ofMinkContext's string-argument steps:press,follow,fill in,select,check,uncheck,attach the file,should (not) see,the field should (not) contain,the element should (not) contain, etc.Root cause
MinkContext's step patterns are declared as PHP single-quoted string literals, e.g.:PHP decodes the
\\"escape sequence in a single-quoted string to a single backslash followed by a literal quote before Behat ever sees the string. That decoded string (one backslash and one quote) is what Behat's translator uses as the lookup key (msgid) against each catalog's<source>.The
i18n/*.xlifffiles are raw XMLCDATA, not PHP string literals, so there is no such decoding step for their contents. Every affected<source>still contained the literal two-backslash sequence\\"(i.e. an actual backslash character followed by a quote character, four bytes wide:\,\,"), rather than the one-backslash sequence Behat actually looks for.Because Symfony's translator does an exact match on the
msgid, the lookup silently fails for all of these entries. The practical effect: any step that uses this escaped-quote pattern is never translated in any locale. Writing a# language: xxfeature file and using, say, "I press" translated into that language for these steps doesn't work, with no error, just a step definition that doesn't match.Why this is a Behat 4 BC break, not a long-standing bug
This bug is new-ish and specifically tied to Behat 4 support, not something that's been silently broken since the extension's creation:
c0d7a83("Fix CI shell safety, widen Symfony constraints, add Behat 4 step attributes"),MinkContextdeclared its steps only via@When-style docblock annotations, read viagetDocComment()reflection, i.e. raw comment bytes, with no PHP string-escape decoding. The docblock's\\"was byte-identical to the XLIFF<source>'s\\", so translation matching worked fine under Behat 3 (which reads docblocks).#[\Behat\Step\When(...)]PHP 8 attributes alongside the docblocks because Behat 4 ignores docblock annotations and requires attributes. But inside an attribute, the regex is a real single-quoted PHP string literal, which PHP does decode (\\"→\") before Behat ever sees it.\\"XLIFF catalog, so translated steps silently fall back to untranslated matching.So this is effectively a backwards-compatibility break introduced by Behat 4 support: projects that translate their feature files and upgrade from Behat 3 to Behat 4 (without any other change) will see their translated steps stop matching, with no error, just steps reported as undefined. It's not a pre-existing per-language quirk and not related to
symfony/translationitself (the package doesn't even depend on it).Fix
Strip the redundant backslash so each
<source>(and<target>, where it also used the pattern) exactly mirrors the string PHP actually decodes from the corresponding attribute inMinkContext.php. Applied identically across all 17 locale files, this is the same bug repeated in every catalog, not a per-language issue.Verified byte-for-byte for a sample unit (
i-press-button) viaReflectionClass/ReflectionAttributeagainst the liveMinkContext::pressButtonstep definition (both hex-dumped to 43 identical bytes) rather than by inspection alone.Test plan
hex_dump/ReflectionAttributecomparison that the fixedfr.xliff<source>fori-press-buttonis now byte-identical to the actual PHP-decoded regex used byMinkContext::pressButton.features/search.fr.feature, a French translation of the existingsearch.feature, exercising 4 of the affected steps. Confirmed it reproduces the regression against the pre-fix catalog (6 of 8 steps undefined) and passes fully with the fix (8/8 steps, full suite green against Wikipedia).