mirror of
https://github.com/torrentpier/torrentpier
synced 2025-07-05 12:36:12 -07:00
feat: implement Language singleton with shorthand functions (#1966)
- Add Language singleton class (src/Language.php) following TorrentPier patterns - Implement automatic source language fallback loading - Add dot notation support for nested language arrays - Provide convenient shorthand functions __() and _e() in common.php - Maintain 100% backward compatibility with global $lang variable - Replace manual language file loading in bb_die() and bb_date() functions - Update poll.php as modern usage example with __() shorthand - Integrate with User.php initialization via lang()->initializeLanguage() - Clean up Template.php compilation removing legacy source language logic - Add comprehensive UPGRADE_GUIDE.md documentation section BREAKING CHANGE: None - full backward compatibility maintained
This commit is contained in:
parent
2fd306704f
commit
49717d3a68
7 changed files with 622 additions and 40 deletions
214
UPGRADE_GUIDE.md
214
UPGRADE_GUIDE.md
|
@ -7,6 +7,7 @@ This guide helps you upgrade your TorrentPier installation to the latest version
|
|||
- [Database Layer Migration](#database-layer-migration)
|
||||
- [Unified Cache System Migration](#unified-cache-system-migration)
|
||||
- [Configuration System Migration](#configuration-system-migration)
|
||||
- [Language System Migration](#language-system-migration)
|
||||
- [Censor System Migration](#censor-system-migration)
|
||||
- [Select System Migration](#select-system-migration)
|
||||
- [Development System Migration](#development-system-migration)
|
||||
|
@ -297,6 +298,219 @@ if (isset(config()->bt_announce_url)) {
|
|||
}
|
||||
```
|
||||
|
||||
## 🌐 Language System Migration
|
||||
|
||||
TorrentPier has modernized its language system with a singleton pattern while maintaining 100% backward compatibility with existing global `$lang` variable.
|
||||
|
||||
### No Code Changes Required
|
||||
|
||||
**Important**: All existing `global $lang` calls continue to work exactly as before. This is an internal modernization that requires **zero code changes** in your application.
|
||||
|
||||
```php
|
||||
// ✅ All existing code continues to work unchanged
|
||||
global $lang;
|
||||
echo $lang['FORUM'];
|
||||
echo $lang['DATETIME']['TODAY'];
|
||||
```
|
||||
|
||||
### Key Improvements
|
||||
|
||||
#### Modern Foundation
|
||||
- **Singleton Pattern**: Efficient memory usage and consistent TorrentPier architecture
|
||||
- **Centralized Management**: Single point of control for language loading and switching
|
||||
- **Type Safety**: Better error detection and IDE support
|
||||
- **Dot Notation Support**: Access nested language arrays with simple syntax
|
||||
|
||||
#### Enhanced Functionality
|
||||
- **Automatic Fallback**: Source language fallback for missing translations
|
||||
- **Dynamic Loading**: Load additional language files for modules/extensions
|
||||
- **Runtime Modification**: Add or modify language strings at runtime
|
||||
- **Locale Management**: Automatic locale setting based on language selection
|
||||
|
||||
### Enhanced Capabilities
|
||||
|
||||
New code can leverage the modern Language singleton features with convenient shorthand functions:
|
||||
|
||||
```php
|
||||
// ✅ Convenient shorthand functions (recommended for frequent use)
|
||||
echo __('FORUM'); // Same as lang()->get('FORUM')
|
||||
echo __('DATETIME.TODAY'); // Dot notation for nested arrays
|
||||
_e('WELCOME_MESSAGE'); // Echo shorthand
|
||||
$message = __('CUSTOM_MESSAGE', 'Default'); // With default value
|
||||
|
||||
// ✅ Full singleton access (for advanced features)
|
||||
echo lang()->get('FORUM');
|
||||
echo lang()->get('DATETIME.TODAY'); // Dot notation for nested arrays
|
||||
|
||||
// ✅ Check if language key exists
|
||||
if (lang()->has('ADVANCED_FEATURE')) {
|
||||
echo __('ADVANCED_FEATURE');
|
||||
}
|
||||
|
||||
// ✅ Get current language information
|
||||
$currentLang = lang()->getCurrentLanguage();
|
||||
$langName = lang()->getLanguageName();
|
||||
$langLocale = lang()->getLanguageLocale();
|
||||
|
||||
// ✅ Load additional language files for modules
|
||||
lang()->loadAdditionalFile('custom_module', 'en');
|
||||
|
||||
// ✅ Runtime language modifications
|
||||
lang()->set('CUSTOM_KEY', 'Custom Value');
|
||||
lang()->set('NESTED.KEY', 'Nested Value');
|
||||
```
|
||||
|
||||
### Language Management
|
||||
|
||||
#### Available Languages
|
||||
```php
|
||||
// Get all available languages from configuration
|
||||
$availableLanguages = lang()->getAvailableLanguages();
|
||||
|
||||
// Get language display name
|
||||
$englishName = lang()->getLanguageName('en'); // Returns: "English"
|
||||
$currentName = lang()->getLanguageName(); // Current language name
|
||||
|
||||
// Get language locale for formatting
|
||||
$locale = lang()->getLanguageLocale('ru'); // Returns: "ru_RU.UTF-8"
|
||||
```
|
||||
|
||||
#### Dynamic Language Loading
|
||||
```php
|
||||
// Load additional language files (useful for modules/plugins)
|
||||
$success = lang()->loadAdditionalFile('torrent_management');
|
||||
if ($success) {
|
||||
echo lang()->get('TORRENT_UPLOADED');
|
||||
}
|
||||
|
||||
// Load from specific language
|
||||
lang()->loadAdditionalFile('admin_panel', 'de');
|
||||
```
|
||||
|
||||
#### Runtime Modifications
|
||||
```php
|
||||
// Set custom language strings
|
||||
lang()->set('SITE_WELCOME', 'Welcome to Our Tracker!');
|
||||
lang()->set('ERRORS.INVALID_TORRENT', 'Invalid torrent file');
|
||||
|
||||
// Modify existing strings
|
||||
lang()->set('LOGIN', 'Sign In');
|
||||
```
|
||||
|
||||
### Backward Compatibility Features
|
||||
|
||||
The singleton automatically maintains all global variables:
|
||||
|
||||
```php
|
||||
// Global variable is automatically updated by the singleton
|
||||
global $lang;
|
||||
|
||||
// When you call lang()->set(), global is updated
|
||||
lang()->set('CUSTOM', 'Value');
|
||||
echo $lang['CUSTOM']; // Outputs: "Value"
|
||||
|
||||
// When language is initialized, $lang is populated
|
||||
// $lang contains user language + source language fallbacks
|
||||
```
|
||||
|
||||
### Integration with User System
|
||||
|
||||
The Language singleton integrates seamlessly with the User system:
|
||||
|
||||
```php
|
||||
// User language is automatically detected and initialized
|
||||
// Based on user preferences, browser detection, or defaults
|
||||
|
||||
// In User->init_userprefs(), language is now initialized with:
|
||||
lang()->initializeLanguage($userLanguage);
|
||||
|
||||
// This replaces the old manual language file loading
|
||||
// while maintaining exact same functionality
|
||||
```
|
||||
|
||||
### Convenient Shorthand Functions
|
||||
|
||||
For frequent language access, TorrentPier provides convenient shorthand functions:
|
||||
|
||||
```php
|
||||
// ✅ __() - Get language string (most common)
|
||||
echo __('FORUM'); // Returns: "Forum"
|
||||
echo __('DATETIME.TODAY'); // Nested access: "Today"
|
||||
$msg = __('MISSING_KEY', 'Default'); // With default value
|
||||
|
||||
// ✅ _e() - Echo language string directly
|
||||
_e('WELCOME_MESSAGE'); // Same as: echo __('WELCOME_MESSAGE')
|
||||
_e('USER_ONLINE', 'Online'); // With default value
|
||||
|
||||
// ✅ Common usage patterns
|
||||
$title = __('PAGE_TITLE', config()->get('sitename'));
|
||||
$error = __('ERROR.INVALID_INPUT', 'Invalid input');
|
||||
```
|
||||
|
||||
These functions make language access much more convenient compared to the full `lang()->get()` syntax:
|
||||
|
||||
```php
|
||||
// Before (verbose)
|
||||
echo lang()->get('FORUM');
|
||||
echo lang()->get('DATETIME.TODAY');
|
||||
$msg = lang()->get('WELCOME', 'Welcome');
|
||||
|
||||
// After (concise)
|
||||
echo __('FORUM');
|
||||
echo __('DATETIME.TODAY');
|
||||
$msg = __('WELCOME', 'Welcome');
|
||||
```
|
||||
|
||||
### Magic Methods Support
|
||||
```php
|
||||
// Magic getter (same as lang()->get())
|
||||
$welcome = lang()->WELCOME;
|
||||
$today = lang()->{'DATETIME.TODAY'};
|
||||
|
||||
// Magic setter (same as lang()->set())
|
||||
lang()->CUSTOM_MESSAGE = 'Hello World';
|
||||
lang()->{'NESTED.KEY'} = 'Nested Value';
|
||||
|
||||
// Magic isset
|
||||
if (isset(lang()->ADVANCED_FEATURE)) {
|
||||
// Language key exists
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Benefits
|
||||
|
||||
While maintaining compatibility, you get:
|
||||
- **Single Language Loading**: Languages loaded once and cached in singleton
|
||||
- **Memory Efficiency**: No duplicate language arrays across application
|
||||
- **Automatic Locale Setting**: Proper locale configuration for date/time formatting
|
||||
- **Fallback Chain**: Source language → Default language → Requested language
|
||||
|
||||
### Verification
|
||||
|
||||
To verify the migration is working correctly:
|
||||
|
||||
```php
|
||||
// ✅ Test convenient shorthand functions
|
||||
echo "Forum text: " . __('FORUM');
|
||||
echo "Today text: " . __('DATETIME.TODAY');
|
||||
_e('INFORMATION'); // Echo directly
|
||||
|
||||
// ✅ Test with default values
|
||||
echo "Custom: " . __('CUSTOM_KEY', 'Default Value');
|
||||
|
||||
// ✅ Test full singleton access
|
||||
echo "Current language: " . lang()->getCurrentLanguage();
|
||||
echo "Language name: " . lang()->getLanguageName();
|
||||
|
||||
// ✅ Test backward compatibility
|
||||
global $lang;
|
||||
echo "Global access: " . $lang['FORUM'];
|
||||
|
||||
// ✅ Verify globals are synchronized
|
||||
lang()->set('TEST_KEY', 'Test Value');
|
||||
echo "Sync test: " . $lang['TEST_KEY']; // Should output: "Test Value"
|
||||
```
|
||||
|
||||
## 🛡️ Censor System Migration
|
||||
|
||||
The word censoring system has been refactored to use a singleton pattern, similar to the Configuration system, providing better performance and consistency.
|
||||
|
|
34
common.php
34
common.php
|
@ -120,6 +120,40 @@ function dev(): \TorrentPier\Dev
|
|||
return \TorrentPier\Dev::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Language instance
|
||||
*
|
||||
* @return \TorrentPier\Language
|
||||
*/
|
||||
function lang(): \TorrentPier\Language
|
||||
{
|
||||
return \TorrentPier\Language::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a language string (shorthand for lang()->get())
|
||||
*
|
||||
* @param string $key Language key, supports dot notation (e.g., 'DATETIME.TODAY')
|
||||
* @param mixed $default Default value if key doesn't exist
|
||||
* @return mixed Language string or default value
|
||||
*/
|
||||
function __(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return \TorrentPier\Language::getInstance()->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Echo a language string (shorthand for echo __())
|
||||
*
|
||||
* @param string $key Language key, supports dot notation
|
||||
* @param mixed $default Default value if key doesn't exist
|
||||
* @return void
|
||||
*/
|
||||
function _e(string $key, mixed $default = null): void
|
||||
{
|
||||
echo \TorrentPier\Language::getInstance()->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize debug
|
||||
*/
|
||||
|
|
|
@ -1110,7 +1110,7 @@ function bb_date($gmepoch, $format = false, $friendly_date = true)
|
|||
$format = config()->get('default_dateformat');
|
||||
}
|
||||
if (empty($lang)) {
|
||||
require_once(config()->get('default_lang_dir') . 'main.php');
|
||||
lang()->initializeLanguage();
|
||||
}
|
||||
|
||||
if (!defined('IS_GUEST') || IS_GUEST) {
|
||||
|
@ -1347,9 +1347,9 @@ function bb_die($msg_text, $status_code = null)
|
|||
define('HAS_DIED', 1);
|
||||
define('DISABLE_CACHING_OUTPUT', true);
|
||||
|
||||
// If empty lang
|
||||
// If empty lang, initialize language singleton
|
||||
if (empty($lang)) {
|
||||
require(config()->get('default_lang_dir') . 'main.php');
|
||||
lang()->initializeLanguage();
|
||||
}
|
||||
|
||||
// If empty session
|
||||
|
|
44
poll.php
44
poll.php
|
@ -28,19 +28,19 @@ $poll = new TorrentPier\Legacy\Poll();
|
|||
|
||||
// Checking $topic_id
|
||||
if (!$topic_id) {
|
||||
bb_die($lang['INVALID_TOPIC_ID']);
|
||||
bb_die(__('INVALID_TOPIC_ID'));
|
||||
}
|
||||
|
||||
// Getting topic data if present
|
||||
if (!$t_data = DB()->table(BB_TOPICS)->where('topic_id', $topic_id)->fetch()?->toArray()) {
|
||||
bb_die($lang['INVALID_TOPIC_ID_DB']);
|
||||
bb_die(__('INVALID_TOPIC_ID_DB'));
|
||||
}
|
||||
|
||||
// Checking the rights
|
||||
if ($mode != 'poll_vote') {
|
||||
if ($t_data['topic_poster'] != $userdata['user_id']) {
|
||||
if (!IS_AM) {
|
||||
bb_die($lang['NOT_AUTHORISED']);
|
||||
bb_die(__('NOT_AUTHORISED'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,10 +48,10 @@ if ($mode != 'poll_vote') {
|
|||
// Checking the ability to make changes
|
||||
if ($mode == 'poll_delete') {
|
||||
if ($t_data['topic_time'] < TIMENOW - config()->get('poll_max_days') * 86400) {
|
||||
bb_die(sprintf($lang['NEW_POLL_DAYS'], config()->get('poll_max_days')));
|
||||
bb_die(sprintf(__('NEW_POLL_DAYS'), config()->get('poll_max_days')));
|
||||
}
|
||||
if (!IS_ADMIN && ($t_data['topic_vote'] != POLL_FINISHED)) {
|
||||
bb_die($lang['CANNOT_DELETE_POLL']);
|
||||
bb_die(__('CANNOT_DELETE_POLL'));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,25 +59,25 @@ switch ($mode) {
|
|||
case 'poll_vote':
|
||||
// Checking for poll existence
|
||||
if (!$t_data['topic_vote']) {
|
||||
bb_die($lang['POST_HAS_NO_POLL']);
|
||||
bb_die(__('POST_HAS_NO_POLL'));
|
||||
}
|
||||
|
||||
// Checking that the topic has not been locked
|
||||
if ($t_data['topic_status'] == TOPIC_LOCKED) {
|
||||
bb_die($lang['TOPIC_LOCKED_SHORT']);
|
||||
bb_die(__('TOPIC_LOCKED_SHORT'));
|
||||
}
|
||||
|
||||
// Checking that poll has not been finished
|
||||
if (!\TorrentPier\Legacy\Poll::pollIsActive($t_data)) {
|
||||
bb_die($lang['NEW_POLL_ENDED']);
|
||||
bb_die(__('NEW_POLL_ENDED'));
|
||||
}
|
||||
|
||||
if (!$vote_id) {
|
||||
bb_die($lang['NO_VOTE_OPTION']);
|
||||
bb_die(__('NO_VOTE_OPTION'));
|
||||
}
|
||||
|
||||
if (\TorrentPier\Legacy\Poll::userIsAlreadyVoted($topic_id, (int)$userdata['user_id'])) {
|
||||
bb_die($lang['ALREADY_VOTED']);
|
||||
bb_die(__('ALREADY_VOTED'));
|
||||
}
|
||||
|
||||
$affected_rows = DB()->table(BB_POLL_VOTES)
|
||||
|
@ -86,7 +86,7 @@ switch ($mode) {
|
|||
->update(['vote_result' => new \Nette\Database\SqlLiteral('vote_result + 1')]);
|
||||
|
||||
if ($affected_rows != 1) {
|
||||
bb_die($lang['NO_VOTE_OPTION']);
|
||||
bb_die(__('NO_VOTE_OPTION'));
|
||||
}
|
||||
|
||||
// Voting process
|
||||
|
@ -101,46 +101,46 @@ switch ($mode) {
|
|||
// Ignore duplicate entry (equivalent to INSERT IGNORE)
|
||||
}
|
||||
CACHE('bb_poll_data')->rm("poll_$topic_id");
|
||||
bb_die($lang['VOTE_CAST']);
|
||||
bb_die(__('VOTE_CAST'));
|
||||
break;
|
||||
case 'poll_start':
|
||||
// Checking for poll existence
|
||||
if (!$t_data['topic_vote']) {
|
||||
bb_die($lang['POST_HAS_NO_POLL']);
|
||||
bb_die(__('POST_HAS_NO_POLL'));
|
||||
}
|
||||
|
||||
// Starting the poll
|
||||
DB()->table(BB_TOPICS)
|
||||
->where('topic_id', $topic_id)
|
||||
->update(['topic_vote' => 1]);
|
||||
bb_die($lang['NEW_POLL_START']);
|
||||
bb_die(__('NEW_POLL_START'));
|
||||
break;
|
||||
case 'poll_finish':
|
||||
// Checking for poll existence
|
||||
if (!$t_data['topic_vote']) {
|
||||
bb_die($lang['POST_HAS_NO_POLL']);
|
||||
bb_die(__('POST_HAS_NO_POLL'));
|
||||
}
|
||||
|
||||
// Finishing the poll
|
||||
DB()->table(BB_TOPICS)
|
||||
->where('topic_id', $topic_id)
|
||||
->update(['topic_vote' => POLL_FINISHED]);
|
||||
bb_die($lang['NEW_POLL_END']);
|
||||
bb_die(__('NEW_POLL_END'));
|
||||
break;
|
||||
case 'poll_delete':
|
||||
// Checking for poll existence
|
||||
if (!$t_data['topic_vote']) {
|
||||
bb_die($lang['POST_HAS_NO_POLL']);
|
||||
bb_die(__('POST_HAS_NO_POLL'));
|
||||
}
|
||||
|
||||
// Removing poll from database
|
||||
$poll->delete_poll($topic_id);
|
||||
bb_die($lang['NEW_POLL_DELETE']);
|
||||
bb_die(__('NEW_POLL_DELETE'));
|
||||
break;
|
||||
case 'poll_add':
|
||||
// Checking that no other poll exists
|
||||
if ($t_data['topic_vote']) {
|
||||
bb_die($lang['NEW_POLL_ALREADY']);
|
||||
bb_die(__('NEW_POLL_ALREADY'));
|
||||
}
|
||||
|
||||
// Make a poll from $_POST data
|
||||
|
@ -153,12 +153,12 @@ switch ($mode) {
|
|||
|
||||
// Adding poll info to the database
|
||||
$poll->insert_votes_into_db($topic_id);
|
||||
bb_die($lang['NEW_POLL_ADDED']);
|
||||
bb_die(__('NEW_POLL_ADDED'));
|
||||
break;
|
||||
case 'poll_edit':
|
||||
// Checking for poll existence
|
||||
if (!$t_data['topic_vote']) {
|
||||
bb_die($lang['POST_HAS_NO_POLL']);
|
||||
bb_die(__('POST_HAS_NO_POLL'));
|
||||
}
|
||||
|
||||
// Make a poll from $_POST data
|
||||
|
@ -172,7 +172,7 @@ switch ($mode) {
|
|||
// Updating poll info to the database
|
||||
$poll->insert_votes_into_db($topic_id);
|
||||
CACHE('bb_poll_data')->rm("poll_$topic_id");
|
||||
bb_die($lang['NEW_POLL_RESULTS']);
|
||||
bb_die(__('NEW_POLL_RESULTS'));
|
||||
break;
|
||||
default:
|
||||
bb_die('Invalid mode: ' . htmlCHR($mode));
|
||||
|
|
343
src/Language.php
Normal file
343
src/Language.php
Normal file
|
@ -0,0 +1,343 @@
|
|||
<?php
|
||||
/**
|
||||
* TorrentPier – Bull-powered BitTorrent tracker engine
|
||||
*
|
||||
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
|
||||
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
|
||||
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
|
||||
*/
|
||||
|
||||
namespace TorrentPier;
|
||||
|
||||
/**
|
||||
* Language management class
|
||||
*
|
||||
* Singleton class that manages language loading and provides access to language variables
|
||||
* while maintaining backward compatibility with global $lang variable.
|
||||
*/
|
||||
class Language
|
||||
{
|
||||
private static ?Language $instance = null;
|
||||
private array $userLanguage = [];
|
||||
private array $sourceLanguage = [];
|
||||
private string $currentLanguage = '';
|
||||
private string $sourceLanguageCode = 'source';
|
||||
private bool $initialized = false;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance of Language
|
||||
*/
|
||||
public static function getInstance(): Language
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the language system (for compatibility)
|
||||
*/
|
||||
public static function init(): Language
|
||||
{
|
||||
return self::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize language loading based on user preferences
|
||||
* Maintains compatibility with existing User.php language initialization
|
||||
*/
|
||||
public function initializeLanguage(string $userLang = '', bool $forceReload = false): void
|
||||
{
|
||||
if ($this->initialized && !$forceReload) {
|
||||
return; // Prevent multiple calling, same as existing logic
|
||||
}
|
||||
|
||||
// Determine language to use
|
||||
if (empty($userLang)) {
|
||||
$userLang = config()->get('default_lang', 'en');
|
||||
}
|
||||
|
||||
$this->currentLanguage = $userLang;
|
||||
|
||||
// Load source language first
|
||||
$this->loadSourceLanguage();
|
||||
|
||||
// Load user language
|
||||
$this->loadUserLanguage($userLang);
|
||||
|
||||
// Set locale
|
||||
$locale = config()->get("lang.{$userLang}.locale", 'en_US.UTF-8');
|
||||
setlocale(LC_ALL, $locale);
|
||||
|
||||
$this->initialized = true;
|
||||
|
||||
// Update global variables for backward compatibility
|
||||
$this->updateGlobalVariables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load source language (fallback)
|
||||
*/
|
||||
private function loadSourceLanguage(): void
|
||||
{
|
||||
$sourceFile = LANG_ROOT_DIR . '/source/main.php';
|
||||
if (is_file($sourceFile)) {
|
||||
$lang = [];
|
||||
require $sourceFile;
|
||||
$this->sourceLanguage = $lang;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user language
|
||||
*/
|
||||
private function loadUserLanguage(string $userLang): void
|
||||
{
|
||||
$userFile = LANG_ROOT_DIR . '/' . $userLang . '/main.php';
|
||||
if (is_file($userFile)) {
|
||||
$lang = [];
|
||||
require $userFile;
|
||||
$this->userLanguage = $lang;
|
||||
} else {
|
||||
// Fall back to default language if user language doesn't exist
|
||||
$defaultFile = LANG_ROOT_DIR . '/' . config()->get('default_lang', 'source') . '/main.php';
|
||||
if (is_file($defaultFile)) {
|
||||
$lang = [];
|
||||
require $defaultFile;
|
||||
$this->userLanguage = $lang;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with source language as fallback
|
||||
$this->userLanguage = array_merge($this->sourceLanguage, $this->userLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update global variables for backward compatibility
|
||||
*/
|
||||
private function updateGlobalVariables(): void
|
||||
{
|
||||
global $lang;
|
||||
$lang = $this->userLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a language string by key
|
||||
* Supports dot notation for nested arrays (e.g., 'DATETIME.TODAY')
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (str_contains($key, '.')) {
|
||||
return $this->getNestedValue($this->userLanguage, $key, $default);
|
||||
}
|
||||
|
||||
return $this->userLanguage[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a language string from source language
|
||||
*/
|
||||
public function getSource(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (str_contains($key, '.')) {
|
||||
return $this->getNestedValue($this->sourceLanguage, $key, $default);
|
||||
}
|
||||
|
||||
return $this->sourceLanguage[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a language key exists
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
if (str_contains($key, '.')) {
|
||||
return $this->getNestedValue($this->userLanguage, $key) !== null;
|
||||
}
|
||||
|
||||
return array_key_exists($key, $this->userLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all language variables
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->userLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all source language variables
|
||||
*/
|
||||
public function allSource(): array
|
||||
{
|
||||
return $this->sourceLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current language code
|
||||
*/
|
||||
public function getCurrentLanguage(): string
|
||||
{
|
||||
return $this->currentLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available languages from config
|
||||
*/
|
||||
public function getAvailableLanguages(): array
|
||||
{
|
||||
return config()->get('lang', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load additional language file (for modules/extensions)
|
||||
*/
|
||||
public function loadAdditionalFile(string $filename, string $language = ''): bool
|
||||
{
|
||||
if (empty($language)) {
|
||||
$language = $this->currentLanguage;
|
||||
}
|
||||
|
||||
$filepath = LANG_ROOT_DIR . '/' . $language . '/' . $filename . '.php';
|
||||
if (!is_file($filepath)) {
|
||||
// Try source language as fallback
|
||||
$filepath = LANG_ROOT_DIR . '/source/' . $filename . '.php';
|
||||
if (!is_file($filepath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$lang = [];
|
||||
require $filepath;
|
||||
|
||||
// Merge with existing language data
|
||||
$this->userLanguage = array_merge($this->userLanguage, $lang);
|
||||
|
||||
// Update global variable for backward compatibility
|
||||
global $lang;
|
||||
$lang = $this->userLanguage;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a language variable (runtime modification)
|
||||
*/
|
||||
public function set(string $key, mixed $value): void
|
||||
{
|
||||
if (str_contains($key, '.')) {
|
||||
$this->setNestedValue($this->userLanguage, $key, $value);
|
||||
} else {
|
||||
$this->userLanguage[$key] = $value;
|
||||
}
|
||||
|
||||
// Update global variable for backward compatibility
|
||||
global $lang;
|
||||
$lang = $this->userLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested value using dot notation
|
||||
*/
|
||||
private function getNestedValue(array $array, string $key, mixed $default = null): mixed
|
||||
{
|
||||
$keys = explode('.', $key);
|
||||
$value = $array;
|
||||
|
||||
foreach ($keys as $k) {
|
||||
if (!is_array($value) || !array_key_exists($k, $value)) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$k];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set nested value using dot notation
|
||||
*/
|
||||
private function setNestedValue(array &$array, string $key, mixed $value): void
|
||||
{
|
||||
$keys = explode('.', $key);
|
||||
$target = &$array;
|
||||
|
||||
foreach ($keys as $k) {
|
||||
if (!isset($target[$k]) || !is_array($target[$k])) {
|
||||
$target[$k] = [];
|
||||
}
|
||||
$target = &$target[$k];
|
||||
}
|
||||
|
||||
$target = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get language name for display
|
||||
*/
|
||||
public function getLanguageName(string $code = ''): string
|
||||
{
|
||||
if (empty($code)) {
|
||||
$code = $this->currentLanguage;
|
||||
}
|
||||
|
||||
return config()->get("lang.{$code}.name", $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get language locale
|
||||
*/
|
||||
public function getLanguageLocale(string $code = ''): string
|
||||
{
|
||||
if (empty($code)) {
|
||||
$code = $this->currentLanguage;
|
||||
}
|
||||
|
||||
return config()->get("lang.{$code}.locale", 'en_US.UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to allow property access for backward compatibility
|
||||
*/
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
return $this->get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to allow property setting for backward compatibility
|
||||
*/
|
||||
public function __set(string $key, mixed $value): void
|
||||
{
|
||||
$this->set($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to check if property exists
|
||||
*/
|
||||
public function __isset(string $key): bool
|
||||
{
|
||||
return $this->has($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent cloning of the singleton instance
|
||||
*/
|
||||
private function __clone()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent unserialization of the singleton instance
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \Exception("Cannot unserialize a singleton.");
|
||||
}
|
||||
}
|
|
@ -583,7 +583,7 @@ class User
|
|||
*/
|
||||
public function init_userprefs()
|
||||
{
|
||||
global $theme, $source_lang, $DeltaTime;
|
||||
global $theme, $DeltaTime;
|
||||
|
||||
if (defined('LANG_DIR')) {
|
||||
return;
|
||||
|
@ -621,17 +621,8 @@ class User
|
|||
define('LANG_DIR', DEFAULT_LANG_DIR);
|
||||
}
|
||||
|
||||
/** Temporary place source language to the global */
|
||||
$lang = [];
|
||||
require(SOURCE_LANG_DIR . 'main.php');
|
||||
$source_lang = $lang;
|
||||
unset($lang);
|
||||
|
||||
/** Place user language to the global */
|
||||
global $lang;
|
||||
require(LANG_DIR . 'main.php');
|
||||
setlocale(LC_ALL, config()->get('lang')[$this->data['user_lang']]['locale'] ?? 'en_US.UTF-8');
|
||||
$lang += $source_lang;
|
||||
// Initialize Language singleton with user preferences
|
||||
lang()->initializeLanguage($this->data['user_lang']);
|
||||
|
||||
$theme = setup_style();
|
||||
$DeltaTime = new DateDelta();
|
||||
|
|
|
@ -107,6 +107,7 @@ class Template
|
|||
$this->tpldir = TEMPLATES_DIR;
|
||||
$this->root = $root;
|
||||
$this->tpl = basename($root);
|
||||
// Use Language singleton but maintain backward compatibility with global $lang
|
||||
$this->lang =& $lang;
|
||||
$this->use_cache = config()->get('xs_use_cache');
|
||||
|
||||
|
@ -230,11 +231,10 @@ class Template
|
|||
|
||||
/** @noinspection PhpUnusedLocalVariableInspection */
|
||||
// bb_cfg deprecated, but kept for compatibility with non-adapted themes
|
||||
global $lang, $source_lang, $bb_cfg, $user;
|
||||
global $lang, $bb_cfg, $user;
|
||||
|
||||
$L =& $lang;
|
||||
$V =& $this->vars;
|
||||
$SL =& $source_lang;
|
||||
|
||||
if ($filename) {
|
||||
include $filename;
|
||||
|
@ -766,7 +766,7 @@ class Template
|
|||
$code = str_replace($search, $replace, $code);
|
||||
}
|
||||
// This will handle the remaining root-level varrefs
|
||||
$code = preg_replace('#\{(L_([a-z0-9\-_]+?))\}#i', '<?php echo isset($L[\'$2\']) ? $L[\'$2\'] : (isset($SL[\'$2\']) ? $SL[\'$2\'] : $V[\'$1\']); ?>', $code);
|
||||
$code = preg_replace('#\{(L_([a-z0-9\-_]+?))\}#i', '<?php echo isset($L[\'$2\']) ? $L[\'$2\'] : $V[\'$1\']; ?>', $code);
|
||||
$code = preg_replace('#\{(\$[a-z_][a-z0-9_$\->\'\"\.\[\]]*?)\}#i', '<?php echo isset($1) ? $1 : \'\'; ?>', $code);
|
||||
$code = preg_replace('#\{(\#([a-z_][a-z0-9_]*?)\#)\}#i', '<?php echo defined(\'$2\') ? $2 : \'\'; ?>', $code);
|
||||
$code = preg_replace('#\{([a-z0-9\-_]+?)\}#i', '<?php echo isset($V[\'$1\']) ? $V[\'$1\'] : \'\'; ?>', $code);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue