feat: implement Language singleton with shorthand functions (#1966)
Some checks are pending
Continuous Integration / Nightly builds 📦 (push) Waiting to run
Continuous Integration / 🎉 Deploy (push) Waiting to run
PHPMD / Run PHPMD scanning (push) Waiting to run

- 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:
Yury Pikhtarev 2025-06-18 19:29:06 +04:00 committed by GitHub
parent 2fd306704f
commit 49717d3a68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 622 additions and 40 deletions

View file

@ -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.

View file

@ -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
*/

View file

@ -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

View file

@ -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
View 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.");
}
}

View file

@ -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();

View file

@ -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);