1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
class classLocale implements ArrayAccess { |
4
|
|
|
public $container = array(); |
5
|
|
|
public $lang_list = null; |
6
|
|
|
public $active = null; |
7
|
|
|
|
8
|
|
|
public $enable_stat_usage = false; |
9
|
|
|
protected $stat_usage = array(); |
10
|
|
|
protected $stat_usage_new = array(); |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Порядок проверки языков |
14
|
|
|
* |
15
|
|
|
* @var array $fallback |
16
|
|
|
*/ |
17
|
|
|
protected $fallback = array(); |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @var classCache $cache |
21
|
|
|
*/ |
22
|
|
|
protected $cache = null; |
23
|
|
|
protected $cache_prefix = 'lng_'; |
24
|
|
|
protected $cache_prefix_lang = ''; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* ex $lang |
28
|
|
|
* |
29
|
|
|
* @var classLocale $lang |
30
|
|
|
*/ |
31
|
|
|
public static $lang = null; |
32
|
|
|
|
33
|
|
|
public function __construct($enable_stat_usage = false) { |
34
|
|
|
classSupernova::log_file('locale.__constructor: Starting', 1); |
35
|
|
|
|
36
|
|
|
$this->container = array(); |
37
|
|
|
|
38
|
|
|
if(classSupernova::$cache->getMode() != CACHER_NO_CACHE && !classSupernova::$config->locale_cache_disable) { |
39
|
|
|
$this->cache = classSupernova::$cache; |
40
|
|
|
classSupernova::log_file('locale.__constructor: Cache is present'); |
41
|
|
|
//$this->cache->unset_by_prefix($this->cache_prefix); // TODO - remove? 'cause debug! |
|
|
|
|
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
if($enable_stat_usage && empty($this->stat_usage)) { |
45
|
|
|
$this->enable_stat_usage = $enable_stat_usage; |
46
|
|
|
$this->usage_stat_load(); |
47
|
|
|
// TODO shutdown function |
48
|
|
|
register_shutdown_function(array($this, 'usage_stat_save')); |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
classSupernova::log_file("locale.__constructor: Switching language to default"); |
52
|
|
|
$this->lng_switch(DEFAULT_LANG); |
53
|
|
|
|
54
|
|
|
classSupernova::log_file("locale.__constructor: Complete - EXIT", -1); |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Фоллбэк для строки на другие локали |
59
|
|
|
* |
60
|
|
|
* @param array|string $offset |
61
|
|
|
*/ |
62
|
|
|
protected function locale_string_fallback($offset) { |
63
|
|
|
global $locale_cache_statistic; |
64
|
|
|
// Фоллбэк вызывается только если мы не нашли нужную строку в массиве... |
65
|
|
|
$fallback = $this->fallback; |
66
|
|
|
// ...поэтому $offset в активном языке заведомо нет |
67
|
|
|
unset($fallback[$this->active]); |
68
|
|
|
|
69
|
|
|
// Проходим по оставшимся локалям |
70
|
|
|
foreach($fallback as $try_language) { |
71
|
|
|
// Если нет такой строки - пытаемся вытащить из кэша |
72
|
|
|
if(!isset($this->container[$try_language][$offset]) && $this->cache) { |
73
|
|
|
$this->container[$try_language][$offset] = $this->cache->__get($this->cache_prefix . $try_language . '_' . $offset); |
74
|
|
|
// Записываем результат работы кэша |
75
|
|
|
$locale_cache_statistic['queries']++; |
76
|
|
|
isset($this->container[$try_language][$offset]) ? $locale_cache_statistic['hits']++ : $locale_cache_statistic['misses']++; |
77
|
|
|
!isset($this->container[$try_language][$offset]) ? $locale_cache_statistic['missed_str'][] = $this->cache_prefix . $try_language . '_' . $offset : false; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
// Если мы как-то где-то нашли строку... |
81
|
|
|
if(isset($this->container[$try_language][$offset])) { |
82
|
|
|
// ...значит она получена в результате фоллбэка и записываем её в кэш и контейнер |
83
|
|
|
$this[$offset] = $this->container[$try_language][$offset]; |
84
|
|
|
$locale_cache_statistic['fallbacks']++; |
85
|
|
|
break; |
86
|
|
|
} |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
public function offsetSet($offset, $value) { |
91
|
|
|
//pdump('set', $this->cache_prefix_lang . $offset); |
|
|
|
|
92
|
|
|
//pdump($this->container[$this->active][$offset]); |
93
|
|
|
if (is_null($offset)) { |
94
|
|
|
$this->container[$this->active][] = $value; |
95
|
|
View Code Duplication |
} else { |
|
|
|
|
96
|
|
|
$this->container[$this->active][$offset] = $value; |
97
|
|
|
if($this->cache) { |
98
|
|
|
$this->cache->__set($this->cache_prefix_lang . $offset, $value); |
99
|
|
|
} |
100
|
|
|
} |
101
|
|
|
} |
102
|
|
|
public function offsetExists($offset) { |
103
|
|
|
// Шорткат если у нас уже есть строка в памяти PHP |
104
|
|
|
if(!isset($this->container[$this->active][$offset])) { |
105
|
|
|
// pdump($this->cache_prefix_lang . $offset); |
|
|
|
|
106
|
|
View Code Duplication |
if(!$this->cache || !($this->container[$this->active][$offset] = $this->cache->__get($this->cache_prefix_lang . $offset))) { |
|
|
|
|
107
|
|
|
// pdump($this->cache_prefix_lang . $offset); |
|
|
|
|
108
|
|
|
// Если нету такой строки - делаем фоллбэк |
109
|
|
|
$this->locale_string_fallback($offset); |
110
|
|
|
} |
111
|
|
|
//pdump($offset); |
112
|
|
|
return isset($this->container[$this->active][$offset]); |
113
|
|
|
} else { |
114
|
|
|
return true; |
115
|
|
|
} |
116
|
|
|
} |
117
|
|
|
public function offsetUnset($offset) { |
118
|
|
|
unset($this->container[$this->active][$offset]); |
119
|
|
|
} |
120
|
|
|
public function offsetGet($offset) { |
121
|
|
|
$value = $this->offsetExists($offset) ? $this->container[$this->active][$offset] : null; |
122
|
|
|
if($this->enable_stat_usage) { |
123
|
|
|
$this->usage_stat_log($offset, $value); |
124
|
|
|
} |
125
|
|
|
return $value; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
|
129
|
|
|
public function merge($array) { |
130
|
|
|
$this->container[$this->active] = is_array($this->container[$this->active]) ? $this->container[$this->active] : array(); |
131
|
|
|
// $this->container[$this->active] = array_merge($this->container[$this->active], $array); |
|
|
|
|
132
|
|
|
$this->container[$this->active] = array_replace_recursive($this->container[$this->active], $array); |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
|
136
|
|
|
public function usage_stat_load() { |
137
|
|
|
$this->stat_usage = classSupernova::$cache->lng_stat_usage = array(); |
138
|
|
|
if(empty($this->stat_usage)) { |
139
|
|
|
$query = classSupernova::$db->doSelect("SELECT * FROM `{{lng_usage_stat}}`"); |
140
|
|
|
while($row = db_fetch($query)) { |
141
|
|
|
$this->stat_usage[$row['lang_code'] . ':' . $row['string_id'] . ':' . $row['file'] . ':' . $row['line']] = $row['is_empty']; |
142
|
|
|
} |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
public function usage_stat_save() { |
146
|
|
|
if(!empty($this->stat_usage_new)) { |
147
|
|
|
classSupernova::$cache->lng_stat_usage = $this->stat_usage; |
148
|
|
|
classSupernova::$db->doSelect("SELECT 1 FROM `{{lng_usage_stat}}` LIMIT 1"); |
149
|
|
|
// foreach($this->stat_usage_new as &$value) { |
|
|
|
|
150
|
|
|
// foreach($value as &$value2) { |
151
|
|
|
// $value2 = '"' . db_escape($value2) . '"'; |
152
|
|
|
// } |
153
|
|
|
// $value = '(' . implode(',', $value) .')'; |
154
|
|
|
// } |
155
|
|
|
classSupernova::$gc->db->doInsertBatch( |
|
|
|
|
156
|
|
|
'lng_usage_stat', $this->stat_usage_new, array( |
157
|
|
|
'lang_code', |
158
|
|
|
'string_id', |
159
|
|
|
'file', |
160
|
|
|
'line', |
161
|
|
|
'is_empty', |
162
|
|
|
'locale', |
163
|
|
|
), DB_INSERT_REPLACE |
164
|
|
|
); |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
public function usage_stat_log(&$offset, &$value) { |
168
|
|
|
$trace = debug_backtrace(); |
169
|
|
|
unset($trace[0]); |
170
|
|
|
unset($trace[1]['object']); |
171
|
|
|
|
172
|
|
|
$file = str_replace('\\', '/', substr($trace[1]['file'], strlen(SN_ROOT_PHYSICAL) - 1)); |
173
|
|
|
|
174
|
|
|
$string_id = $this->active . ':' . $offset . ':' . $file . ':' . $trace[1]['line']; |
175
|
|
|
if(!isset($this->stat_usage[$string_id]) || $this->stat_usage[$string_id] != empty($value)) { |
176
|
|
|
$this->stat_usage[$string_id] = empty($value); |
177
|
|
|
$this->stat_usage_new[] = array( |
178
|
|
|
'lang_code' => $this->active, |
179
|
|
|
'string_id' => $offset, |
180
|
|
|
'file' => $file, |
181
|
|
|
'line' => $trace[1]['line'], |
182
|
|
|
'is_empty' => intval(empty($value)), |
183
|
|
|
'locale' => '' . $value, |
184
|
|
|
); |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
|
189
|
|
|
protected function lng_try_filepath($path, $file_path_relative) { |
190
|
|
|
$file_path = SN_ROOT_PHYSICAL . ($path && file_exists(SN_ROOT_PHYSICAL . $path . $file_path_relative) ? $path : '') . $file_path_relative; |
191
|
|
|
return file_exists($file_path) ? $file_path : false; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
protected function make_fallback($language = '') { |
195
|
|
|
global $user; |
196
|
|
|
|
197
|
|
|
$this->fallback = array(); |
198
|
|
|
$language ? $this->fallback[$language] = $language : false; // Desired language |
199
|
|
|
$this->active ? $this->fallback[$this->active] = $this->active : false; // Active language |
200
|
|
|
// TODO - account_language |
201
|
|
|
!empty($user['lang']) ? $this->fallback[$user['lang']] = $user['lang'] : false; // Player language |
202
|
|
|
$this->fallback[DEFAULT_LANG] = DEFAULT_LANG; // Server default language |
203
|
|
|
$this->fallback['ru'] = 'ru'; // Russian |
204
|
|
|
$this->fallback['en'] = 'en'; // English |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
public function lng_include($filename, $path = '', $ext = '.mo.php') { |
208
|
|
|
global $language; |
209
|
|
|
|
210
|
|
|
classSupernova::log_file("locale.include: Loading data from domain '{$filename}'", 1); |
211
|
|
|
|
212
|
|
|
$cache_file_key = $this->cache_prefix_lang . '__' . $filename; |
213
|
|
|
|
214
|
|
|
// Подключен ли внешний кэш? |
215
|
|
|
if($this->cache) { |
216
|
|
|
// Загружен ли уже данный файл? |
217
|
|
|
$cache_file_status = $this->cache->__get($cache_file_key); |
218
|
|
|
classSupernova::log_file("locale.include: Cache - '{$filename}' has key '{$cache_file_key}' and is " . ($cache_file_status ? 'already loaded - EXIT' : 'EMPTY'), $cache_file_status ? -1 : 0); |
219
|
|
|
if($cache_file_status) { |
220
|
|
|
// Если да - повторять загрузку нет смысла |
221
|
|
|
return null; |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
// У нас нет внешнего кэша или в кэш не загружена данная локализация текущего файла |
226
|
|
|
|
227
|
|
|
$ext = $ext ? $ext : '.mo.php'; |
228
|
|
|
$filename_ext = "{$filename}{$ext}"; |
229
|
|
|
|
230
|
|
|
$this->make_fallback($language); |
231
|
|
|
|
232
|
|
|
$file_path = ''; |
233
|
|
|
foreach($this->fallback as $lang_try) { |
234
|
|
|
if(!$lang_try /* || isset($language_tried[$lang_try]) */) { |
|
|
|
|
235
|
|
|
continue; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
if($file_path = $this->lng_try_filepath($path, "language/{$lang_try}/{$filename_ext}")) { |
239
|
|
|
break; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
if($file_path = $this->lng_try_filepath($path, "language/{$filename}_{$lang_try}{$ext}")) { |
243
|
|
|
break; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
$file_path = ''; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
if($file_path) { |
250
|
|
|
$a_lang_array = array(); |
251
|
|
|
include($file_path); |
252
|
|
|
|
253
|
|
|
if(!empty($a_lang_array)) { |
254
|
|
|
$this->merge($a_lang_array); |
255
|
|
|
|
256
|
|
|
// Загрузка данных из файла в кэш |
257
|
|
|
if($this->cache) { |
258
|
|
|
classSupernova::log_file("Locale: loading '{$filename}' into cache"); |
259
|
|
|
foreach($a_lang_array as $key => $value) { |
260
|
|
|
$value_cache_key = $this->cache_prefix_lang . $key; |
261
|
|
|
if($this->cache->__isset($value_cache_key)) { |
262
|
|
|
if(is_array($value)) { |
263
|
|
|
$alt_value = $this->cache->__get($value_cache_key); |
264
|
|
|
$value = array_replace_recursive($alt_value, $value); |
265
|
|
|
// pdump($alt_value, $alt_value); |
|
|
|
|
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
$this->cache->__set($this->cache_prefix_lang . $key, $value); |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
if($this->cache) { |
274
|
|
|
$this->cache->__set($cache_file_key, true); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
unset($a_lang_array); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
classSupernova::log_file("locale.include: Complete - EXIT", -1); |
281
|
|
|
|
282
|
|
|
return null; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
public function lng_load_i18n($i18n) { |
286
|
|
|
if(!isset($i18n)) { |
287
|
|
|
return; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
foreach($i18n as $i18n_data) { |
291
|
|
|
if(is_string($i18n_data)) { |
292
|
|
|
$this->lng_include($i18n_data); |
293
|
|
|
} elseif(is_array($i18n_data)) { |
294
|
|
|
$this->lng_include($i18n_data['file'], $i18n_data['path']); |
295
|
|
|
} |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
return null; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
public function lng_switch($language_new) { |
302
|
|
|
global $language, $user; |
303
|
|
|
|
304
|
|
|
classSupernova::log_file("locale.switch: Request for switch to '{$language_new}'", 1); |
305
|
|
|
|
306
|
|
|
$language_new = str_replace(array('?', '&', 'lang='), '', $language_new); |
307
|
|
|
$language_new = $language_new ? $language_new : (!empty($user['lang']) ? $user['lang'] : DEFAULT_LANG); |
308
|
|
|
|
309
|
|
|
classSupernova::log_file("locale.switch: Trying to switch language to '{$language_new}'"); |
310
|
|
|
|
311
|
|
|
if($language_new == $this->active) { |
312
|
|
|
classSupernova::log_file("locale.switch: New language '{$language_new}' is equal to current language '{$this->active}' - EXIT", -1); |
313
|
|
|
return false; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
$this->active = $language = $language_new; |
317
|
|
|
$this->cache_prefix_lang = $this->cache_prefix . $this->active . '_'; |
318
|
|
|
|
319
|
|
|
$this['LANG_INFO'] = $this->lng_get_info($this->active); |
320
|
|
|
$this->make_fallback($this->active); |
321
|
|
|
|
322
|
|
|
if($this->cache) { |
323
|
|
|
$cache_lang_init_status = $this->cache->__get($this->cache_prefix_lang . '__INIT'); |
324
|
|
|
classSupernova::log_file("locale.switch: Cache for '{$this->active}' prefixed '{$this->cache_prefix_lang}' is " . ($cache_lang_init_status ? 'already loaded. Doing nothing - EXIT' : 'EMPTY'), $cache_lang_init_status ? -1 : 0); |
325
|
|
|
if($cache_lang_init_status) { |
326
|
|
|
return false; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
// Чистим текущие локализации из кэша. Достаточно почистить только флаги инициализации языкового кэша и загрузки файлов - они начинаются с '__' |
330
|
|
|
classSupernova::log_file("locale.switch: Cache - invalidating data"); |
331
|
|
|
$this->cache->unset_by_prefix($this->cache_prefix_lang . '__'); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
$this->lng_include('system'); |
335
|
|
|
// $this->lng_include('menu'); |
|
|
|
|
336
|
|
|
$this->lng_include('tech'); |
337
|
|
|
$this->lng_include('payment'); |
338
|
|
|
// Loading global language files |
339
|
|
|
$this->lng_load_i18n(classSupernova::$sn_mvc['i18n']['']); |
340
|
|
|
|
341
|
|
|
if($this->cache) { |
342
|
|
|
classSupernova::log_file("locale.switch: Cache - setting flag " . $this->cache_prefix_lang . '__INIT'); |
343
|
|
|
$this->cache->__set($this->cache_prefix_lang . '__INIT', true); |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
classSupernova::log_file("locale.switch: Complete - EXIT"); |
347
|
|
|
|
348
|
|
|
return true; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
|
352
|
|
|
public function lng_get_info($entry) { |
353
|
|
|
$file_name = SN_ROOT_PHYSICAL . 'language/' . $entry . '/language.mo.php'; |
354
|
|
|
$lang_info = array(); |
355
|
|
|
if(file_exists($file_name)) { |
356
|
|
|
include($file_name); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
return($lang_info); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
public function lng_get_list() { |
363
|
|
|
if(empty($this->lang_list)) { |
364
|
|
|
$this->lang_list = array(); |
365
|
|
|
|
366
|
|
|
$path = SN_ROOT_PHYSICAL . 'language/'; |
367
|
|
|
$dir = dir($path); |
368
|
|
|
while(false !== ($entry = $dir->read())) { |
369
|
|
|
if(is_dir($path . $entry) && $entry[0] != '.') { |
370
|
|
|
$lang_info = $this->lng_get_info($entry); |
371
|
|
|
if($lang_info['LANG_NAME_ISO2'] == $entry) { |
372
|
|
|
$this->lang_list[$lang_info['LANG_NAME_ISO2']] = $lang_info; |
373
|
|
|
} |
374
|
|
|
} |
375
|
|
|
} |
376
|
|
|
$dir->close(); |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
return $this->lang_list; |
380
|
|
|
} |
381
|
|
|
} |
382
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.