fix(i18n): update language retrieval in database and settings views

This commit is contained in:
Divarion-D
2026-04-12 16:41:07 +03:00
parent 336db9ad93
commit 1ee9760de1
3 changed files with 149 additions and 157 deletions

View File

@@ -1,190 +1,182 @@
<?php
/**
* Translator PHP class - multilingual support system with auto-key generation
* Translator — XC_VM multilingual support system.
*
* @package VateronMedia_Translator
* Loads translations from INI files, switches language via cookie,
* automatically copies missing keys from en.ini into the current language.
*
* @package XC_VM\Core\Localization
* @author Divarion_D <https://github.com/Divarion-D>
* @copyright 2025-2026 Vateron Media
* @link https://github.com/Vateron-Media/XC_VM
* @version 1.0.0
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*
* A PHP class created specifically for the XC_VM project to provide multilingual support.
* Features automatic translation key generation, INI file management, language switching,
* and missing key auto-creation with file locking for concurrent access.
*/
class Translator {
/** @var array<string, string> Currently loaded translations */
private static array $translations = [];
/** @var array<string, string> */
private static array $translations = [];
/** @var string Current language */
private static string $currentLang = 'en';
/** @var string */
private static string $currentLang = 'en';
/** @var string Path to translations folder (with trailing slash) */
private static string $langsDir = __DIR__ . '/lang/';
/** @var string */
private static string $langsDir = __DIR__ . '/lang/';
/** @var array<string> Cache of available languages */
private static array $availableLanguages = [];
/** @var string[] */
private static array $availableLanguages = [];
/**
* Initialize translator
*
* @param string|null $langsDir Path to .ini files folder (e.g., /var/www/lang/)
*/
public static function init(?string $langsDir = null): void {
if ($langsDir !== null) {
self::$langsDir = rtrim($langsDir, '/') . '/';
}
/**
* Initialize translator: scans available languages, detects current from cookie.
*
* @param string|null $langsDir Path to .ini files directory
*/
public static function init(?string $langsDir = null): void {
if ($langsDir !== null) {
self::$langsDir = rtrim($langsDir, '/') . '/';
}
// Get and cache list of languages
self::$availableLanguages = self::scanAvailableLanguages();
self::$availableLanguages = self::scanAvailableLanguages();
// Detect user language
$requestedLang = $_COOKIE['lang'] ?? 'en';
self::$currentLang = in_array($requestedLang, self::$availableLanguages)
? $requestedLang
: 'en';
$requestedLang = $_COOKIE['lang'] ?? 'en';
self::$currentLang = in_array($requestedLang, self::$availableLanguages)
? $requestedLang
: 'en';
self::loadLanguage(self::$currentLang);
}
self::loadLanguage(self::$currentLang);
}
/**
* Change language at runtime + update cookie
*/
public static function setLanguage(string $lang): bool {
if (!in_array($lang, self::$availableLanguages)) {
return false;
}
/**
* Switch language at runtime and set cookie for 1 year.
*
* @param string $lang Language code (en, ru, de, ...)
* @return bool true if language exists and was switched
*/
public static function setLanguage(string $lang): bool {
if (!in_array($lang, self::$availableLanguages)) {
return false;
}
self::$currentLang = $lang;
self::loadLanguage($lang);
self::$currentLang = $lang;
self::loadLanguage($lang);
// Cookie for one year
if (!headers_sent()) {
setcookie('lang', $lang, time() + 365 * 24 * 3600, '/');
}
if (!headers_sent()) {
setcookie('lang', $lang, time() + 365 * 24 * 3600, '/');
}
return true;
}
return true;
}
/**
* Get translation by key
* If key doesn't exist — it will be automatically added to all language files
*/
public static function get(string $key, array $replace = []): string {
$text = self::$translations[$key] ?? null;
/**
* Get translation by key. If missing — copies from en.ini into current language.
*
* @param string $key Translation key
* @param array<string, string> $replace Substitutions for strtr()
* @return string Translated string or key as fallback
*/
public static function get(string $key, array $replace = []): string {
$text = self::$translations[$key] ?? null;
if ($text === null) {
$enFallback = self::addMissingKeyToAllLanguages($key);
if ($text === null) {
$enFallback = self::copyMissingKeyFromEnglish($key);
$text = $enFallback ?? $key;
self::$translations[$key] = $text;
}
// For non-English: use English value as fallback; for English: key name
$text = (self::$currentLang !== 'en' && $enFallback !== null)
? $enFallback
: $key;
return !empty($replace) ? strtr($text, $replace) : $text;
}
// Update current loaded array so subsequent calls see the value immediately
self::$translations[$key] = $text;
}
/**
* @return string Current language code
*/
public static function current(): string {
return self::$currentLang;
}
return !empty($replace) ? strtr($text, $replace) : $text;
}
/**
* @return string[] List of available language codes
*/
public static function available(): array {
return self::$availableLanguages;
}
/**
* Current language
*/
public static function current(): string {
return self::$currentLang;
}
/**
* Scan langs directory for .ini files.
*
* @return string[]
*/
private static function scanAvailableLanguages(): array {
$languages = [];
$files = glob(self::$langsDir . '*.ini');
/**
* All available languages
*/
public static function available(): array {
return self::$availableLanguages;
}
foreach ($files as $file) {
if (is_file($file) && is_readable($file)) {
$languages[] = pathinfo($file, PATHINFO_FILENAME);
}
}
// ═════════════════════════════════════════════════════════════════
// Internal methods
// ═════════════════════════════════════════════════════════════════
$languages = array_unique($languages);
if (empty($languages)) {
$languages = ['en'];
}
private static function scanAvailableLanguages(): array {
$languages = [];
$files = glob(self::$langsDir . '*.ini');
return $languages;
}
foreach ($files as $file) {
if (is_file($file) && is_readable($file)) {
$code = pathinfo($file, PATHINFO_FILENAME);
// Optionally filter by valid language codes if needed
$languages[] = $code;
}
}
/**
* Load translations from .ini file into $translations. Falls back to en.ini.
*
* @param string $lang Language code
*/
private static function loadLanguage(string $lang): void {
$file = self::$langsDir . $lang . '.ini';
// If nothing found at all — guarantee at least en
$languages = array_unique($languages);
if (empty($languages)) {
$languages = ['en'];
}
if (!is_readable($file)) {
$file = self::$langsDir . 'en.ini';
}
return $languages;
}
$data = parse_ini_file($file, false, INI_SCANNER_RAW);
self::$translations = ($data !== false) ? $data : [];
}
private static function loadLanguage(string $lang): void {
$file = self::$langsDir . $lang . '.ini';
/**
* Copy a missing key from en.ini into the current language file.
* Uses file locking to prevent race conditions on concurrent requests.
*
* @param string $key Translation key
* @return string|null Value from en.ini, or null if key not found
*/
private static function copyMissingKeyFromEnglish(string $key): ?string {
$enFile = self::$langsDir . 'en.ini';
$enValue = null;
if (is_readable($enFile)) {
$enData = parse_ini_file($enFile, false, INI_SCANNER_RAW);
if ($enData !== false && isset($enData[$key])) {
$enValue = $enData[$key];
}
}
// If current language file is not readable — fallback to en
if (!is_readable($file)) {
$file = self::$langsDir . 'en.ini';
}
$file = self::$langsDir . self::$currentLang . '.ini';
$data = parse_ini_file($file, false, INI_SCANNER_RAW);
self::$translations = ($data !== false) ? $data : [];
}
if (!file_exists($file)) {
file_put_contents($file, "; " . self::$currentLang . " language file\n");
}
/**
* Add missing key to all language files.
* Returns the English value for the key (or null if not found in en.ini).
*/
private static function addMissingKeyToAllLanguages(string $key): ?string {
// Load English values to use as default for non-English languages
$enFile = self::$langsDir . 'en.ini';
$enValue = $key; // fallback to key name
if (is_readable($enFile)) {
$enData = parse_ini_file($enFile, false, INI_SCANNER_RAW);
if ($enData !== false && isset($enData[$key])) {
$enValue = $enData[$key];
}
}
$content = file_get_contents($file);
if (str_contains($content, "\n{$key} =") || str_contains($content, "\r{$key} =")) {
return $enValue;
}
foreach (self::$availableLanguages as $lang) {
$file = self::$langsDir . $lang . '.ini';
$value = $enValue ?? $key;
$escapedValue = str_replace('"', '\\"', $value);
$endsWithNewline = str_ends_with($content, "\n");
$lineToAdd = ($endsWithNewline ? '' : "\n") . "{$key} = \"{$escapedValue}\"\n";
// If file doesn't exist — create empty one
if (!file_exists($file)) {
file_put_contents($file, "; {$lang} language file\n");
}
$fp = fopen($file, 'a');
if ($fp && flock($fp, LOCK_EX)) {
fwrite($fp, $lineToAdd);
flock($fp, LOCK_UN);
fclose($fp);
}
// Check if key already exists
$content = file_get_contents($file);
if (str_contains($content, "\n{$key} =") || str_contains($content, "\r{$key} =")) {
continue; // key already exists
}
// For English: use key name as value; for others: use English value
$value = ($lang === 'en') ? $key : $enValue;
$escapedValue = str_replace('"', '\\"', $value);
$lineToAdd = "\n{$key} = \"{$escapedValue}\"\n";
// Safe write with locking
$fp = fopen($file, 'a');
if ($fp && flock($fp, LOCK_EX)) {
fwrite($fp, $lineToAdd);
flock($fp, LOCK_UN);
fclose($fp);
}
}
return ($enValue !== $key) ? $enValue : null;
}
return $enValue;
}
}

View File

@@ -228,7 +228,7 @@ and TABLE_SCHEMA=" . dbq($DB['db']) . "ORDER BY TABLE_NAME ASC";
}
function display_select($sth, $q) {
global $dbh, $SRV, $DB, $sqldr, $reccount, $is_sht, $xurl, $is_sm;
global $dbh, $SRV, $DB, $sqldr, $reccount, $is_sht, $xurl, $is_sm, $language;
$rc = ["o", "e"];
$srvn = ue($SRV);
$dbn = ue($DB['db']);
@@ -289,10 +289,10 @@ function display_select($sth, $q) {
$headers .= "<th><div>" . hs($meta->name) . "</div></th>";
}
if ($is_shd) {
$headers .= "<th>' . $language::get('show_create_database') . '</th><th>' . $language::get('show_table_status') . '</th><th>' . $language::get('show_triggers') . '</th>";
$headers .= "<th>" . $language::get('show_create_database') . "</th><th>" . $language::get('show_table_status') . "</th><th>" . $language::get('show_triggers') . "</th>";
}
if ($is_sht) {
$headers .= "<th>' . $language::get('engine') . '</th><th>' . $language::get('rows') . '</th><th>' . $language::get('data_size') . '</th><th>' . $language::get('index_size') . '</th><th>' . $language::get('show_create_table') . '</th><th>' . $language::get('explain') . '</th><th>' . $language::get('indexes') . '</th><th>' . $language::get('export') . '</th><th>' . $language::get('drop') . '</th><th>' . $language::get('truncate') . '</th><th>' . $language::get('optimize') . '</th><th>' . $language::get('repair') . '</th><th>' . $language::get('comment') . '</th>";
$headers .= "<th>" . $language::get('engine') . "</th><th>" . $language::get('rows') . "</th><th>" . $language::get('data_size') . "</th><th>" . $language::get('index_size') . "</th><th>" . $language::get('show_create_table') . "</th><th>" . $language::get('explain') . "</th><th>" . $language::get('indexes') . "</th><th>" . $language::get('export') . "</th><th>" . $language::get('drop') . "</th><th>" . $language::get('truncate') . "</th><th>" . $language::get('optimize') . "</th><th>" . $language::get('repair') . "</th><th>" . $language::get('comment') . "</th>";
}
$headers .= "</tr>\n";
$sqldr .= $headers;
@@ -352,7 +352,7 @@ function display_select($sth, $q) {
}
function print_header() {
global $err_msg, $VERSION, $DBSERVERS, $SRV, $DB, $dbh, $self, $is_sht, $xurl, $SHOW_T;
global $err_msg, $VERSION, $DBSERVERS, $SRV, $DB, $dbh, $self, $is_sht, $xurl, $SHOW_T, $language;
$dbn = $DB['db'] ?? '';
?>
<!DOCTYPE html>
@@ -766,7 +766,7 @@ function print_header() {
}
function print_screen() {
global $out_message, $SQLq, $err_msg, $reccount, $time_all, $sqldr, $page, $MAX_ROWS_PER_PAGE, $is_limited_sql, $last_count, $is_sm;
global $out_message, $SQLq, $err_msg, $reccount, $time_all, $sqldr, $page, $MAX_ROWS_PER_PAGE, $is_limited_sql, $last_count, $is_sm, $language;
$nav = '';
if ($is_limited_sql && ($page || $reccount >= $MAX_ROWS_PER_PAGE)) {
@@ -1209,7 +1209,7 @@ $show_all=$_[5]; #print Totals?
}
function print_export() {
global $self, $xurl, $SRV, $DB, $DUMP_FILE;
global $self, $xurl, $SRV, $DB, $DUMP_FILE, $language;
$t = $_REQUEST['rt'];
$l = ($t) ? "Table $t" : "whole DB";
print_header();

View File

@@ -1155,7 +1155,7 @@ endif; // !$__settingsViewMode
} else {
echo ' selected';
}
echo ' value="manual"><?= $language::get('manual') ?></option></select></div><label class="col-md-4 col-form-label" for="vod_sort_newest">Sort VOD by Date <i title="' . $language::get('change_default_sorting_for_vod_tooltip') . '" class="tooltip text-secondary far fa-circle"></i></label><div class="col-md-2"><input name="vod_sort_newest" id="vod_sort_newest" type="checkbox"';
echo ' value="manual">'. $language::get('manual') .'</option></select></div><label class="col-md-4 col-form-label" for="vod_sort_newest">Sort VOD by Date <i title="' . $language::get('change_default_sorting_for_vod_tooltip') . '" class="tooltip text-secondary far fa-circle"></i></label><div class="col-md-2"><input name="vod_sort_newest" id="vod_sort_newest" type="checkbox"';
if ($rSettings["vod_sort_newest"] == 1) {
echo ' checked ';
}