Skip to content

SL-387 Add German and French module translations - #347

Open
TLabutis wants to merge 2 commits into
feature/react-admin-settingsfrom
SL-387-german-french-module-translations
Open

SL-387 Add German and French module translations#347
TLabutis wants to merge 2 commits into
feature/react-admin-settingsfrom
SL-387-german-french-module-translations

Conversation

@TLabutis

@TLabutis TLabutis commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What

Ships German and French translations for every translatable string in the module: 352 dictionary rows covering 332 unique wordings, across the React back office settings app, all admin controllers and templates, the front office checkout, and the order page panel.

Targets feature/react-admin-settings, not master, because roughly half the strings only exist on the React settings branch.

Why the files are generated

Module::l() resolves through Translate::getModuleTranslation(), which reads only translations/<iso>.php and builds each key from the specifier passed at the call site plus an md5 of the source string. Most calls pass that specifier as self::FILE_NAME, a literal, or __CLASS__, and templates key by their own basename, so PrestaShop's own extractor cannot produce these keys.

tests/tools/translation-dictionary.php handles both directions:

php tests/tools/translation-dictionary.php extract    # code -> wording CSV
php tests/tools/translation-dictionary.php build de   # CSV -> translations/de.php

extract preserves already-filled translation columns, so future wording corrections are a CSV edit plus a rebuild. The tool and the CSV are stripped from the merchant zip, which drops tests/ on release.

Code changes beyond the dictionaries

  • LegacyTranslator passed no specifier for Googlepay, keying it apart from the other payment names. It now passes self::FILE_NAME like its 15 siblings.
  • The React entry point hardcoded its Failed to load settings data. fallback. settings_react.tpl now supplies it through a data attribute, and main.tsx writes it via textContent instead of innerHTML.

One gotcha worth knowing

getModuleTranslation() runs every hit through htmlspecialchars(), so a translated value containing a quote or an ampersand renders as a literal &quot; / &amp; in React labels. German uses low and high quotation marks instead, and both languages spell the conjunction out. A quote count of zero in the dictionaries guards this.

Admin menu tab names

The Saferpay entries in the back office menu ("Settings", "Payments", "Order", "Logs") stayed English on every shop, including fresh installs on a German or French shop.

A tab name is a stored row per language. AbstractInstaller::tabs() passed getTabs() a single string from Module::l(), and core's ModuleTabRegister copies a plain string into every id_lang, so the wording of the employee running the install was written into all languages and no dictionary shipped later could reach those rows.

tabs() now calls tabName(), which returns an iso-keyed map. ModuleTabRegister::getTabNames() has accepted that shape since 1.7, spreading it across the installed languages. English is first in the map because getTabNames() falls back to the first entry for any language the map does not cover.

tabName() reads translations/<iso>.php directly instead of calling Module::l() once per language. getModuleTranslation() merges every dictionary it loads into one global $_MODULES under identical keys, so the file loaded last wins, and en.php and lt.php are empty in this repo, which would hand those languages another language's wording.

saferpayofficial_2_1_0_translate_tabs() rewrites ps_tab_lang on existing installs, and only while a row still holds the English wording the module wrote, so a rename made in Advanced Parameters survives.

Testing

Verified on PrestaShop 1.7.6.1:

  • Every CSV row asserted against Translate::getModuleTranslation() at runtime: 352/352 German, 352/352 French, one PHP process per language.
  • German back office: all five settings tabs translated and still fitting one row, no literal HTML entities on any tab.
  • German front office: 12 translated payment options, full sandbox payment through 3DS, order created in state Payment authorized by Saferpay, order page panel translated.
  • French spot-check: back office tabs and labels, front office payment options.
  • Menu tab names verified on PrestaShop 9.1 and 1.7.6.1, on all three paths: resolution through getTabs(), the 2.1.0 upgrade backfill against a live database, and a real prestashop:module uninstall plus install through core's own registrar. French reads "Paramètres | Journaux", German "Einstellungen | Protokolle", and a language the module ships no wording for falls back to English.
  • Full sandbox payment through Saferpay Fields on PrestaShop 9.1 in German, order reaching Payment authorized by Saferpay.
  • php-cs-fixer --dry-run clean repo-wide, tsc --noEmit clean.

Known limitations, by design

  • Order state names stay English on every shop, fresh installs included. Installer::createAllOrderStatus() resolves one string and copies it into every language, the same defect the menu tabs had. Left as is deliberately to keep this PR scoped; it needs its own ticket and its own upgrade backfill.
  • notify.php forwards raw gateway exception messages, which no translation file can reach.
  • The UNIONPAY label comes straight from the Saferpay API in every language. Pre-existing.
  • Merchants cannot override the shipped wording from the back office translation page. That was scoped out deliberately; corrections go through the CSV and a module update.

Tadas Labutis added 2 commits August 27, 2026 15:24
Module::l() resolves through Translate::getModuleTranslation(), which reads
only translations/<iso>.php and keys every entry by the specifier passed at
the call site plus an md5 of the source string. Most calls pass that
specifier as self::FILE_NAME, a literal, or __CLASS__, and templates key by
their own basename, so PrestaShop's extractor cannot produce these keys and
the dictionaries had to be generated.

tests/tools/translation-dictionary.php extracts every wording together with
its resolved specifier into tests/tools/wording/SL-387-wording-de-fr.csv,
then builds translations/de.php and translations/fr.php from the reviewed
de and fr columns. Both files carry all 352 rows. The tool and the CSV stay
out of the merchant zip, which drops tests/ on release.

getModuleTranslation() passes every hit through htmlspecialchars(), so a
translated value holding a quote or an ampersand would render as a literal
entity in React labels. German uses low and high quotation marks instead,
and both languages spell the conjunction out.

LegacyTranslator passed no specifier for Googlepay, keying it apart from the
other payment names; it now passes self::FILE_NAME like its siblings. The
React entry point hardcoded its "Failed to load settings data." fallback,
which settings_react.tpl now supplies through a data attribute so it can be
translated.

Order state names on shops that already have the module installed stay
English because they are stored rows, and notify.php forwards raw gateway
exception messages. Both are left as they are on purpose.
A tab name is a stored row per language, but tabs() passed getTabs() a single
string produced by Module::l(), so core's ModuleTabRegister copied the wording
of the employee who ran the install into every id_lang. The dictionaries shipped
in this version never reached those rows.

tabs() now calls tabName(), which returns an iso-keyed map that core's
ModuleTabRegister::getTabNames() spreads across languages. English is first in
the map because getTabNames() falls back to the first entry for any language the
map does not cover.

tabName() reads translations/<iso>.php directly rather than calling Module::l()
per language. getModuleTranslation() merges every dictionary into one global
$_MODULES under identical keys, so the file loaded last wins, and en.php and
lt.php are empty here, which would hand those languages another language's
wording.

saferpayofficial_2_1_0_translate_tabs() rewrites ps_tab_lang on existing
installs, and only while a row still holds the English wording the module wrote,
so a rename made in Advanced Parameters survives.

Order state names have the same defect and are deliberately left in English.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant