Centralize language loading with English fallback (#361)

This commit is contained in:
Fabian
2026-07-31 06:12:33 +01:00
committed by GitHub
parent 4f9bc3c37c
commit a1a714b062
19 changed files with 260 additions and 40 deletions
+3 -2
View File
@@ -38,7 +38,8 @@ include_once("../GameEngine/Admin/csrf.php");
// ─── CORE INCLUDES ───────────────────────────────────────────────────────────
include_once("../GameEngine/config.php");
include_once("../GameEngine/Database.php");
include_once("../GameEngine/Lang/" . LANG . ".php");
require_once __DIR__ . "/../GameEngine/Lang/loader.php";
tz_load_language(LANG);
include_once("../GameEngine/Admin/database.php");
// Helperul care descopera pachetele grafice din gpack/ (tz_available_gpacks).
@@ -982,4 +983,4 @@ body.app #menu li.sub ul li a:hover{color:#d97706!important}
</div><!-- #lmidall -->
</body>
</html>
</html>
+3 -2
View File
@@ -21,7 +21,8 @@
#################################################################################
include_once("config.php");
include_once("Lang/" . LANG . ".php");
require_once __DIR__ . "/Lang/loader.php";
tz_load_language(LANG);
/**
* Ensure input exists (avoid undefined variable issues)
@@ -207,4 +208,4 @@ $bbcoded = preg_replace_callback(
$bbcoded = preg_replace('/\[message\]/', '', $bbcoded);
$bbcoded = preg_replace('/\[\/message\]/', '', $bbcoded);
?>
?>
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* Load one interface language and fill its gaps from English.
*
* Language files define constants through tz_def(), so loading the selected
* locale first preserves translated constants while English defines only the
* missing ones. Their legacy $lang arrays need an explicit merge because those
* assignments are not guarded.
*/
if (!function_exists('tz_language_array_from_file')) {
function tz_language_array_from_file($file)
{
$lang = [];
require $file;
return is_array($lang) ? $lang : [];
}
}
if (!function_exists('tz_load_language')) {
function tz_load_language($language, $languageDirectory = __DIR__)
{
global $lang;
$language = strtolower(trim((string) $language));
if (!preg_match('/\A[a-z_]+\z/', $language)) {
$language = 'en';
}
$languageDirectory = rtrim($languageDirectory, '/\\');
$englishFile = $languageDirectory . '/en.php';
if (!is_file($englishFile)) {
throw new RuntimeException('English language fallback not found: ' . $englishFile);
}
$selectedFile = $languageDirectory . '/' . $language . '.php';
if (!is_file($selectedFile)) {
$selectedFile = $englishFile;
}
$localized = tz_language_array_from_file($selectedFile);
if ($selectedFile === $englishFile) {
$lang = $localized;
return $lang;
}
$english = tz_language_array_from_file($englishFile);
$lang = array_replace_recursive($english, $localized);
return $lang;
}
}
+3 -7
View File
@@ -60,12 +60,8 @@ include_once("Form.php");
include_once("Generator.php");
include_once("Multisort.php");
include_once("Ranking.php");
include_once("Lang/" . LANG . ".php");
// en.php is an idempotent fallback (tz_def only defines missing keys); loading
// it after the player language guarantees every interface constant is defined,
// so a key missing from a translation degrades to English instead of a PHP 8.3
// "undefined constant" fatal (blank page). See issue #189.
include_once("Lang/en.php");
require_once __DIR__ . "/Lang/loader.php";
tz_load_language(LANG);
include_once("Logging.php");
include_once("Message.php");
include_once("Alliance.php");
@@ -585,4 +581,4 @@ if (!empty($_SESSION['id_user'])) {
$message = new Message;
$user = new User((int)$_SESSION['id_user'], $database);
}
?>
?>
+3 -2
View File
@@ -25,7 +25,8 @@
========================= */
session_start();
include_once('GameEngine/config.php');
include_once("GameEngine/Lang/". LANG. ".php");
require_once dirname(__DIR__, 2) . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
include_once("GameEngine/Generator.php");
include_once("GameEngine/Database.php");
@@ -278,4 +279,4 @@ while ($donnees = mysqli_fetch_assoc($result2)) {
$i++;
}
echo '[['. $map_js. ']';
echo '[['. $map_js. ']';
+3 -2
View File
@@ -25,7 +25,8 @@
========================= */
session_start();
include_once('GameEngine/config.php');
include_once("GameEngine/Lang/". LANG. ".php");
require_once dirname(__DIR__, 2) . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
include_once("GameEngine/Generator.php");
include_once("GameEngine/Database.php");
@@ -279,4 +280,4 @@ while ($donnees = mysqli_fetch_assoc($result2)) {
$i++;
}
echo '[['. $map_js. ']';
echo '[['. $map_js. ']';
+3 -2
View File
@@ -24,7 +24,8 @@ use App\Utils\AccessLogger;
include_once("GameEngine/config.php");
include_once("GameEngine/Database.php");
include_once("GameEngine/Lang/".LANG.".php");
require_once __DIR__ . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
AccessLogger::logRequest();
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
@@ -163,4 +164,4 @@ include("Templates/Anleitung/4.tpl"); }
</body>
</html>
</html>
+3 -2
View File
@@ -13,7 +13,8 @@ use App\Utils\AccessLogger;
include_once("GameEngine/config.php");
include_once("GameEngine/Database.php");
include_once("GameEngine/Lang/".LANG.".php");
require_once __DIR__ . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
AccessLogger::logRequest();
?>
@@ -183,4 +184,4 @@ AccessLogger::logRequest();
</div>
</body>
</html>
</html>
+2 -1
View File
@@ -50,7 +50,8 @@ else
}
include_once "GameEngine/Database.php";
include_once "GameEngine/Lang/".LANG.".php";
require_once __DIR__ . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
AccessLogger::logRequest();
?>
+3 -2
View File
@@ -35,7 +35,8 @@
include_once($configFile);
include_once("../../GameEngine/Database.php");
include_once("../../GameEngine/Admin/database.php");
include_once("../../GameEngine/Lang/" . LANG . ".php");
require_once dirname(__DIR__, 2) . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
// update Admin details first
$gameConfig = file_get_contents($configFile);
@@ -113,4 +114,4 @@
$gameinstall = 0;
header("Location: ../index.php?s=5");
?>
?>
+3 -9
View File
@@ -22,14 +22,8 @@
#################################################################################
include_once("GameEngine/config.php");
// Load the interface language for the manual templates. Since #186 these
// templates use translation constants (OVERVIEW, TRIBE1, ...) instead of
// hardcoded English; without a language file every constant would be
// undefined and PHP 8.3 would fatal, leaving the in-game help iframe blank.
// en.php is loaded last as an idempotent fallback (tz_def) to fill any key
// missing from the active language.
include_once("GameEngine/Lang/" . LANG . ".php");
include_once("GameEngine/Lang/en.php");
require_once __DIR__ . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
?>
<html>
@@ -84,4 +78,4 @@ else {
?>
</body>
</html>
</html>
+4 -3
View File
@@ -17,8 +17,9 @@
#################################################################################
include("../GameEngine/config.php");
include("../GameEngine/Lang/".LANG.".php");
include("lang/".LANG.".php");
require_once dirname(__DIR__) . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
include(__DIR__ . "/lang/".LANG.".php");
if(T4_COMING==true){
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
@@ -161,4 +162,4 @@ if(T4_COMING==true){
header("Location: ../index.php");
exit;
}
?>
?>
+2 -1
View File
@@ -28,7 +28,8 @@ if (!file_exists('var/installed') && @opendir('install')) {
}
include_once("GameEngine/config.php");
include_once("GameEngine/Lang/" . LANG . ".php");
require_once __DIR__ . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
include_once("GameEngine/Database.php");
include_once("GameEngine/Mailer.php");
include_once("GameEngine/Generator.php");
+3 -2
View File
@@ -17,7 +17,8 @@ use App\Utils\AccessLogger;
include_once("GameEngine/config.php");
include_once("GameEngine/Database.php");
include_once("GameEngine/Lang/".LANG.".php");
require_once __DIR__ . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
AccessLogger::logRequest();
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
@@ -219,4 +220,4 @@ Displaying battle reports or messages in public without consent of both concerne
</body>
</html>
</html>
+3 -2
View File
@@ -24,7 +24,8 @@ use App\Utils\AccessLogger;
include_once("GameEngine/config.php");
include_once("GameEngine/Database.php");
include_once("GameEngine/Lang/".LANG.".php");
require_once __DIR__ . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
AccessLogger::logRequest();
?>
@@ -196,4 +197,4 @@ AccessLogger::logRequest();
</div>
</body>
</html>
</html>
+152
View File
@@ -0,0 +1,152 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
function fail_test($message)
{
throw new RuntimeException($message);
}
function assert_same($expected, $actual, $message)
{
if ($expected !== $actual) {
fail_test(
$message
. "\nExpected: " . var_export($expected, true)
. "\nActual: " . var_export($actual, true)
);
}
}
function run_language_case($language)
{
$expected = [
'en' => [
'tribe' => 'Romans',
'welcome' => 'Welcome to Test Server',
],
'fr' => [
'tribe' => 'Romains',
'welcome' => 'Bienvenue sur le Test Server!',
],
'it' => [
'tribe' => 'Romani',
'welcome' => 'Benvenuto in Test Server',
'fallback' => ['TRIBE7', 'Egyptians'],
],
'ro' => [
'tribe' => 'Romani',
'welcome' => 'Bine ai venit pe Test Server',
'fallback' => ['PUBLIC_WELCOME_TO', 'Welcome to'],
],
'zh' => [
'tribe' => '罗马',
'welcome' => '欢迎来到 Test Server',
'fallback' => ['TRIBE7', 'Egyptians'],
],
];
if (!isset($expected[$language])) {
fail_test('Unknown test language: ' . $language);
}
define('SERVER_NAME', 'Test Server');
define('USRNM_MIN_LENGTH', 3);
define('PW_MIN_LENGTH', 6);
define('COMMENCE', 0);
define('TS_THRESHOLD', 0);
define('CRANNY_CAPACITY', 100);
define('SPEED', 1);
require dirname(__DIR__) . '/GameEngine/Lang/loader.php';
$loaded = tz_load_language($language);
assert_same($expected[$language]['tribe'], TRIBE1, $language . ': selected constants must win');
assert_same(
$expected[$language]['welcome'],
$loaded['index'][0][1] ?? null,
$language . ': selected $lang values must survive the English fallback'
);
assert_same($loaded, $GLOBALS['lang'], $language . ': loader must publish the merged global array');
if (isset($expected[$language]['fallback'])) {
[$constant, $value] = $expected[$language]['fallback'];
assert_same($value, constant($constant), $language . ': missing constant must fall back to English');
}
$fixtureDirectory = __DIR__ . '/fixtures/languages';
$fixture = tz_load_language('xx', $fixtureDirectory);
assert_same(
'English fallback',
$fixture['nested']['fallback_only'] ?? null,
$language . ': missing nested array value must fall back to English'
);
assert_same(
'Localized shared',
$fixture['nested']['shared'] ?? null,
$language . ': localized nested array value must override English'
);
assert_same(
'Localized value',
$fixture['nested']['localized_only'] ?? null,
$language . ': localized-only nested array value must be preserved'
);
assert_same(
'English fallback',
LANGUAGE_LOADER_FIXTURE_FALLBACK,
$language . ': fixture constant must fall back to English'
);
assert_same(
'Localized value',
LANGUAGE_LOADER_FIXTURE_LOCALIZED,
$language . ': fixture localized constant must win'
);
$invalid = tz_load_language('../xx', $fixtureDirectory);
assert_same(
'English shared',
$invalid['nested']['shared'] ?? null,
$language . ': invalid locale must use English'
);
echo "PASS {$language}\n";
}
if (isset($argv[1])) {
run_language_case($argv[1]);
exit(0);
}
$languages = ['en', 'fr', 'it', 'ro', 'zh'];
foreach ($languages as $language) {
$pipes = [];
$process = proc_open(
[PHP_BINARY, __FILE__, $language],
[
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
],
$pipes,
__DIR__
);
if (!is_resource($process)) {
fail_test('Unable to start test process for ' . $language);
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_close($process);
if ($status !== 0) {
fwrite(STDERR, $stderr !== '' ? $stderr : $stdout);
exit($status);
}
echo $stdout;
}
echo "PASS language loader: 5 locales\n";
+6
View File
@@ -0,0 +1,6 @@
<?php
tz_def('LANGUAGE_LOADER_FIXTURE_FALLBACK', 'English fallback');
$lang['nested']['fallback_only'] = 'English fallback';
$lang['nested']['shared'] = 'English shared';
+6
View File
@@ -0,0 +1,6 @@
<?php
tz_def('LANGUAGE_LOADER_FIXTURE_LOCALIZED', 'Localized value');
$lang['nested']['localized_only'] = 'Localized value';
$lang['nested']['shared'] = 'Localized shared';
+2 -1
View File
@@ -24,7 +24,8 @@ use App\Utils\AccessLogger;
include_once("GameEngine/config.php");
include_once("GameEngine/Database.php");
include_once("GameEngine/Lang/".LANG.".php");
require_once __DIR__ . "/GameEngine/Lang/loader.php";
tz_load_language(LANG);
AccessLogger::logRequest();
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">