From 79fc9e2ebf7b1002a3cb9a9a1bab2b418bb52a14 Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Mon, 24 Aug 2026 12:32:03 -0400 Subject: [PATCH] fix(i18n): MediaField hardcoded English (leaked on 3 admin screens) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared media-picker helper rendered 'Choose media' and 'Clear' as literals, so they stayed English in every locale. It's now on the Identity, CMS page-editor and SEO screens, so the leak is visible three times over. Routes through the 't' view helper when a view is attached (inheriting its source-locale fallback), then the registry translator, then the English literal — fail-soft because the helper is also instantiated bare, with no view and no translator, in ViewHelperTest. Also gives the preview real alt text instead of alt="": it conveys WHICH media is selected, so it isn't decorative. The non-image tile gets a title + visually-hidden label and an aria-hidden icon. 4 keys x 6 locales (147 -> 151, parity held). Co-Authored-By: Claude Opus 4.8 --- core/languages/de/core.php | 6 ++++ core/languages/en/core.php | 6 ++++ core/languages/es/core.php | 6 ++++ core/languages/fr/core.php | 6 ++++ core/languages/hi/core.php | 6 ++++ core/languages/pt/core.php | 6 ++++ library/Tiger/View/Helper/MediaField.php | 45 +++++++++++++++++++++--- 7 files changed, 77 insertions(+), 4 deletions(-) diff --git a/core/languages/de/core.php b/core/languages/de/core.php index 8c38560a..d6b8252c 100644 --- a/core/languages/de/core.php +++ b/core/languages/de/core.php @@ -187,4 +187,10 @@ 'core.datatable.paginate_next' => 'Weiter', 'core.datatable.paginate_prev' => 'Zurück', 'core.nav.modules_manage' => 'Verwalten', + + // Media picker field (Tiger_View_Helper_MediaField) — shared by the Identity, CMS and SEO screens. + 'core.media.field.choose' => 'Medium auswählen', + 'core.media.field.clear' => 'Entfernen', + 'core.media.field.preview_alt' => 'Vorschau des ausgewählten Mediums', + 'core.media.field.file' => 'Ausgewählte Datei', ]; diff --git a/core/languages/en/core.php b/core/languages/en/core.php index 88866687..488339de 100644 --- a/core/languages/en/core.php +++ b/core/languages/en/core.php @@ -191,4 +191,10 @@ 'core.datatable.paginate_next' => 'Next', 'core.datatable.paginate_prev' => 'Previous', 'core.nav.modules_manage' => 'Manage', + + // Media picker field (Tiger_View_Helper_MediaField) — shared by the Identity, CMS and SEO screens. + 'core.media.field.choose' => 'Choose media', + 'core.media.field.clear' => 'Clear', + 'core.media.field.preview_alt' => 'Selected media preview', + 'core.media.field.file' => 'Selected file', ]; diff --git a/core/languages/es/core.php b/core/languages/es/core.php index 58e4cab5..ec1def2e 100644 --- a/core/languages/es/core.php +++ b/core/languages/es/core.php @@ -187,4 +187,10 @@ 'core.datatable.paginate_next' => 'Siguiente', 'core.datatable.paginate_prev' => 'Anterior', 'core.nav.modules_manage' => 'Gestionar', + + // Media picker field (Tiger_View_Helper_MediaField) — shared by the Identity, CMS and SEO screens. + 'core.media.field.choose' => 'Elegir medio', + 'core.media.field.clear' => 'Quitar', + 'core.media.field.preview_alt' => 'Vista previa del medio seleccionado', + 'core.media.field.file' => 'Archivo seleccionado', ]; diff --git a/core/languages/fr/core.php b/core/languages/fr/core.php index 47d91a40..a6fd8227 100644 --- a/core/languages/fr/core.php +++ b/core/languages/fr/core.php @@ -187,4 +187,10 @@ 'core.datatable.paginate_next' => 'Suivant', 'core.datatable.paginate_prev' => 'Précédent', 'core.nav.modules_manage' => 'Gérer', + + // Media picker field (Tiger_View_Helper_MediaField) — shared by the Identity, CMS and SEO screens. + 'core.media.field.choose' => 'Choisir un média', + 'core.media.field.clear' => 'Retirer', + 'core.media.field.preview_alt' => 'Aperçu du média sélectionné', + 'core.media.field.file' => 'Fichier sélectionné', ]; diff --git a/core/languages/hi/core.php b/core/languages/hi/core.php index 239eeef0..b1d79376 100644 --- a/core/languages/hi/core.php +++ b/core/languages/hi/core.php @@ -187,4 +187,10 @@ 'core.datatable.paginate_next' => 'अगला', 'core.datatable.paginate_prev' => 'पिछला', 'core.nav.modules_manage' => 'प्रबंधित करें', + + // Media picker field (Tiger_View_Helper_MediaField) — shared by the Identity, CMS and SEO screens. + 'core.media.field.choose' => 'मीडिया चुनें', + 'core.media.field.clear' => 'हटाएँ', + 'core.media.field.preview_alt' => 'चयनित मीडिया का प्रीव्यू', + 'core.media.field.file' => 'चयनित फ़ाइल', ]; diff --git a/core/languages/pt/core.php b/core/languages/pt/core.php index 900f47af..b8afb57f 100644 --- a/core/languages/pt/core.php +++ b/core/languages/pt/core.php @@ -187,4 +187,10 @@ 'core.datatable.paginate_next' => 'Próximo', 'core.datatable.paginate_prev' => 'Anterior', 'core.nav.modules_manage' => 'Gerenciar', + + // Media picker field (Tiger_View_Helper_MediaField) — shared by the Identity, CMS and SEO screens. + 'core.media.field.choose' => 'Escolher mídia', + 'core.media.field.clear' => 'Remover', + 'core.media.field.preview_alt' => 'Pré-visualização da mídia selecionada', + 'core.media.field.file' => 'Arquivo selecionado', ]; diff --git a/library/Tiger/View/Helper/MediaField.php b/library/Tiger/View/Helper/MediaField.php index 62d6ce2b..55aae6c0 100644 --- a/library/Tiger/View/Helper/MediaField.php +++ b/library/Tiger/View/Helper/MediaField.php @@ -29,7 +29,8 @@ public function mediaField($name, $value = '', array $options = []) $esc = function ($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }; $kind = (string) ($options['kind'] ?? ''); $multiple = !empty($options['multiple']) ? '1' : '0'; - $label = (string) ($options['label'] ?? 'Choose media'); + // A caller-supplied label is already localized by the view; only the default needs a key. + $label = (string) ($options['label'] ?? $this->_t('core.media.field.choose', 'Choose media')); $id = (string) ($options['id'] ?? $name); $value = (string) $value; $hasVal = trim($value) !== ''; @@ -41,9 +42,14 @@ public function mediaField($name, $value = '', array $options = []) $row = $model->findById($value); if ($row) { $m = $row->toArray(); + // The preview conveys WHICH media is selected, so it carries real alt text rather + // than being marked decorative; the non-image tile gets the same treatment. + $altText = $this->_t('core.media.field.preview_alt', 'Selected media preview'); + $fileText = $this->_t('core.media.field.file', 'Selected file'); $preview = ($m['kind'] === 'image') - ? '' - : ''; + ? '' . $esc($altText) . '' + : '' + . '' . $esc($fileText) . ''; } } @@ -52,7 +58,38 @@ public function mediaField($name, $value = '', array $options = []) . '' . '' - . '' + . '' . ''; } + + /** + * Translate a key, falling back to the English literal. + * + * A view helper has no `$this->t()`, so this routes through the registered `t` helper when a + * view is attached (inheriting its source-locale fallback), then the registry translator, then + * the literal. Fail-soft by design: this helper is also instantiated bare (no view, no + * translator) in tests, and a raw key rendered into a button would be worse than English. + * + * @param string $key the translation key + * @param string $fallback the English literal to use when nothing resolves + * @return string the translated text, or the fallback + */ + protected function _t($key, $fallback) + { + try { + if ($this->view instanceof Zend_View_Interface && method_exists($this->view, 't')) { + $out = (string) $this->view->t($key); + if ($out !== '' && $out !== $key) { return $out; } + } + if (Zend_Registry::isRegistered('Zend_Translate')) { + $tr = Zend_Registry::get('Zend_Translate'); + $out = (string) $tr->translate($key); + if ($out !== '' && $out !== $key) { return $out; } + } + } catch (Throwable $e) { + // fail-open — a head/label lookup must never break a render + } + return $fallback; + } }