SL-387 Add German and French module translations - #347
Open
TLabutis wants to merge 2 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 throughTranslate::getModuleTranslation(), which reads onlytranslations/<iso>.phpand builds each key from the specifier passed at the call site plus an md5 of the source string. Most calls pass that specifier asself::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.phphandles both directions:extractpreserves 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 dropstests/on release.Code changes beyond the dictionaries
LegacyTranslatorpassed no specifier for Googlepay, keying it apart from the other payment names. It now passesself::FILE_NAMElike its 15 siblings.Failed to load settings data.fallback.settings_react.tplnow supplies it through a data attribute, andmain.tsxwrites it viatextContentinstead ofinnerHTML.One gotcha worth knowing
getModuleTranslation()runs every hit throughhtmlspecialchars(), so a translated value containing a quote or an ampersand renders as a literal"/&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()passedgetTabs()a single string fromModule::l(), and core'sModuleTabRegistercopies a plain string into everyid_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 callstabName(), 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 becausegetTabNames()falls back to the first entry for any language the map does not cover.tabName()readstranslations/<iso>.phpdirectly instead of callingModule::l()once per language.getModuleTranslation()merges every dictionary it loads into one global$_MODULESunder identical keys, so the file loaded last wins, anden.phpandlt.phpare empty in this repo, which would hand those languages another language's wording.saferpayofficial_2_1_0_translate_tabs()rewritesps_tab_langon 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:
Translate::getModuleTranslation()at runtime: 352/352 German, 352/352 French, one PHP process per language.Payment authorized by Saferpay, order page panel translated.getTabs(), the 2.1.0 upgrade backfill against a live database, and a realprestashop:module uninstallplusinstallthrough 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.Payment authorized by Saferpay.php-cs-fixer --dry-runclean repo-wide,tsc --noEmitclean.Known limitations, by design
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.phpforwards raw gateway exception messages, which no translation file can reach.UNIONPAYlabel comes straight from the Saferpay API in every language. Pre-existing.