diff --git a/_backend/Console/Extract.php b/_backend/Console/Extract.php index 2a5873cea5..3d93d1a908 100644 --- a/_backend/Console/Extract.php +++ b/_backend/Console/Extract.php @@ -52,19 +52,47 @@ $isVerbose = $cli->arguments->defined('verbose'); $page = 'index.php'; +$isLayout = false; if ($cli->arguments->defined('page') !== false) { $cliPage = $cli->arguments->get('page'); + $cliPage = preg_replace('/\.(php|md)$/', '', $cliPage); - $pages = array_map(function ($path) use ($root) { - return substr(realpath($path), strlen(realpath($root)) + 1); - }, L10n::pages()); - - if (in_array($cliPage . '.php', $pages)) { - $page = $cliPage . '.php'; - } elseif (in_array($cliPage . '.md', $pages)) { - $page = $cliPage . '.md'; + if ($cliPage === 'layout' || strpos($cliPage, '_templates/') === 0) { + $isLayout = true; + $page = 'layout'; } else { - throw new \Exception("Page '$cliPage' is not part of the website"); + $pages = array_map(function ($path) use ($root) { + $rootPath = str_replace( + '\\', + '/', + substr(realpath($path), strlen($root) + 1) + ); + + if (strpos($rootPath, '_templates/') === 0) { + return 'layout'; + } + + $isMarkdown = substr($path, -3) === '.md'; + $url = substr($rootPath, 0, -3); + + if ($isMarkdown === false) { + $url = substr($url, 0, -1); + } + + return $url; + }, L10n::pages()); + + if (in_array($cliPage, $pages, true) === false) { + throw new \Exception("Page '$cliPage' is not part of the website"); + } + + if (is_file($root . '/' . $cliPage . '.php')) { + $page = $cliPage . '.php'; + } elseif (is_file($root . '/' . $cliPage . '.md')) { + $page = $cliPage . '.md'; + } else { + throw new \Exception("Page '$cliPage' is not part of the website"); + } } } @@ -72,15 +100,26 @@ $cli->whisper("Extracting text from '$page'"); } -$path = $root . '/' . $page; -$isMarkdown = (substr($path, -3) === '.md'); +if ($isLayout) { + $path = null; + $isMarkdown = false; + $currentUrl = 'layout'; +} else { + $path = $root . '/' . $page; + $isMarkdown = (substr($path, -3) === '.md'); + + $currentUrl = substr($page, 0, -3); + if ($isMarkdown === false) { + $currentUrl = substr($currentUrl, 0, -1); + } +} -$currentUrl = substr($page, 0, -3); -if ($isMarkdown === false) { - $currentUrl = substr($currentUrl, 0, -1); +$domain = $currentUrl; +if ($cli->arguments->defined('domain') !== false) { + $domain = $cli->arguments->get('domain'); } -$currentTranslations = $l10n->loadTranslations($currentUrl); +$currentTranslations = $l10n->loadTranslations($domain); if ($currentTranslations === false) { $currentTranslations = array(); } @@ -153,10 +192,14 @@ function translatePHP($path, $domain) global $l10n, $root; chdir($root); - $_SERVER['DOCUMENT_ROOT'] = dirname(__DIR__); + $_SERVER['DOCUMENT_ROOT'] = $root; + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_HOST'] = 'localhost'; $_GET['page'] = $domain; $page['name'] = $domain; + require_once $root . '/_backend/preload.php'; + // Do not start output buffering twice if (defined('HTML_I18N') === false) { define('HTML_I18N', 1); @@ -165,6 +208,7 @@ function translatePHP($path, $domain) } $l10n->setDomain($domain); + ob_start(function ($input) use ($l10n) { $l10n->translateHtml($input, '\App\Console\captureTranslations'); return ''; @@ -177,29 +221,82 @@ function translatePHP($path, $domain) } // Add page title to translations - if (isset($page['title']) !== false) { + if (isset($page['title'])) { captureTranslations($page['title'], $domain, $page['title']); } } +/** + * translateLayout + * Extracts translations from shared templates in their expected context. + * + * @return void + */ +function translateLayout() +{ + global $root, $l10n, $page, $template, $sitewide; -$domain = $currentUrl; -if ($cli->arguments->defined('domain') !== false) { - $domain = $cli->arguments->get('domain'); + chdir($root); + + $_SERVER['DOCUMENT_ROOT'] = $root; + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_HOST'] = 'localhost'; + $_SERVER['HTTP_USER_AGENT'] = 'MSIE 10.0'; + + $_GET['page'] = 'layout'; + + require_once $root . '/_backend/preload.php'; + + $page = array( + 'name' => 'layout', + 'path' => '/', + 'styles' => array(), + 'scripts' => array(), + 'script-plugins' => array(), + ); + + // Avoid requiring JavaScript manifest entries during extraction. + $scriptless = true; + + $l10n->setDomain('layout'); + + if (defined('HTML_I18N') === false) { + define('HTML_I18N', 1); + } + + $initialBufferLevel = ob_get_level(); + + ob_start(function ($input) use ($l10n) { + $l10n->translateHtml($input, '\App\Console\captureTranslations'); + return ''; + }); + + include $template['header']; + include $template['alert']; + include $template['footer']; + + while (ob_get_level() > $initialBufferLevel) { + ob_end_flush(); + } } -if ($isMarkdown) { - translateMarkdown($path, $currentUrl); +if ($isLayout) { + translateLayout(); +} elseif ($isMarkdown) { + translateMarkdown($path, $domain); } else { - translatePHP($path, $currentUrl); + translatePHP($path, $domain); } // Preprocess translations to remove newlines in the keys $processedTranslations = []; foreach ($currentTranslations as $key => $value) { // Remove newlines from the key completely and reduce multiple spaces to a single space - $processedKey = str_replace(["\r\n", "\n"], " ", $key); + $processedKey = str_replace(["\r\n", "\n"], ' ', $key); $processedKey = preg_replace('/\s+/', ' ', $processedKey); $processedTranslations[$processedKey] = $value; } -echo json_encode($processedTranslations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); +echo json_encode( + $processedTranslations, + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES +) . "\n"; diff --git a/_backend/Console/Translation.php b/_backend/Console/Translation.php index c15b9fd3f0..50e409b7e4 100644 --- a/_backend/Console/Translation.php +++ b/_backend/Console/Translation.php @@ -7,21 +7,19 @@ namespace App\Console; -require_once __DIR__ . '/../bootstrap.php'; - use \App\Lib\L10n; use \League\CLImate\CLImate; -$cli = new CLImate(); -$l10n = new L10n('en'); +require_once __DIR__ . '/../bootstrap.php'; +$cli = new CLImate(); $root = realpath(__DIR__ . '/../..'); $cli->arguments->add([ 'language' => [ - 'prefix' => 'l', - 'longPrefix' => 'language', - 'description' => 'Language to extract', + 'prefix' => 'l', + 'longPrefix' => 'language', + 'description' => 'Language to extract', ], 'page' => [ 'prefix' => 'p', @@ -38,8 +36,9 @@ 'longPrefix' => 'help', 'description' => 'Prints a usage statement', 'noValue' => true, - ] + ], ]); + $cli->arguments->parse(); if ($cli->arguments->defined('help')) { @@ -50,14 +49,15 @@ $isVerbose = $cli->arguments->defined('verbose'); $languages = L10n::languageFolders(); + if ($cli->arguments->defined('language') !== false) { $cliLanguage = $cli->arguments->get('language'); - if (in_array($cliLanguage, $languages) === false) { + if (in_array($cliLanguage, $languages, true) === false) { throw new \Exception("Language '$cliLanguage' is not part of the website"); } - $languages = array($cliLanguage); + $languages = [$cliLanguage]; } if ($isVerbose) { @@ -66,24 +66,41 @@ } $pages = array_map(function ($path) use ($root) { - $rootPath = substr(realpath($path), strlen(realpath($root)) + 1); + $rootPath = str_replace( + '\\', + '/', + substr(realpath($path), strlen($root) + 1) + ); + + if (strpos($rootPath, '_templates/') === 0) { + return 'layout'; + } - $isMarkdown = (substr($path, -3) === '.md'); + $isMarkdown = substr($path, -3) === '.md'; $url = substr($rootPath, 0, -3); + if ($isMarkdown === false) { $url = substr($url, 0, -1); } return $url; }, L10n::pages()); + +$pages = array_values(array_unique($pages)); + if ($cli->arguments->defined('page') !== false) { $cliPage = $cli->arguments->get('page'); + $cliPage = preg_replace('/\.(php|md)$/', '', $cliPage); - if (in_array($cliPage, $pages) === false) { + if (strpos($cliPage, '_templates/') === 0) { + $cliPage = 'layout'; + } + + if (in_array($cliPage, $pages, true) === false) { throw new \Exception("Page '$cliPage' is not part of the website"); } - $pages = array($cliPage); + $pages = [$cliPage]; } if ($isVerbose) { @@ -92,75 +109,84 @@ } $progress = $cli->progress(count($languages) * count($pages)); -foreach ($pages as $page) { - $path = $root . '/' . $page; +$extractScript = __DIR__ . '/Extract.php'; +$phpBinary = PHP_BINARY ?: 'php'; + +foreach ($pages as $page) { if ($isVerbose) { $cli->whisper("Extracting text for '$page'"); } try { - $extractScript = __DIR__ . '/Extract.php'; - $translations = `php $extractScript --page $page`; + $translations = `"$phpBinary" "$extractScript" --page "$page"`; $strings = json_decode($translations); } catch (\Exception $e) { $cli->error("Unable to extract text for '$page'"); continue; } + if (is_object($strings) === false) { + $cli->error("Unable to decode extracted translations for '$page'"); + continue; + } + foreach ($languages as $language) { $languagePath = L10n::languageDirectory($language) . '/' . $page . '.json'; - $newTranslations = array(); - if (file_exists($languagePath)) { $currentData = file_get_contents($languagePath); $currentTranslations = json_decode($currentData, true); + + if (is_array($currentTranslations) === false) { + $cli->error("Unable to decode existing translations in '$languagePath'"); + $progress->advance(); + continue; + } } else { - $currentTranslations = array(); + $currentTranslations = []; } - if (is_object($strings)) { // Ensure $strings is an object - foreach ($strings as $processedSourceKey => $originalSourceValue) { - // $processedSourceKey is the key from Extract.php's output (already processed) - // $originalSourceValue is the original English text for that key - - if (isset($currentTranslations[$processedSourceKey])) { - // Preserve existing translation - $newTranslations[$processedSourceKey] = $currentTranslations[$processedSourceKey]; - } else { - // New string for this language file - if ($language === 'en') { - // For English, the "translation" is the original source value - $newTranslations[$processedSourceKey] = $originalSourceValue; - } else { - // For other languages, initialize as an empty string (needs translation) - $newTranslations[$processedSourceKey] = ""; - } - } + $normalizedCurrentTranslations = []; + foreach ($currentTranslations as $key => $value) { + $normKey = preg_replace('/\s+/', ' ', str_replace(["\r\n", "\n"], ' ', $key)); + $normalizedCurrentTranslations[$normKey] = $value; + } + + $newTranslations = []; + + foreach ($strings as $sourceKey => $originalValue) { + if (isset($normalizedCurrentTranslations[$sourceKey])) { + // Preserve existing translations. + $newTranslations[$sourceKey] = $normalizedCurrentTranslations[$sourceKey]; + } elseif ($language === 'en') { + // English uses the original source text. + $newTranslations[$sourceKey] = $originalValue; + } else { + // New translations start empty. + $newTranslations[$sourceKey] = ''; } } $directory = dirname($languagePath); + if (is_dir($directory) === false) { mkdir($directory, 0766, true); } if (count($newTranslations) > 0) { - // Preprocess strings to remove newlines in the source strings - $processedTranslations = []; - foreach ($newTranslations as $key => $value) { - // Remove newlines from the key completely and reduce multiple spaces to a single space - $processedKey = str_replace(["\r\n", "\n"], " ", $key); - $processedKey = preg_replace('/\s+/', ' ', $processedKey); - $processedTranslations[$processedKey] = $value; - } - $newData = json_encode( - $processedTranslations, + $newTranslations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); - file_put_contents($languagePath, $newData . PHP_EOL); + + if ($newData === false) { + $cli->error("Unable to encode translations for '$languagePath'"); + $progress->advance(); + continue; + } + + file_put_contents($languagePath, $newData . "\n"); } elseif (file_exists($languagePath)) { unlink($languagePath); }