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