Issues (1369)

classes/classLocale.php (10 issues)

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) {
0 ignored issues
show
Bug Best Practice introduced by
The property locale_cache_disable does not exist on classConfig. Since you implemented __get, consider adding a @property annotation.
Loading history...
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);
0 ignored issues
show
Are you sure $offset of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

66
        $this->container[$try_language][$offset] = $this->cache->__get($this->cache_prefix . $try_language . '_' . /** @scrutinizer ignore-type */ $offset);
Loading history...
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}}");
0 ignored issues
show
Deprecated Code introduced by
The function doquery() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

134
      $query = /** @scrutinizer ignore-deprecated */ doquery("SELECT * FROM {{lng_usage_stat}}");
Loading history...
135
      while ($row = db_fetch($query)) {
0 ignored issues
show
Deprecated Code introduced by
The function db_fetch() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

135
      while ($row = /** @scrutinizer ignore-deprecated */ db_fetch($query)) {
Loading history...
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");
0 ignored issues
show
Deprecated Code introduced by
The function doquery() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

145
      /** @scrutinizer ignore-deprecated */ doquery("SELECT 1 FROM {{lng_usage_stat}} LIMIT 1");
Loading history...
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));
0 ignored issues
show
Deprecated Code introduced by
The function doquery() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

152
      /** @scrutinizer ignore-deprecated */ doquery("REPLACE INTO {{lng_usage_stat}} (lang_code,string_id,`file`,line,is_empty,locale) VALUES " . implode(',', $this->stat_usage_new));
Loading history...
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
The variable $empty seems to be never defined.
Loading history...
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
The variable $a_lang_array seems to never exist and therefore empty should always be true.
Loading history...
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);
0 ignored issues
show
It seems like $alt_value can also be of type null; however, parameter $array of array_replace_recursive() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

253
                $value     = array_replace_recursive(/** @scrutinizer ignore-type */ $alt_value, $value);
Loading history...
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
The variable $a_lang_array seems to be never defined.
Loading history...
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