German back office: hotel modules, reports, dashboard and menu names
The German language pack leaves the Webkul hotel modules (Hotel Reservation System, Book Now, refunds, rooms, settings), reports, dashboard widgets, cron manager, channel manager, reviews and 31 menu names in English. - docker/find-missing-admin-translations.php: scans module admin controllers/classes/templates and core admin pages for keys without a German text - docker/translations-de-admin.php: 1361 translations (formal "Sie", shared glossary, placeholders/markup verified automatically) - docker/translations-de-tabs.php: menu names (Hotelverwaltung, Zimmer buchen, Zimmerstatus, Erstattungsanträge, Zeitgesteuerte Aufgaben ...) - apply-translations.php now also writes back-office module texts, core admin texts and menu names; module texts the core admin pack already translates are reused from it (modules do not fall back to the admin pack at runtime). Result locally: 0 untranslated strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,38 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Writes missing front-office translations into modules/<module>/translations/<iso>.php.
|
||||
* Usage: docker compose exec -u www-data app php docker/apply-translations.php [iso]
|
||||
* Reads the dictionary docker/translations-<iso>-front.php (English => target language)
|
||||
* and only fills keys that are missing; existing translations are never overwritten.
|
||||
* Fills translation gaps the official language pack leaves (shop, back office, menu names).
|
||||
* docker compose exec -u www-data app php docker/apply-translations.php [iso]
|
||||
*
|
||||
* Dictionaries (English => target language), only missing keys are written, existing
|
||||
* translations are never overwritten:
|
||||
* docker/translations-<iso>-front.php shop texts (scanner: find-missing-translations.php)
|
||||
* docker/translations-<iso>-admin.php back-office texts (scanner: find-missing-admin-translations.php)
|
||||
* docker/translations-<iso>-tabs.php menu names (tab class => name), English names only
|
||||
* Module texts go to modules/<module>/translations/<iso>.php, core admin texts to
|
||||
* translations/<iso>/admin.php (both git-ignored upstream: re-run after every install).
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
exit;
|
||||
}
|
||||
$iso = isset($argv[1]) ? $argv[1] : 'de';
|
||||
$root = realpath(__DIR__.'/..');
|
||||
$dict = require __DIR__.'/translations-'.$iso.'-front.php';
|
||||
|
||||
$json = shell_exec('php '.escapeshellarg(__DIR__.'/find-missing-translations.php').' '.escapeshellarg($iso).' 2>/dev/null');
|
||||
$missing = json_decode($json, true);
|
||||
if (!is_array($missing)) {
|
||||
fwrite(STDERR, "Could not read missing translations\n");
|
||||
exit(1);
|
||||
function loadDict($file)
|
||||
{
|
||||
return is_file($file) ? require $file : array();
|
||||
}
|
||||
|
||||
$added = 0;
|
||||
$untranslated = array();
|
||||
foreach ($missing['modules'] as $mod => $entries) {
|
||||
$lines = array();
|
||||
foreach ($entries as $key => $english) {
|
||||
if (!isset($dict[$english])) {
|
||||
$untranslated[$mod][] = $english;
|
||||
continue;
|
||||
}
|
||||
$lines[] = '$_MODULE['.var_export($key, true).'] = '.var_export($dict[$english], true).';';
|
||||
}
|
||||
if (!$lines) {
|
||||
continue;
|
||||
function scan($script, $iso)
|
||||
{
|
||||
$json = shell_exec('php '.escapeshellarg(__DIR__.'/'.$script).' '.escapeshellarg($iso).' 2>/dev/null');
|
||||
$data = json_decode((string)$json, true);
|
||||
if (!is_array($data)) {
|
||||
fwrite(STDERR, "Could not run $script\n");
|
||||
exit(1);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/** Append $_MODULE lines before a trailing "return $_MODULE;" if the pack file has one. */
|
||||
function appendModuleLines($root, $mod, $iso, array $lines)
|
||||
{
|
||||
$file = $root.'/modules/'.$mod.'/translations/'.$iso.'.php';
|
||||
if (!is_file($file)) {
|
||||
@mkdir(dirname($file), 0755, true);
|
||||
@@ -42,7 +45,6 @@ foreach ($missing['modules'] as $mod => $entries) {
|
||||
if (substr($content, -2) === '?>') {
|
||||
$content = rtrim(substr($content, 0, -2));
|
||||
}
|
||||
// Some language-pack files end with "return $_MODULE;" - new lines must go before it.
|
||||
$hasReturn = (bool)preg_match('/\n\s*return \$_MODULE;\s*$/', $content);
|
||||
if ($hasReturn) {
|
||||
$content = rtrim(preg_replace('/\n\s*return \$_MODULE;\s*$/', '', $content));
|
||||
@@ -52,11 +54,40 @@ foreach ($missing['modules'] as $mod => $entries) {
|
||||
$content .= "\nreturn \$_MODULE;\n";
|
||||
}
|
||||
file_put_contents($file, $content);
|
||||
}
|
||||
|
||||
function applyModules($root, $iso, array $missing, array $dict, &$untranslated)
|
||||
{
|
||||
$added = 0;
|
||||
foreach ($missing as $mod => $entries) {
|
||||
$lines = array();
|
||||
foreach ($entries as $key => $english) {
|
||||
if (!isset($dict[$english]) || !is_string($dict[$english])) {
|
||||
$untranslated[] = $english;
|
||||
continue;
|
||||
}
|
||||
$lines[] = '$_MODULE['.var_export($key, true).'] = '.var_export($dict[$english], true).';';
|
||||
}
|
||||
if ($lines) {
|
||||
appendModuleLines($root, $mod, $iso, $lines);
|
||||
$added += count($lines);
|
||||
echo sprintf(" %-26s +%d\n", $mod, count($lines));
|
||||
}
|
||||
}
|
||||
return $added;
|
||||
}
|
||||
|
||||
$untranslated = array();
|
||||
|
||||
echo "Shop and module templates\n";
|
||||
$front = loadDict(__DIR__.'/translations-'.$iso.'-front.php');
|
||||
$admin = loadDict(__DIR__.'/translations-'.$iso.'-admin.php');
|
||||
$missing = scan('find-missing-translations.php', $iso);
|
||||
// the template scanner also finds dashboard/report modules: their texts live in the admin dictionary
|
||||
$added = applyModules($root, $iso, $missing['modules'], $front + $admin, $untranslated);
|
||||
|
||||
// Theme strings the language pack "translates" with English text: replace those values.
|
||||
$themeFixes = isset($dict['__theme_value_fixes']) ? $dict['__theme_value_fixes'] : array();
|
||||
$themeFixes = isset($front['__theme_value_fixes']) ? $front['__theme_value_fixes'] : array();
|
||||
$themeFile = $root.'/themes/hotel-reservation-theme/lang/'.$iso.'.php';
|
||||
if ($themeFixes && is_file($themeFile)) {
|
||||
$c = file_get_contents($themeFile);
|
||||
@@ -69,6 +100,68 @@ if ($themeFixes && is_file($themeFile)) {
|
||||
echo " theme lang fixes $n\n";
|
||||
}
|
||||
|
||||
if ($admin) {
|
||||
echo "Back office\n";
|
||||
$missingAdmin = scan('find-missing-admin-translations.php', $iso);
|
||||
// texts the core admin pack already translates (same English text): reuse its German value
|
||||
$_LANGADM = array();
|
||||
if (is_file($root.'/translations/'.$iso.'/admin.php')) {
|
||||
include $root.'/translations/'.$iso.'/admin.php';
|
||||
}
|
||||
$byMd5 = array();
|
||||
foreach ($_LANGADM as $k => $v) {
|
||||
if ($v !== '') {
|
||||
$byMd5[substr($k, -32)] = $v;
|
||||
}
|
||||
}
|
||||
$packDict = array();
|
||||
foreach ($missingAdmin['modules'] as $entries) {
|
||||
foreach ($entries as $english) {
|
||||
if (!isset($admin[$english]) && isset($byMd5[md5($english)])) {
|
||||
$packDict[$english] = $byMd5[md5($english)];
|
||||
}
|
||||
}
|
||||
}
|
||||
$added += applyModules($root, $iso, $missingAdmin['modules'], $admin + $packDict, $untranslated);
|
||||
$lines = array();
|
||||
foreach ($missingAdmin['core'] as $key => $english) {
|
||||
if (isset($admin[$english])) {
|
||||
$lines[] = '$_LANGADM['.var_export($key, true).'] = '.var_export($admin[$english], true).';';
|
||||
} else {
|
||||
$untranslated[] = $english;
|
||||
}
|
||||
}
|
||||
if ($lines) {
|
||||
$file = $root.'/translations/'.$iso.'/admin.php';
|
||||
$content = is_file($file) ? rtrim(file_get_contents($file)) : "<?php\n\nglobal \$_LANGADM;\n\$_LANGADM = array();";
|
||||
$content = preg_replace('/\?>\s*$/', '', $content);
|
||||
file_put_contents($file, rtrim($content)."\n\n// Added by docker/apply-translations.php\n".implode("\n", $lines)."\n");
|
||||
$added += count($lines);
|
||||
echo sprintf(" %-26s +%d\n", 'core admin', count($lines));
|
||||
}
|
||||
}
|
||||
|
||||
$tabs = loadDict(__DIR__.'/translations-'.$iso.'-tabs.php');
|
||||
if ($tabs) {
|
||||
echo "Menu\n";
|
||||
define('_PS_ADMIN_DIR_', __DIR__);
|
||||
require_once $root.'/config/config.inc.php';
|
||||
$idLang = (int)Language::getIdByIso($iso);
|
||||
$n = 0;
|
||||
foreach ($tabs as $class => $name) {
|
||||
$idTab = (int)Tab::getIdFromClassName($class);
|
||||
if (!$idTab || !$idLang) {
|
||||
continue;
|
||||
}
|
||||
$current = (string)Db::getInstance()->getValue('SELECT `name` FROM `'._DB_PREFIX_.'tab_lang` WHERE `id_tab` = '.$idTab.' AND `id_lang` = '.$idLang);
|
||||
// only replace names that are still the English default (leave own renames alone)
|
||||
if ($current !== $name && preg_match('/^[\x20-\x7E]*$/', $current) && !preg_match('/(ung|en|e)$/', $current)) {
|
||||
Db::getInstance()->update('tab_lang', array('name' => pSQL($name)), '`id_tab` = '.$idTab.' AND `id_lang` = '.$idLang);
|
||||
$n++;
|
||||
}
|
||||
}
|
||||
echo " menu names $n\n";
|
||||
}
|
||||
|
||||
echo "Added $added translations.\n";
|
||||
$skip = array_sum(array_map('count', $untranslated));
|
||||
echo "Not in dictionary: $skip (mostly back-office modules)\n";
|
||||
echo 'Not in dictionary: '.count(array_unique($untranslated))."\n";
|
||||
|
||||
134
docker/find-missing-admin-translations.php
Normal file
134
docker/find-missing-admin-translations.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
/**
|
||||
* Lists back-office strings without a German translation (iso arg, default de).
|
||||
* docker compose ... exec -u www-data app php docker/find-missing-admin-translations.php de > missing.json
|
||||
* Covers the hotel modules' admin controllers/classes/templates and the core admin pages
|
||||
* the reception works with (orders, customers, carts, guest registration).
|
||||
* Output: {"modules": {mod: {key: english}}, "core": {key: english}}
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
exit;
|
||||
}
|
||||
$iso = isset($argv[1]) ? $argv[1] : 'de';
|
||||
$root = realpath(__DIR__.'/..');
|
||||
|
||||
$modules = array('hotelreservationsystem', 'qlohotelreview', 'qlocmconnector', 'wkroomsearchblock', 'blocknavigationmenu', 'qlocrontaskmanager');
|
||||
$coreControllers = array('AdminOrders', 'AdminCustomers', 'AdminCarts', 'AdminGuestRegistration', 'AdminAddresses', 'AdminDashboard', 'AdminLogin');
|
||||
$coreTplDirs = array('orders', 'customers', 'carts', 'guest_registration', 'addresses', 'dashboard', 'login');
|
||||
|
||||
$rePhp = '/->l\(\s*\'((?:[^\'\\\\]|\\\\.)*)\'(?:\s*,\s*\'([^\']*)\')?/';
|
||||
$reTpl = '/\{l\s+s=(?:\'((?:[^\'\\\\]|\\\\.)*)\'|"((?:[^"\\\\]|\\\\.)*)")(?:[^}]*?\smod=[\'"]([\w-]+)[\'"])?[^}]*\}/';
|
||||
|
||||
function files($dir, $ext)
|
||||
{
|
||||
$out = array();
|
||||
if (!is_dir($dir)) {
|
||||
return $out;
|
||||
}
|
||||
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($it as $f) {
|
||||
if (substr($f, -strlen($ext)) === $ext) {
|
||||
$out[] = (string)$f;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
$out = array('modules' => array(), 'core' => array());
|
||||
|
||||
// core admin pack: module strings fall back to any admin translation with the same md5
|
||||
$_LANGADM = array();
|
||||
if (is_file("$root/translations/$iso/admin.php")) {
|
||||
include "$root/translations/$iso/admin.php";
|
||||
}
|
||||
$haveMd5 = array();
|
||||
foreach ($_LANGADM as $k => $v) {
|
||||
if ($v !== '') {
|
||||
$haveMd5[substr($k, -32)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- modules
|
||||
foreach ($modules as $mod) {
|
||||
$_MODULE = array();
|
||||
foreach (array("$root/modules/$mod/translations/$iso.php", "$root/themes/hotel-reservation-theme/modules/$mod/translations/$iso.php") as $f) {
|
||||
if (is_file($f)) {
|
||||
include $f;
|
||||
}
|
||||
}
|
||||
$have = $_MODULE;
|
||||
$add = function ($source, $string) use (&$out, $have, $mod, $haveMd5) {
|
||||
$key = strtolower('<{'.$mod.'}prestashop>'.$source).'_'.md5($string);
|
||||
// module strings do NOT fall back to the admin pack at runtime: report them all
|
||||
if (empty($have[$key])) {
|
||||
$out['modules'][$mod][$key] = $string;
|
||||
}
|
||||
};
|
||||
foreach (files("$root/modules/$mod", '.php') as $f) {
|
||||
if (strpos($f, '/translations/') !== false || strpos($f, '/vendor/') !== false) {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match_all($rePhp, file_get_contents($f), $m, PREG_SET_ORDER)) {
|
||||
continue;
|
||||
}
|
||||
$base = basename($f, '.php');
|
||||
foreach ($m as $x) {
|
||||
$string = stripslashes($x[1]);
|
||||
if (!empty($x[2])) {
|
||||
$source = $x[2];
|
||||
} elseif (preg_match('/Controller$/', $base) && strpos($f, '/controllers/admin/') !== false) {
|
||||
$source = $base; // ModuleAdminController -> "<class>controller"
|
||||
} elseif (strpos($f, '/controllers/') !== false) {
|
||||
$source = preg_replace('/\.php$/', '', basename($f)); // front controller file name
|
||||
} else {
|
||||
$source = $mod;
|
||||
}
|
||||
$add($source, $string);
|
||||
}
|
||||
}
|
||||
foreach (files("$root/modules/$mod/views/templates/admin", '.tpl') as $f) {
|
||||
if (!preg_match_all($reTpl, file_get_contents($f), $m, PREG_SET_ORDER)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($m as $x) {
|
||||
$string = stripslashes($x[1] !== '' ? $x[1] : $x[2]);
|
||||
$add(basename($f, '.tpl'), $string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- core admin
|
||||
$addCore = function ($class, $string) use (&$out, $_LANGADM, $haveMd5) {
|
||||
$md5 = md5($string);
|
||||
if (empty($_LANGADM[$class.$md5]) && empty($haveMd5[$md5])) {
|
||||
$out['core'][$class.$md5] = $string;
|
||||
}
|
||||
};
|
||||
foreach ($coreControllers as $c) {
|
||||
$f = "$root/controllers/admin/{$c}Controller.php";
|
||||
if (is_file($f) && preg_match_all($rePhp, file_get_contents($f), $m, PREG_SET_ORDER)) {
|
||||
foreach ($m as $x) {
|
||||
$addCore($c, stripslashes($x[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($coreTplDirs as $i => $dir) {
|
||||
$class = isset($coreControllers[$i]) ? $coreControllers[$i] : 'AdminTab';
|
||||
foreach (files("$root/admin-dev/themes/default/template/controllers/$dir", '.tpl') as $f) {
|
||||
if (preg_match_all($reTpl, file_get_contents($f), $m, PREG_SET_ORDER)) {
|
||||
foreach ($m as $x) {
|
||||
if (!empty($x[3])) {
|
||||
continue; // module string inside a core template
|
||||
}
|
||||
$addCore($class, stripslashes($x[1] !== '' ? $x[1] : $x[2]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$counts = array('core' => count($out['core']));
|
||||
foreach ($out['modules'] as $mod => $s) {
|
||||
$counts[$mod] = count($s);
|
||||
}
|
||||
fwrite(STDERR, json_encode($counts).PHP_EOL);
|
||||
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
1369
docker/translations-de-admin.php
Normal file
1369
docker/translations-de-admin.php
Normal file
File diff suppressed because it is too large
Load Diff
39
docker/translations-de-tabs.php
Normal file
39
docker/translations-de-tabs.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* German back-office menu names (tab class => name) for entries the language pack leaves
|
||||
* in English. Applied by docker/apply-translations.php (only replaces English names).
|
||||
*/
|
||||
return array(
|
||||
'AdminHotelReservationSystemManagement' => 'Hotelverwaltung',
|
||||
'AdminHotelRoomsBooking' => 'Zimmer buchen',
|
||||
'AdminAddHotel' => 'Hotel verwalten',
|
||||
'AdminHotelPropertyTypes' => 'Unterkunftsarten',
|
||||
'AdminHotelAmenities' => 'Ausstattung',
|
||||
'AdminOrderRefundRules' => 'Stornoregeln',
|
||||
'AdminOrderRefundRequests' => 'Erstattungsanträge',
|
||||
'AdminHotelConfigurationSetting' => 'Einstellungen',
|
||||
'AdminHotelGeneralSettings' => 'Allgemeine Hoteleinstellungen',
|
||||
'AdminHotelFeaturePricesSettings' => 'Preisregeln',
|
||||
'AdminHotelHeaderImage' => 'Titelbilder',
|
||||
'AdminBookingDocument' => 'Buchungsdokumente',
|
||||
'AdminHotelImageCategory' => 'Bildkategorien',
|
||||
'AdminRoomStatuses' => 'Zimmerstatus',
|
||||
'AdminHotelRoomModuleSetting' => 'Zimmeranzeige auf der Startseite',
|
||||
'AdminAboutHotelBlockSetting' => 'Hotelbeschreibung',
|
||||
'AdminFeaturesModuleSetting' => 'Ausstattung auf der Startseite',
|
||||
'AdminTestimonialsModuleSetting' => 'Gästestimmen',
|
||||
'AdminParentHotelReview' => 'Hotelbewertungen',
|
||||
'AdminHotelReviewHotelReview' => 'Bewertungen',
|
||||
'AdminHotelReviewCategory' => 'Einstellungen',
|
||||
'AdminFooterPaymentBlockSetting' => 'Zahlungsarten im Fußbereich',
|
||||
'AdminCustomNavigationLinkSetting' => 'Navigationslinks',
|
||||
'AdminDashgoals' => 'Ziele',
|
||||
'AdminCronTaskManager' => 'Zeitgesteuerte Aufgaben',
|
||||
'AdminCronTaskLogs' => 'Protokoll zeitgesteuerter Aufgaben',
|
||||
'AdminQloappsChannelManagerConnector' => 'Channel Manager',
|
||||
'AdminGoogleHotelParent' => 'Google Hotel',
|
||||
'AdminGoogleHotelConfig' => 'Einstellungen',
|
||||
'AdminGoogleHotelLogs' => 'API-Protokoll',
|
||||
'AdminModulesCatalog' => 'Modulkatalog',
|
||||
'AdminNewsletter' => 'Einstellungen',
|
||||
);
|
||||
Reference in New Issue
Block a user