From 732fc373344145863e73270838018aed577990a4 Mon Sep 17 00:00:00 2001 From: Florian Guimier Date: Thu, 27 Aug 2026 10:08:00 +0200 Subject: [PATCH 1/2] feat(translations): add the Spanish catalogue The bundle shipped English and French only, so a Spanish-speaking user read the English bounce reason or invalid-address message. The new catalogue mirrors the two existing ones, key for key, including the two entries the English catalogue deliberately leaves blank. A test now pins the catalogues against each other, so a missing locale or an untranslated message fails the suite. Nothing failed before: an absent translation silently falls back to the English wording, which is exactly why the gap went unnoticed. Refs IT9PE1-30149 Co-Authored-By: Claude Opus 5 (1M context) --- tests/TranslationCatalogueTest.php | 107 ++++++++++++++++++ ...ssoconnect_smtp_toolbox+intl-icu.es_ES.yml | 18 +++ 2 files changed, 125 insertions(+) create mode 100644 tests/TranslationCatalogueTest.php create mode 100644 translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml diff --git a/tests/TranslationCatalogueTest.php b/tests/TranslationCatalogueTest.php new file mode 100644 index 0000000..1c3b46f --- /dev/null +++ b/tests/TranslationCatalogueTest.php @@ -0,0 +1,107 @@ + */ + public static function provideLocales(): iterable + { + foreach (self::EXPECTED_LOCALES as $locale) { + yield $locale => ['locale' => $locale]; + } + } + + #[DataProvider('provideLocales')] + public function testCatalogueExists(string $locale): void + { + self::assertFileExists(self::cataloguePath($locale)); + } + + #[DataProvider('provideLocales')] + public function testCatalogueCoversEveryReferenceMessage(string $locale): void + { + self::assertSame( + array_keys($this->messages(self::REFERENCE_LOCALE)), + array_keys($this->messages($locale)), + sprintf('The %s catalogue does not cover the same messages as the %s one.', $locale, self::REFERENCE_LOCALE) + ); + } + + #[DataProvider('provideLocales')] + public function testEveryMessageIsTranslated(string $locale): void + { + $messages = $this->messages($locale); + + foreach ($this->messages(self::REFERENCE_LOCALE) as $key => $reference) { + // Some messages are deliberately blank, so emptiness is only a gap where the reference has wording + if ('' === trim($reference)) { + self::assertSame('', trim($messages[$key] ?? ''), sprintf('Message "%s" should stay blank.', $key)); + continue; + } + + self::assertNotSame( + '', + trim($messages[$key] ?? ''), + sprintf('Message "%s" has no %s translation.', $key, $locale) + ); + } + } + + private static function cataloguePath(string $locale): string + { + return sprintf('%s/%s.%s.yml', self::TRANSLATIONS_DIR, self::DOMAIN, $locale); + } + + /** @return array */ + private function messages(string $locale): array + { + $catalogue = Yaml::parseFile(self::cataloguePath($locale)); + self::assertIsArray($catalogue, sprintf('The %s catalogue is not a YAML mapping.', $locale)); + + $messages = self::flatten($catalogue); + ksort($messages); + + return $messages; + } + + /** + * @param array $catalogue + * + * @return array + */ + private static function flatten(array $catalogue, string $prefix = ''): array + { + $messages = []; + + foreach ($catalogue as $key => $value) { + $path = '' === $prefix ? (string) $key : $prefix . '.' . $key; + + if (is_array($value)) { + $messages += self::flatten($value, $path); + continue; + } + + $messages[$path] = (string) $value; + } + + return $messages; + } +} diff --git a/translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml b/translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml new file mode 100644 index 0000000..5d5f1e6 --- /dev/null +++ b/translations/assoconnect_smtp_toolbox+intl-icu.es_ES.yml @@ -0,0 +1,18 @@ +bounce: + reason: + inactive: "El buzón del destinatario ha sido desactivado." + quota: "El buzón del destinatario está lleno y no puede aceptar nuevos correos electrónicos por el momento" + none: "No hemos podido interpretar el motivo del fallo de envío comunicado por el servidor de correo" + other: "" + spam: "El sistema de correo del destinatario ha marcado el mensaje como spam y se niega a entregarlo." + unknown_user: "Esta dirección de correo electrónico no existe." + tips: + contact: "Informe al destinatario de este problema para que pueda resolverse." + 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 }}" + unknown_user: > + La dirección {{ email }} no existe en {{ domain }} : + {{ smtpResponse }} From 84495b81a11364f65023fbd08f4938f9735e107e Mon Sep 17 00:00:00 2001 From: Florian Guimier Date: Thu, 27 Aug 2026 10:23:29 +0200 Subject: [PATCH 2/2] test(translations): guard against a catalogue left in the reference wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the review gate warnings. The emptiness check let a catalogue copied from the reference locale and never translated through, which is the very shape of the defect this test was added for. Non-reference locales now also have to differ from the reference wording, message by message — the deliberately blank entries keep their own branch. The class docblock stated a fallback to English that is the consuming application's configuration, not this bundle's, so it is now the invariant the test actually enforces. Refs IT9PE1-30149 Co-Authored-By: Claude Opus 5 (1M context) --- tests/TranslationCatalogueTest.php | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/TranslationCatalogueTest.php b/tests/TranslationCatalogueTest.php index 1c3b46f..fa4c1b8 100644 --- a/tests/TranslationCatalogueTest.php +++ b/tests/TranslationCatalogueTest.php @@ -9,8 +9,8 @@ use Symfony\Component\Yaml\Yaml; /** - * Nothing fails at runtime when a catalogue is missing or incomplete: the message simply falls back to - * its English wording, so the gap only shows up in front of an end user. + * Every catalogue must cover the reference locale's messages, each in a wording of its own. Neither a + * missing catalogue nor an untranslated message fails at runtime, so this test is what guards them. */ class TranslationCatalogueTest extends TestCase { @@ -51,16 +51,25 @@ public function testEveryMessageIsTranslated(string $locale): void $messages = $this->messages($locale); foreach ($this->messages(self::REFERENCE_LOCALE) as $key => $reference) { + $message = trim($messages[$key] ?? ''); + // Some messages are deliberately blank, so emptiness is only a gap where the reference has wording if ('' === trim($reference)) { - self::assertSame('', trim($messages[$key] ?? ''), sprintf('Message "%s" should stay blank.', $key)); + self::assertSame('', $message, sprintf('Message "%s" should stay blank.', $key)); + continue; + } + + self::assertNotSame('', $message, sprintf('Message "%s" has no %s translation.', $key, $locale)); + + if (self::REFERENCE_LOCALE === $locale) { continue; } + // A catalogue copied from the reference locale and left untranslated clears the emptiness check self::assertNotSame( - '', - trim($messages[$key] ?? ''), - sprintf('Message "%s" has no %s translation.', $key, $locale) + trim($reference), + $message, + sprintf('Message "%s" still reads as its %s wording.', $key, self::REFERENCE_LOCALE) ); } }