From f9f2ed555033def37fbb8a3e75509eb9ffc405af Mon Sep 17 00:00:00 2001 From: Florian Guimier Date: Thu, 27 Aug 2026 10:36:03 +0200 Subject: [PATCH 1/2] fix(translations): render the invalid-address messages as ICU patterns The domain carries the +intl-icu suffix, so every message is an ICU pattern, where `{{ name }}` is a syntax error. The three invalid_address messages threw on render in every locale, English included: Invalid message format (error #65799): pattern syntax error Nothing surfaced it because the only consumer, InvalidAddressTranslatable, has no caller in the backend yet, and the bounce messages that are rendered carry no placeholder. The catalogues now use the ICU `{name}` form and render. Two tests pin it: every message must parse as an ICU pattern for its locale, and must carry the same placeholders as the reference catalogue, so a `{mail}` typo no longer renders literally. ext-intl moves into require-dev, which is what parses those patterns. The stray space before the colon goes with it in en_US and es_ES; French keeps it, its typography wants one. Refs IT9PE1-30149 Co-Authored-By: Claude Opus 5 (1M context) --- composer.json | 1 + tests/TranslationCatalogueTest.php | 50 +++++++++++++++++++ ...ssoconnect_smtp_toolbox+intl-icu.en_US.yml | 8 +-- ...ssoconnect_smtp_toolbox+intl-icu.es_ES.yml | 8 +-- ...ssoconnect_smtp_toolbox+intl-icu.fr_FR.yml | 8 +-- 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index f7c340c..cdbbba5 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "symfony/framework-bundle": "^7.0|^8.0", "symfony/yaml": "^7.0|^8.0", "symfony/translation": "^7.0|^8.0", + "ext-intl": "*", "assoconnect/php-quality-config": "^2.2", "phpstan/phpstan-symfony": "^2" }, diff --git a/tests/TranslationCatalogueTest.php b/tests/TranslationCatalogueTest.php index fa4c1b8..b2555b9 100644 --- a/tests/TranslationCatalogueTest.php +++ b/tests/TranslationCatalogueTest.php @@ -4,6 +4,8 @@ namespace AssoConnect\SmtpToolbox\Tests; +use IntlException; +use MessageFormatter; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Yaml\Yaml; @@ -74,6 +76,44 @@ public function testEveryMessageIsTranslated(string $locale): void } } + /** + * The domain carries the +intl-icu suffix, so every message is an ICU pattern. `{{ name }}` is a + * syntax error there, and the translator throws on render rather than at load time. + */ + #[DataProvider('provideLocales')] + public function testEveryMessageIsAValidIcuPattern(string $locale): void + { + $invalid = []; + + foreach ($this->messages($locale) as $key => $message) { + if ('' === trim($message)) { + continue; + } + + try { + new MessageFormatter($locale, $message); + } catch (IntlException $exception) { + $invalid[] = sprintf('%s (%s)', $key, $exception->getMessage()); + } + } + + self::assertSame([], $invalid, sprintf('Invalid ICU pattern(s) in the %s catalogue.', $locale)); + } + + #[DataProvider('provideLocales')] + public function testEveryMessageCarriesTheReferencePlaceholders(string $locale): void + { + $messages = $this->messages($locale); + + foreach ($this->messages(self::REFERENCE_LOCALE) as $key => $reference) { + self::assertSame( + self::placeholders($reference), + self::placeholders($messages[$key] ?? ''), + sprintf('Message "%s" does not carry the same placeholders in %s.', $key, $locale) + ); + } + } + private static function cataloguePath(string $locale): string { return sprintf('%s/%s.%s.yml', self::TRANSLATIONS_DIR, self::DOMAIN, $locale); @@ -113,4 +153,14 @@ private static function flatten(array $catalogue, string $prefix = ''): array return $messages; } + + /** @return list */ + private static function placeholders(string $message): array + { + preg_match_all('/\{(\w+)\}/', $message, $matches); + $placeholders = array_unique($matches[1]); + sort($placeholders); + + return $placeholders; + } } diff --git a/translations/assoconnect_smtp_toolbox+intl-icu.en_US.yml b/translations/assoconnect_smtp_toolbox+intl-icu.en_US.yml index f8e9778..ddf9b92 100644 --- a/translations/assoconnect_smtp_toolbox+intl-icu.en_US.yml +++ b/translations/assoconnect_smtp_toolbox+intl-icu.en_US.yml @@ -11,8 +11,8 @@ bounce: none: "" review: "Please check the email address for typo." invalid_address: - no_at_symbol: "The email address {{ email }} is missing the @ symbol" - no_mx_servers: "No email servers are set up for {{ domain }}" + no_at_symbol: "The email address {email} is missing the @ symbol" + no_mx_servers: "No email servers are set up for {domain}" unknown_user: > - The email address {{ email }} doesn't exist at {{ domain }} : - {{ smtpResponse }} + The email address {email} doesn't exist at {domain}: + {smtpResponse} diff --git a/translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml b/translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml index 5d5f1e6..ae2fad4 100644 --- a/translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml +++ b/translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml @@ -11,8 +11,8 @@ bounce: none: "" review: "Compruebe si la dirección de correo electrónico contiene alguna errata." invalid_address: - no_at_symbol: "A la dirección {{ email }} le falta el símbolo @" - no_mx_servers: "No hay ningún servidor de correo configurado para {{ domain }}" + no_at_symbol: "A la dirección {email} le falta el símbolo @" + no_mx_servers: "No hay ningún servidor de correo configurado para {domain}" unknown_user: > - La dirección {{ email }} no existe en {{ domain }} : - {{ smtpResponse }} + La dirección {email} no existe en {domain}: + {smtpResponse} diff --git a/translations/assoconnect_smtp_toolbox+intl-icu.fr_FR.yml b/translations/assoconnect_smtp_toolbox+intl-icu.fr_FR.yml index b14b49c..ac91a42 100644 --- a/translations/assoconnect_smtp_toolbox+intl-icu.fr_FR.yml +++ b/translations/assoconnect_smtp_toolbox+intl-icu.fr_FR.yml @@ -11,8 +11,8 @@ bounce: none: "" review: "Vérifiez l'adresse enregistrée : elle contient peut-être une faute de frappe." invalid_address: - no_at_symbol: "L'adresse {{ email }} ne contient pas le signe @" - no_mx_servers: "Aucun serveur email n'est configuré pour le domain {{ domain }}" + no_at_symbol: "L'adresse {email} ne contient pas le signe @" + no_mx_servers: "Aucun serveur email n'est configuré pour le domain {domain}" unknown_user: > - L'adresse {{ email }} n'existe pas chez {{ domain }} : - {{ smtpResponse }} + L'adresse {email} n'existe pas chez {domain} : + {smtpResponse} From 2b902442a3c8c1affa5f7f28a37c2fb4a45b9d69 Mon Sep 17 00:00:00 2001 From: Florian Guimier Date: Thu, 27 Aug 2026 11:10:22 +0200 Subject: [PATCH 2/2] refactor(translations): name the catalogues after the base language The files were named after a country variant, en_US and fr_FR, so they only served those exact locales. Symfony resolves a locale by falling back to its parent, so a base-language name serves every regional variant: en_GB and en_US both reach en. Verified against the consuming application's own configuration, which restricts enabled_locales to ['en_US', 'fr_FR', 'es_ES']: all three still resolve, the restriction does not disable the parent fallback. The backend names its own ~2300 catalogues this way and none with a country code, so the bundle was the outlier. Refs IT9PE1-30149 Co-Authored-By: Claude Opus 5 (1M context) --- tests/TranslationCatalogueTest.php | 4 ++-- ...icu.en_US.yml => assoconnect_smtp_toolbox+intl-icu.en.yml} | 0 ...icu.es_ES.yml => assoconnect_smtp_toolbox+intl-icu.es.yml} | 0 ...icu.fr_FR.yml => assoconnect_smtp_toolbox+intl-icu.fr.yml} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename translations/{assoconnect_smtp_toolbox+intl-icu.en_US.yml => assoconnect_smtp_toolbox+intl-icu.en.yml} (100%) rename translations/{assoconnect_smtp_toolbox+intl-icu.es_ES.yml => assoconnect_smtp_toolbox+intl-icu.es.yml} (100%) rename translations/{assoconnect_smtp_toolbox+intl-icu.fr_FR.yml => assoconnect_smtp_toolbox+intl-icu.fr.yml} (100%) diff --git a/tests/TranslationCatalogueTest.php b/tests/TranslationCatalogueTest.php index b2555b9..378a4de 100644 --- a/tests/TranslationCatalogueTest.php +++ b/tests/TranslationCatalogueTest.php @@ -18,10 +18,10 @@ class TranslationCatalogueTest extends TestCase { private const string TRANSLATIONS_DIR = __DIR__ . '/../translations'; private const string DOMAIN = 'assoconnect_smtp_toolbox+intl-icu'; - private const string REFERENCE_LOCALE = 'en_US'; + private const string REFERENCE_LOCALE = 'en'; /** Every locale the catalogues are expected to cover. */ - private const array EXPECTED_LOCALES = ['en_US', 'fr_FR', 'es_ES']; + private const array EXPECTED_LOCALES = ['en', 'fr', 'es']; /** @return iterable */ public static function provideLocales(): iterable diff --git a/translations/assoconnect_smtp_toolbox+intl-icu.en_US.yml b/translations/assoconnect_smtp_toolbox+intl-icu.en.yml similarity index 100% rename from translations/assoconnect_smtp_toolbox+intl-icu.en_US.yml rename to translations/assoconnect_smtp_toolbox+intl-icu.en.yml diff --git a/translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml b/translations/assoconnect_smtp_toolbox+intl-icu.es.yml similarity index 100% rename from translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml rename to translations/assoconnect_smtp_toolbox+intl-icu.es.yml diff --git a/translations/assoconnect_smtp_toolbox+intl-icu.fr_FR.yml b/translations/assoconnect_smtp_toolbox+intl-icu.fr.yml similarity index 100% rename from translations/assoconnect_smtp_toolbox+intl-icu.fr_FR.yml rename to translations/assoconnect_smtp_toolbox+intl-icu.fr.yml