This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
1 | <?php |
||||
2 | |||||
3 | namespace App; |
||||
4 | |||||
5 | /** |
||||
6 | * Language basic class. |
||||
7 | * |
||||
8 | * @package App |
||||
9 | * |
||||
10 | * @copyright YetiForce S.A. |
||||
11 | * @license YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com) |
||||
12 | * @author Mariusz Krzaczkowski <[email protected]> |
||||
13 | * @author Adrian Koń <[email protected]> |
||||
14 | * @author Radosław Skrzypczak <[email protected]> |
||||
15 | */ |
||||
16 | class Language |
||||
17 | { |
||||
18 | /** |
||||
19 | * Default language code. |
||||
20 | */ |
||||
21 | public const DEFAULT_LANG = 'en-US'; |
||||
22 | |||||
23 | /** |
||||
24 | * Allowed types of language variables. |
||||
25 | */ |
||||
26 | const LANG_TYPE = ['php', 'js']; |
||||
27 | |||||
28 | /** |
||||
29 | * Language files format. |
||||
30 | */ |
||||
31 | const FORMAT = 'json'; |
||||
32 | /** |
||||
33 | * Custom language directory. |
||||
34 | * |
||||
35 | * @var string |
||||
36 | */ |
||||
37 | public static $customDirectory = 'custom'; |
||||
38 | |||||
39 | /** @var string Current language. */ |
||||
40 | private static $language = ''; |
||||
41 | |||||
42 | /** @var string Temporary language. */ |
||||
43 | private static $temporaryLanguage = ''; |
||||
44 | |||||
45 | /** |
||||
46 | * Short current language. |
||||
47 | * |
||||
48 | * @var bool|string |
||||
49 | */ |
||||
50 | private static $shortLanguage = false; |
||||
51 | |||||
52 | /** |
||||
53 | * Pluralize cache. |
||||
54 | * |
||||
55 | * @var array |
||||
56 | */ |
||||
57 | private static $pluralizeCache = []; |
||||
58 | |||||
59 | /** |
||||
60 | * Contains module language translations. |
||||
61 | * |
||||
62 | * @var array |
||||
63 | */ |
||||
64 | protected static $languageContainer; |
||||
65 | |||||
66 | /** |
||||
67 | * Function that returns current language. |
||||
68 | * |
||||
69 | * @return string - |
||||
70 | */ |
||||
71 | public static function getLanguage() |
||||
72 | { |
||||
73 | if (static::$temporaryLanguage) { |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
74 | return static::$temporaryLanguage; |
||||
75 | } |
||||
76 | 90 | if (static::$language) { |
|||
0 ignored issues
–
show
|
|||||
77 | return static::$language; |
||||
78 | 90 | } |
|||
79 | 22 | if (!empty(\App\Session::get('language'))) { |
|||
80 | $language = \App\Session::get('language'); |
||||
81 | 71 | } else { |
|||
82 | 71 | $language = User::getCurrentUserModel()->getDetail('language'); |
|||
83 | } |
||||
84 | 1 | return static::$language = empty($language) ? \App\Config::main('default_language') : $language; |
|||
85 | } |
||||
86 | |||||
87 | 1 | /** |
|||
88 | * Get IETF language tag. |
||||
89 | 1 | * |
|||
90 | * @see https://en.wikipedia.org/wiki/IETF_language_tag |
||||
91 | * |
||||
92 | * @param mixed $separator |
||||
93 | * |
||||
94 | * @return string |
||||
95 | */ |
||||
96 | public static function getLanguageTag($separator = '_') |
||||
97 | { |
||||
98 | return str_replace('-', $separator, static::getLanguage()); |
||||
99 | } |
||||
100 | |||||
101 | /** |
||||
102 | * Set temporary language. |
||||
103 | * |
||||
104 | * @param string $language |
||||
105 | */ |
||||
106 | public static function setTemporaryLanguage(string $language) |
||||
107 | { |
||||
108 | static::$temporaryLanguage = $language; |
||||
0 ignored issues
–
show
|
|||||
109 | } |
||||
110 | |||||
111 | 17 | /** |
|||
112 | * Clear temporary language. |
||||
113 | 17 | * |
|||
114 | 17 | * @return string |
|||
115 | */ |
||||
116 | public static function clearTemporaryLanguage() |
||||
117 | { |
||||
118 | static::$temporaryLanguage = false; |
||||
0 ignored issues
–
show
The property
$temporaryLanguage was declared of type string , but false is of type false . Maybe add a type cast?
This check looks for assignments to scalar types that may be of the wrong type. To ensure the code behaves as expected, it may be a good idea to add an explicit type cast. $answer = 42;
$correct = false;
$correct = (bool) $answer;
![]() |
|||||
119 | } |
||||
120 | |||||
121 | 29 | /** |
|||
122 | * Function that returns current language short name. |
||||
123 | 29 | * |
|||
124 | 29 | * @return string |
|||
125 | */ |
||||
126 | public static function getShortLanguageName() |
||||
127 | { |
||||
128 | if (static::$shortLanguage) { |
||||
0 ignored issues
–
show
|
|||||
129 | return static::$shortLanguage; |
||||
0 ignored issues
–
show
|
|||||
130 | } |
||||
131 | 2 | preg_match('/^[a-z]+/i', static::getLanguage(), $match); |
|||
132 | return static::$shortLanguage = (empty($match[0])) ? \Locale::getPrimaryLanguage(self::DEFAULT_LANG) : $match[0]; |
||||
133 | 2 | } |
|||
134 | 2 | ||||
135 | /** |
||||
136 | 1 | * Function that returns region for language prefix. |
|||
137 | 1 | * |
|||
138 | * @param string|null $lang |
||||
139 | * |
||||
140 | * @return string |
||||
141 | */ |
||||
142 | public static function getLanguageRegion(?string $lang = null): string |
||||
143 | { |
||||
144 | if (!$lang) { |
||||
145 | $lang = static::getLanguage(); |
||||
146 | } |
||||
147 | return \Locale::parseLocale($lang)['region'] ?? substr($lang, -2); |
||||
148 | } |
||||
149 | |||||
150 | 83 | /** |
|||
151 | * Functions that gets translated string. |
||||
152 | 83 | * |
|||
153 | 1 | * @param string $key - string which need to be translated |
|||
154 | * @param string $moduleName - module scope in which the translation need to be check |
||||
155 | 83 | * @param string|null $language - language of translation |
|||
156 | 83 | * @param bool $encode - When no translation was found do encode the output |
|||
157 | * @param string $secondModuleName - Additional module name to be translated when not in $moduleName |
||||
158 | 83 | * |
|||
159 | 1 | * @return string - translated string |
|||
160 | 1 | */ |
|||
161 | public static function translate(string $key, string $moduleName = '_Base', ?string $language = null, bool $encode = true, ?string $secondModuleName = null) |
||||
162 | 83 | { |
|||
163 | 1 | if (empty($key)) { // nothing to translate |
|||
164 | return $key; |
||||
165 | 83 | } |
|||
166 | if (!$language || ($language && 5 !== \strlen($language))) { |
||||
167 | 83 | $language = static::getLanguage(); |
|||
168 | 83 | } |
|||
169 | 82 | if (\is_array($moduleName)) { |
|||
0 ignored issues
–
show
|
|||||
170 | 82 | Log::warning('Invalid module name - module: ' . var_export($moduleName, true)); |
|||
171 | return $key; |
||||
172 | } |
||||
173 | if (is_numeric($moduleName)) { // ok, we have a tab id, lets turn it into name |
||||
174 | $moduleName = Module::getModuleName($moduleName); |
||||
0 ignored issues
–
show
$moduleName of type string is incompatible with the type integer expected by parameter $tabId of App\Module::getModuleName() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
175 | 13 | } else { |
|||
176 | 2 | $moduleName = str_replace([':', '.'], [\DIRECTORY_SEPARATOR, \DIRECTORY_SEPARATOR], $moduleName); |
|||
177 | 2 | } |
|||
178 | 2 | static::loadLanguageFile($language, $moduleName); |
|||
179 | 1 | if (isset(static::$languageContainer[$language][$moduleName]['php'][$key])) { |
|||
180 | 1 | if ($encode) { |
|||
181 | return \nl2br(Purifier::encodeHtml(static::$languageContainer[$language][$moduleName]['php'][$key])); |
||||
182 | } |
||||
183 | return \nl2br(static::$languageContainer[$language][$moduleName]['php'][$key]); |
||||
184 | } |
||||
185 | 13 | if ($secondModuleName) { |
|||
186 | 13 | $secondModuleName = str_replace([':', '.'], [\DIRECTORY_SEPARATOR, \DIRECTORY_SEPARATOR], $secondModuleName); |
|||
187 | 11 | static::loadLanguageFile($language, $secondModuleName); |
|||
188 | 11 | if (isset(static::$languageContainer[$language][$secondModuleName]['php'][$key])) { |
|||
189 | if ($encode) { |
||||
190 | return \nl2br(Purifier::encodeHtml(static::$languageContainer[$language][$secondModuleName]['php'][$key])); |
||||
191 | } |
||||
192 | 4 | return \nl2br(static::$languageContainer[$language][$secondModuleName]['php'][$key]); |
|||
193 | } |
||||
194 | } |
||||
195 | 4 | // Lookup for the translation in base module, in case of sub modules, before ending up with common strings |
|||
196 | 4 | if (0 === strpos($moduleName, 'Settings')) { |
|||
197 | $base = 'Settings' . \DIRECTORY_SEPARATOR . '_Base'; |
||||
198 | static::loadLanguageFile($language, $base); |
||||
199 | if (isset(static::$languageContainer[$language][$base]['php'][$key])) { |
||||
200 | if ($encode) { |
||||
201 | return \nl2br(Purifier::encodeHtml(static::$languageContainer[$language][$base]['php'][$key])); |
||||
202 | } |
||||
203 | return \nl2br(static::$languageContainer[$language][$base]['php'][$key]); |
||||
204 | } |
||||
205 | } |
||||
206 | static::loadLanguageFile($language); |
||||
207 | if (isset(static::$languageContainer[$language]['_Base']['php'][$key])) { |
||||
208 | if ($encode) { |
||||
209 | return \nl2br(Purifier::encodeHtml(static::$languageContainer[$language]['_Base']['php'][$key])); |
||||
210 | } |
||||
211 | return \nl2br(static::$languageContainer[$language]['_Base']['php'][$key]); |
||||
212 | } |
||||
213 | if (\App\Config::performance('recursiveTranslate') && static::DEFAULT_LANG !== $language) { |
||||
214 | return static::translate($key, $moduleName, static::DEFAULT_LANG, $encode, $secondModuleName); |
||||
215 | } |
||||
216 | \App\Log::info("Cannot translate this: '$key' for module '$moduleName', lang: $language"); |
||||
217 | |||||
218 | return $encode ? Purifier::encodeHtml($key) : $key; |
||||
219 | } |
||||
220 | |||||
221 | /** |
||||
222 | * Functions get translate help info. |
||||
223 | * |
||||
224 | * @param \Vtiger_Field_Model $fieldModel |
||||
225 | * @param string $view |
||||
226 | * |
||||
227 | * @return string |
||||
228 | */ |
||||
229 | public static function getTranslateHelpInfo(\Vtiger_Field_Model $fieldModel, string $view): string |
||||
230 | { |
||||
231 | 1 | $translate = ''; |
|||
232 | if (\in_array($view, explode(',', $fieldModel->get('helpinfo')))) { |
||||
233 | 1 | $label = $fieldModel->getFieldLabel(); |
|||
0 ignored issues
–
show
The function
Vtiger_Field_Model::getFieldLabel() has been deprecated: 7.0 Use $this->getLabel()
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||
234 | $key = "{$fieldModel->getModuleName()}|$label"; |
||||
235 | if (($translated = self::translateSingleMod($key, 'Other:HelpInfo')) !== $key) { |
||||
236 | $translate = $translated; |
||||
237 | } elseif (($translated = self::translateSingleMod($label, 'Other:HelpInfo')) !== $label) { |
||||
238 | $translate = $translated; |
||||
239 | } |
||||
240 | } |
||||
241 | return $translate; |
||||
242 | } |
||||
243 | |||||
244 | 1 | /** |
|||
245 | * Functions that gets translated string with encoding html. |
||||
246 | 1 | * |
|||
247 | 1 | * @param string $key - string which need to be translated |
|||
248 | 1 | * @param string $moduleName - module scope in which the translation need to be check |
|||
249 | 1 | * @param mixed $currentLanguage |
|||
250 | * |
||||
251 | 1 | * @return string - translated string with encoding html |
|||
252 | */ |
||||
253 | public static function translateEncodeHtml($key, $moduleName = '_Base', $currentLanguage = false) |
||||
254 | { |
||||
255 | return \App\Purifier::encodeHtml(static::translate($key, $moduleName, $currentLanguage)); |
||||
256 | } |
||||
257 | |||||
258 | /** |
||||
259 | * Functions that gets translated string by $args. |
||||
260 | * |
||||
261 | * @param string $key - string which need to be translated |
||||
262 | * @param string $moduleName - module scope in which the translation need to be check |
||||
263 | * |
||||
264 | * @return string - translated string |
||||
265 | */ |
||||
266 | 2 | public static function translateArgs($key, $moduleName = '_Base') |
|||
267 | { |
||||
268 | 2 | $formattedString = static::translate($key, $moduleName); |
|||
269 | $args = \array_slice(\func_get_args(), 2); |
||||
270 | if (\is_array($args) && !empty($args)) { |
||||
271 | 2 | $formattedString = \call_user_func_array('vsprintf', [$formattedString, $args]); |
|||
272 | } |
||||
273 | 2 | return $formattedString; |
|||
274 | } |
||||
275 | |||||
276 | /** |
||||
277 | * Functions that gets pluralized translated string. |
||||
278 | * |
||||
279 | * @param string $key String which need to be translated |
||||
280 | * @param string $moduleName Module scope in which the translation need to be check |
||||
281 | * @param int $count Quantityu for plural determination |
||||
282 | * |
||||
283 | * @see https://www.i18next.com/plurals.html |
||||
284 | * @see https://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html?id=l10n/pluralforms#pluralforms-list |
||||
285 | 3 | * |
|||
286 | * @return string |
||||
287 | 3 | */ |
|||
288 | 3 | public static function translatePluralized($key, $moduleName, $count) |
|||
289 | { |
||||
290 | 3 | if (isset(static::$pluralizeCache[$count])) { |
|||
0 ignored issues
–
show
|
|||||
291 | 3 | $postfix = static::$pluralizeCache[$count]; |
|||
292 | 3 | } else { |
|||
293 | 2 | $postfix = static::getPluralized((int) $count); |
|||
294 | } |
||||
295 | 1 | return vsprintf(static::translate($key . $postfix, $moduleName), [$count]); |
|||
296 | } |
||||
297 | |||||
298 | /** |
||||
299 | * Translation function based on only one file. |
||||
300 | * |
||||
301 | * @param string $key |
||||
302 | * @param string $moduleName |
||||
303 | * @param bool|string $language |
||||
304 | * @param mixed $encode |
||||
305 | 1 | * |
|||
306 | * @return string |
||||
307 | 1 | */ |
|||
308 | public static function translateSingleMod($key, $moduleName = '_Base', $language = false, $encode = true) |
||||
309 | { |
||||
310 | if (!$language) { |
||||
311 | $language = static::getLanguage(); |
||||
312 | } |
||||
313 | $moduleName = str_replace([':', '.'], [\DIRECTORY_SEPARATOR, \DIRECTORY_SEPARATOR], $moduleName); |
||||
314 | static::loadLanguageFile($language, $moduleName); |
||||
315 | if (isset(static::$languageContainer[$language][$moduleName]['php'][$key])) { |
||||
316 | if ($encode) { |
||||
317 | 1 | return Purifier::encodeHtml(static::$languageContainer[$language][$moduleName]['php'][$key]); |
|||
318 | } |
||||
319 | 1 | return static::$languageContainer[$language][$moduleName]['php'][$key]; |
|||
320 | } |
||||
321 | if (\App\Config::performance('recursiveTranslate') && static::DEFAULT_LANG !== $language) { |
||||
322 | return static::translateSingleMod($key, $moduleName, static::DEFAULT_LANG, $encode); |
||||
323 | } |
||||
324 | return $encode ? Purifier::encodeHtml($key) : $key; |
||||
325 | } |
||||
326 | |||||
327 | /** |
||||
328 | 88 | * Get singular module name. |
|||
329 | * |
||||
330 | 88 | * @param string $moduleName |
|||
331 | 11 | * |
|||
332 | * @return string |
||||
333 | */ |
||||
334 | 11 | public static function getSingularModuleName($moduleName) |
|||
335 | 11 | { |
|||
336 | 11 | return "SINGLE_$moduleName"; |
|||
337 | 11 | } |
|||
338 | 10 | ||||
339 | /** |
||||
340 | 11 | * Translate singular module name. |
|||
341 | 11 | * |
|||
342 | * @param string $moduleName |
||||
343 | * |
||||
344 | * @return string |
||||
345 | */ |
||||
346 | public static function translateSingularModuleName($moduleName) |
||||
347 | { |
||||
348 | return static::translate("SINGLE_$moduleName", $moduleName); |
||||
349 | 11 | } |
|||
350 | 3 | ||||
351 | /** |
||||
352 | 11 | * Load language file. |
|||
353 | * |
||||
354 | * @param string $language |
||||
355 | 88 | * @param string $moduleName |
|||
356 | */ |
||||
357 | public static function loadLanguageFile($language, $moduleName = '_Base') |
||||
358 | { |
||||
359 | if (!isset(static::$languageContainer[$language][$moduleName])) { |
||||
360 | if (Cache::has('LanguageFiles', $language . $moduleName)) { |
||||
361 | static::$languageContainer[$language][$moduleName] = Cache::get('LanguageFiles', $language . $moduleName); |
||||
362 | } else { |
||||
363 | static::$languageContainer[$language][$moduleName] = []; |
||||
364 | $file = \DIRECTORY_SEPARATOR . 'languages' . \DIRECTORY_SEPARATOR . $language . \DIRECTORY_SEPARATOR . $moduleName . '.' . static::FORMAT; |
||||
365 | 1 | $langFile = ROOT_DIRECTORY . $file; |
|||
366 | if (file_exists($langFile)) { |
||||
367 | 1 | static::$languageContainer[$language][$moduleName] = Json::decode(file_get_contents($langFile), true) ?? []; |
|||
0 ignored issues
–
show
true of type true is incompatible with the type integer expected by parameter $objectDecodeType of App\Json::decode() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() $langFile can contain request data and is used in file inclusion context(s) leading to a potential security vulnerability.
5 paths for user data to reach this point
2. Path:
Read from
$_REQUEST, and Request::__construct() is called
in api/webservice/Core/Request.php on line 56
5. Path:
Read tainted data from array, and Data is passed through
explode() , and explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) is assigned to $code
in install/views/Index.php on line 74
General Strategies to prevent injectionIn general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:
if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
throw new \InvalidArgumentException('This input is not allowed.');
}
For numeric data, we recommend to explicitly cast the data: $sanitized = (integer) $tainted;
![]() |
|||||
368 | 1 | } |
|||
369 | 1 | $langCustomFile = ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . static::$customDirectory . $file; |
|||
370 | if (file_exists($langCustomFile)) { |
||||
371 | $translation = Json::decode(file_get_contents($langCustomFile), true) ?? []; |
||||
0 ignored issues
–
show
$langCustomFile can contain request data and is used in file inclusion context(s) leading to a potential security vulnerability.
5 paths for user data to reach this point
2. Path:
Read from
$_REQUEST, and Request::__construct() is called
in api/webservice/Core/Request.php on line 56
5. Path:
Read tainted data from array, and Data is passed through
explode() , and explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) is assigned to $code
in install/views/Index.php on line 74
General Strategies to prevent injectionIn general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:
if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
throw new \InvalidArgumentException('This input is not allowed.');
}
For numeric data, we recommend to explicitly cast the data: $sanitized = (integer) $tainted;
![]() |
|||||
372 | foreach ($translation as $type => $rows) { |
||||
373 | foreach ($rows as $key => $val) { |
||||
374 | static::$languageContainer[$language][$moduleName][$type][$key] = $val; |
||||
375 | } |
||||
376 | } |
||||
377 | } |
||||
378 | if (!file_exists($langFile) && !file_exists($langCustomFile)) { |
||||
379 | \App\Log::info("Language file does not exist, module: $moduleName ,language: $language"); |
||||
380 | 1 | } |
|||
381 | Cache::save('LanguageFiles', $language . $moduleName, static::$languageContainer[$language][$moduleName], Cache::LONG); |
||||
382 | 1 | } |
|||
383 | 1 | } |
|||
384 | 1 | } |
|||
385 | 1 | ||||
386 | 1 | /** |
|||
387 | 1 | * Get language from file. |
|||
388 | * |
||||
389 | 1 | * @param string $moduleName |
|||
390 | 1 | * @param string $language |
|||
391 | 1 | * |
|||
392 | 1 | * @return array |
|||
393 | 1 | */ |
|||
394 | public static function getFromFile($moduleName, $language) |
||||
395 | { |
||||
396 | 1 | static::loadLanguageFile($language, $moduleName); |
|||
397 | 1 | if (isset(static::$languageContainer[$language][$moduleName])) { |
|||
398 | 1 | return static::$languageContainer[$language][$moduleName]; |
|||
399 | } |
||||
400 | 1 | } |
|||
401 | |||||
402 | /** |
||||
403 | * Functions that gets translated string. |
||||
404 | * |
||||
405 | * @param string $moduleName |
||||
406 | * |
||||
407 | * @return string[] |
||||
408 | */ |
||||
409 | public static function getJsStrings($moduleName) |
||||
410 | { |
||||
411 | $language = static::getLanguage(); |
||||
412 | $moduleName = str_replace([':', '.'], [\DIRECTORY_SEPARATOR, \DIRECTORY_SEPARATOR], $moduleName); |
||||
413 | static::loadLanguageFile($language, $moduleName); |
||||
414 | $return = []; |
||||
415 | if (isset(static::$languageContainer[$language][$moduleName]['js'])) { |
||||
416 | $return = static::$languageContainer[$language][$moduleName]['js']; |
||||
417 | 2 | } |
|||
418 | if (0 === strpos($moduleName, 'Settings')) { |
||||
419 | $base = 'Settings' . \DIRECTORY_SEPARATOR . '_Base'; |
||||
420 | 2 | static::loadLanguageFile($language, $base); |
|||
421 | if (isset(static::$languageContainer[$language][$base]['js'])) { |
||||
422 | $return = array_merge(static::$languageContainer[$language][$base]['js'], $return); |
||||
423 | 2 | } |
|||
424 | } |
||||
425 | static::loadLanguageFile($language); |
||||
426 | 2 | if (isset(static::$languageContainer[$language]['_Base']['js'])) { |
|||
427 | $return = array_merge(static::$languageContainer[$language]['_Base']['js'], $return); |
||||
428 | } |
||||
429 | return $return; |
||||
430 | 2 | } |
|||
431 | |||||
432 | /** |
||||
433 | 2 | * This function returns the modified keycode to match the plural form(s) of a given language and a given count with the same pattern used by i18next JS library |
|||
434 | * Global patterns for keycode are as below : |
||||
435 | * - No plural form : only one non modified key is needed :) |
||||
436 | * - 2 forms : unmodified key for singular values and 'key_PLURAL' for plural values |
||||
437 | * - 3 or more forms : key_X with X indented for each plural form. |
||||
438 | * |
||||
439 | * @see https://www.i18next.com/plurals.html for some examples |
||||
440 | * @see https://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html?id=l10n/pluralforms for whole plural rules used by getText |
||||
441 | 2 | * |
|||
442 | 2 | * @param float $count Quantityu for plural determination |
|||
443 | * |
||||
444 | 2 | * @return string Pluralized key to look for |
|||
445 | 2 | */ |
|||
446 | 2 | private static function getPluralized($count) |
|||
447 | 2 | { |
|||
448 | 2 | //Extract language code from locale with special cases |
|||
449 | 2 | if (0 === strcasecmp(static::getLanguage(), 'pt-BR')) { |
|||
450 | $lang = 'pt-BR'; |
||||
451 | } else { |
||||
452 | $lang = static::getShortLanguageName(); |
||||
453 | } |
||||
454 | //No plural form |
||||
455 | if (\in_array($lang, ['ay', 'bo', 'cgg', 'dz', 'id', 'ja', 'jbo', 'ka', 'km', 'ko', 'lo', 'ms', 'my', 'sah', 'su', 'th', 'tt', 'ug', 'vi', 'wo', 'zh'])) { |
||||
456 | return '_0'; |
||||
457 | } |
||||
458 | //Two plural forms |
||||
459 | if (\in_array($lang, ['ach', 'ak', 'am', 'arn', 'br', 'fa', 'fil', 'fr', 'gun', 'ln', 'mfe', 'mg', 'mi', 'oc', 'pt-BR', 'tg', 'ti', 'tr', 'uz', 'wa'])) { |
||||
460 | 2 | return ($count > 1) ? '_1' : '_0'; |
|||
461 | 2 | } |
|||
462 | if (\in_array($lang, [ |
||||
463 | 'af', 'an', 'anp', 'as', 'ast', 'az', 'bg', 'bn', 'brx', 'ca', 'da', 'de', 'doi', 'dz', 'el', 'en', 'eo', 'es', 'et', 'eu', 'ff', 'fi', 'fo', 'fur', 'fy', |
||||
464 | 'gl', 'gu', 'ha', 'he', 'hi', 'hne', 'hu', 'hy', 'ia', 'it', 'kk', 'kl', 'kn', 'ku', 'ky', 'lb', 'mai', 'mk', 'ml', 'mn', 'mni', 'mr', 'nah', 'nap', |
||||
465 | 'nb', 'ne', 'nl', 'nn', 'nso', 'or', 'pa', 'pap', 'pms', 'ps', 'pt', 'rm', 'rw', 'sat', 'sco', 'sd', 'se', 'si', 'so', 'son', 'sq', 'sv', 'sw', |
||||
466 | 'ta', 'te', 'tk', 'ur', 'yo', |
||||
467 | ])) { |
||||
468 | return (1 !== $count) ? '_1' : '_0'; |
||||
0 ignored issues
–
show
|
|||||
469 | } |
||||
470 | 2 | switch ($lang) { |
|||
471 | case 'is': |
||||
472 | return (1 !== $count % 10 || 11 === $count % 100) ? '_1' : '_0'; |
||||
473 | case 'be': |
||||
474 | case 'bs': |
||||
475 | case 'hr': |
||||
476 | case 'ru': |
||||
477 | case 'sr': |
||||
478 | case 'uk': |
||||
479 | $i = $count % 10; |
||||
480 | $j = $count % 100; |
||||
481 | 2 | if (1 === $i && 11 !== $j) { |
|||
482 | return '_0'; |
||||
483 | } |
||||
484 | if ($i >= 2 && $i <= 4 && ($j < 10 || $j >= 20)) { |
||||
485 | return '_1'; |
||||
486 | } |
||||
487 | |||||
488 | return '_2'; |
||||
489 | case 'cs': |
||||
490 | case 'sk': |
||||
491 | if (1 === $count) { |
||||
0 ignored issues
–
show
|
|||||
492 | 2 | return '_0'; |
|||
493 | } |
||||
494 | if ($count >= 2 && $count <= 4) { |
||||
495 | return '_1'; |
||||
496 | } |
||||
497 | |||||
498 | return '_2'; |
||||
499 | case 'csb': |
||||
500 | $i = $count % 10; |
||||
501 | $j = $count % 100; |
||||
502 | if (1 === $count) { |
||||
0 ignored issues
–
show
|
|||||
503 | 2 | return '_0'; |
|||
504 | } |
||||
505 | if ($i >= 2 && $i <= 4 && ($j < 10 || $j >= 20)) { |
||||
506 | return '_1'; |
||||
507 | } |
||||
508 | |||||
509 | return '_2'; |
||||
510 | case 'lt': |
||||
511 | $i = $count % 10; |
||||
512 | $j = $count % 100; |
||||
513 | if (1 == $i && 11 != $j) { |
||||
514 | 2 | return '_0'; |
|||
515 | 2 | } |
|||
516 | 2 | if ($i >= 2 && ($j < 10 || $j >= 20)) { |
|||
517 | 2 | return '_1'; |
|||
518 | 2 | } |
|||
519 | |||||
520 | 2 | return '_2'; |
|||
521 | 2 | case 'lv': |
|||
522 | $i = $count % 10; |
||||
523 | $j = $count % 100; |
||||
524 | 2 | if (1 == $i && 11 != $j) { |
|||
525 | return '_0'; |
||||
526 | } |
||||
527 | if (0 !== $count) { |
||||
0 ignored issues
–
show
|
|||||
528 | return '_1'; |
||||
529 | } |
||||
530 | |||||
531 | return '_2'; |
||||
532 | case 'me': |
||||
533 | $i = $count % 10; |
||||
534 | $j = $count % 100; |
||||
535 | if (1 === $i && 11 !== $j) { |
||||
536 | return '_0'; |
||||
537 | } |
||||
538 | if ($i >= 2 && $i <= 4 && ($j < 10 || $j >= 20)) { |
||||
539 | return '_1'; |
||||
540 | } |
||||
541 | |||||
542 | return '_2'; |
||||
543 | case 'pl': |
||||
544 | $i = $count % 10; |
||||
545 | $j = $count % 100; |
||||
546 | if (1 === $count) { |
||||
0 ignored issues
–
show
|
|||||
547 | return '_0'; |
||||
548 | } |
||||
549 | if ($i >= 2 && $i <= 4 && ($j < 10 || $j >= 20)) { |
||||
550 | return '_1'; |
||||
551 | } |
||||
552 | |||||
553 | return '_2'; |
||||
554 | case 'ro': |
||||
555 | $j = $count % 100; |
||||
556 | if (1 === $count) { |
||||
0 ignored issues
–
show
|
|||||
557 | return '_0'; |
||||
558 | } |
||||
559 | if (0 === $count || ($j > 0 && $j < 20)) { |
||||
560 | return '_1'; |
||||
561 | } |
||||
562 | |||||
563 | return '_2'; |
||||
564 | case 'cy': |
||||
565 | if (1 === $count) { |
||||
0 ignored issues
–
show
|
|||||
566 | return '_0'; |
||||
567 | } |
||||
568 | if (2 === $count) { |
||||
0 ignored issues
–
show
|
|||||
569 | return '_1'; |
||||
570 | } |
||||
571 | if (8 !== $count && 11 !== $count) { |
||||
0 ignored issues
–
show
|
|||||
572 | return '_2'; |
||||
573 | } |
||||
574 | |||||
575 | return '_3'; |
||||
576 | case 'gd': |
||||
577 | if (1 === $count || 11 === $count) { |
||||
0 ignored issues
–
show
|
|||||
578 | return '_0'; |
||||
579 | } |
||||
580 | if (2 === $count || 12 === $count) { |
||||
0 ignored issues
–
show
|
|||||
581 | return '_1'; |
||||
582 | } |
||||
583 | if ($count > 2 && $count < 20) { |
||||
584 | return '_2'; |
||||
585 | } |
||||
586 | |||||
587 | return '_3'; |
||||
588 | case 'kw': |
||||
589 | if (1 === $count) { |
||||
0 ignored issues
–
show
|
|||||
590 | return '_0'; |
||||
591 | } |
||||
592 | if (2 === $count) { |
||||
0 ignored issues
–
show
|
|||||
593 | return '_1'; |
||||
594 | } |
||||
595 | if (3 === $count) { |
||||
0 ignored issues
–
show
|
|||||
596 | return '_2'; |
||||
597 | } |
||||
598 | |||||
599 | return '_3'; |
||||
600 | case 'mt': |
||||
601 | $j = $count % 100; |
||||
602 | if (1 === $count) { |
||||
0 ignored issues
–
show
|
|||||
603 | return '_0'; |
||||
604 | } |
||||
605 | if (0 === $count || ($j > 1 && $j < 11)) { |
||||
606 | return '_1'; |
||||
607 | } |
||||
608 | if ($j > 10 && $j < 20) { |
||||
609 | return '_2'; |
||||
610 | } |
||||
611 | |||||
612 | return '_3'; |
||||
613 | case 'sl': |
||||
614 | $j = $count % 100; |
||||
615 | if (1 === $j) { |
||||
616 | return '_0'; |
||||
617 | } |
||||
618 | if (2 === $j) { |
||||
619 | return '_1'; |
||||
620 | } |
||||
621 | if (3 === $j || 4 === $j) { |
||||
622 | return '_2'; |
||||
623 | } |
||||
624 | |||||
625 | return '_3'; |
||||
626 | case 'ga': |
||||
627 | if (1 === $count) { |
||||
0 ignored issues
–
show
|
|||||
628 | return '_0'; |
||||
629 | } |
||||
630 | if (2 === $count) { |
||||
0 ignored issues
–
show
|
|||||
631 | return '_1'; |
||||
632 | } |
||||
633 | if ($count > 2 && $count < 7) { |
||||
634 | return '_2'; |
||||
635 | } |
||||
636 | if ($count > 6 && $count < 11) { |
||||
637 | return '_3'; |
||||
638 | } |
||||
639 | |||||
640 | return '_4'; |
||||
641 | case 'ar': |
||||
642 | 2 | if (0 === $count) { |
|||
0 ignored issues
–
show
|
|||||
643 | return '_0'; |
||||
644 | 2 | } |
|||
645 | if (1 === $count) { |
||||
0 ignored issues
–
show
|
|||||
646 | return '_1'; |
||||
647 | } |
||||
648 | if (2 === $count) { |
||||
0 ignored issues
–
show
|
|||||
649 | return '_2'; |
||||
650 | } |
||||
651 | if ($count % 100 >= 3 && $count % 100 <= 10) { |
||||
652 | return '_3'; |
||||
653 | } |
||||
654 | if ($count * 100 >= 11) { |
||||
655 | 5 | return '_4'; |
|||
656 | } |
||||
657 | 5 | ||||
658 | 5 | return '_5'; |
|||
659 | 2 | default: |
|||
660 | 1 | return ''; |
|||
661 | } |
||||
662 | 1 | } |
|||
663 | |||||
664 | 5 | /** |
|||
665 | 5 | * Function to get the label name of the Langauge package. |
|||
666 | 5 | * |
|||
667 | 5 | * @param string $prefix |
|||
668 | 5 | * |
|||
669 | 5 | * @return bool|string |
|||
670 | 5 | */ |
|||
671 | public static function getLanguageLabel(string $prefix) |
||||
672 | 5 | { |
|||
673 | return static::getLangInfo($prefix)['name'] ?? null; |
||||
674 | 5 | } |
|||
675 | 5 | ||||
676 | 5 | /** |
|||
677 | 5 | * Function return languages data. |
|||
678 | 4 | * |
|||
679 | * @param bool $active |
||||
680 | 1 | * @param bool $allData |
|||
681 | * |
||||
682 | * @return array |
||||
683 | */ |
||||
684 | public static function getAll(bool $active = true, bool $allData = false) |
||||
685 | { |
||||
686 | $cacheKey = $active ? 'Active' : 'All'; |
||||
687 | if (Cache::has('getAllLanguages', $cacheKey)) { |
||||
688 | if (!$allData) { |
||||
689 | return array_column(Cache::get('getAllLanguages', $cacheKey), 'name', 'prefix'); |
||||
690 | 5 | } |
|||
691 | return Cache::get('getAllLanguages', $cacheKey); |
||||
692 | 5 | } |
|||
693 | 2 | $all = []; |
|||
694 | $actives = []; |
||||
695 | 3 | $dataReader = (new Db\Query())->from('vtiger_language')->createCommand()->query(); |
|||
696 | while ($row = $dataReader->read()) { |
||||
697 | $all[$row['prefix']] = $row; |
||||
698 | if (1 === (int) $row['active']) { |
||||
699 | $actives[$row['prefix']] = $row; |
||||
700 | } |
||||
701 | Cache::save('getLangInfo', $row['prefix'], $row); |
||||
702 | } |
||||
703 | $dataReader->close(); |
||||
704 | Cache::save('getAllLanguages', 'All', $all); |
||||
705 | Cache::save('getAllLanguages', 'Active', $actives); |
||||
706 | if (!$allData) { |
||||
707 | return array_column(Cache::get('getAllLanguages', $cacheKey), 'name', 'prefix'); |
||||
708 | } |
||||
709 | return Cache::get('getAllLanguages', $cacheKey); |
||||
710 | 1 | } |
|||
711 | |||||
712 | 1 | /** |
|||
713 | 1 | * Function return languange data. |
|||
714 | 1 | * |
|||
715 | 1 | * @param string $prefix |
|||
716 | 1 | * |
|||
717 | * @return array |
||||
718 | 1 | */ |
|||
719 | 1 | public static function getLangInfo(string $prefix) |
|||
720 | 1 | { |
|||
721 | 1 | if (Cache::has('getLangInfo', $prefix)) { |
|||
722 | 1 | return Cache::get('getLangInfo', $prefix); |
|||
723 | } |
||||
724 | return Cache::save('getLangInfo', $prefix, (new Db\Query())->from('vtiger_language')->where(['prefix' => $prefix])->one()); |
||||
0 ignored issues
–
show
|
|||||
725 | } |
||||
726 | |||||
727 | 1 | /** |
|||
728 | 1 | * Translation modification. |
|||
729 | 1 | * |
|||
730 | * @param string $language |
||||
731 | 1 | * @param string $fileName |
|||
732 | * @param string $type |
||||
733 | * @param string $label |
||||
734 | 1 | * @param string $translation |
|||
735 | 1 | * @param bool $remove |
|||
736 | * |
||||
737 | * @throws Exceptions\AppException |
||||
738 | */ |
||||
739 | public static function translationModify(string $language, string $fileName, string $type, string $label, string $translation, bool $remove = false) |
||||
740 | 1 | { |
|||
741 | $fileLocation = explode('__', $fileName, 2); |
||||
742 | 1 | array_unshift($fileLocation, 'custom', 'languages', $language); |
|||
743 | 1 | $fileDirectory = ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . implode(\DIRECTORY_SEPARATOR, $fileLocation) . '.' . static::FORMAT; |
|||
744 | 1 | if (file_exists($fileDirectory)) { |
|||
745 | 1 | $translations = Json::decode(file_get_contents($fileDirectory), true); |
|||
0 ignored issues
–
show
true of type true is incompatible with the type integer expected by parameter $objectDecodeType of App\Json::decode() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
746 | 1 | } else { |
|||
747 | 1 | $loc = ''; |
|||
748 | 1 | array_pop($fileLocation); |
|||
749 | 1 | foreach ($fileLocation as $name) { |
|||
750 | $loc .= \DIRECTORY_SEPARATOR . $name; |
||||
751 | 1 | if (!file_exists(ROOT_DIRECTORY . $loc) && !mkdir(ROOT_DIRECTORY . $loc, 0755)) { |
|||
752 | 1 | throw new Exceptions\AppException('ERR_NO_PERMISSIONS_TO_CREATE_DIRECTORIES'); |
|||
753 | 1 | } |
|||
754 | } |
||||
755 | } |
||||
756 | $translations[$type][$label] = $translation; |
||||
757 | if ($remove) { |
||||
758 | 1 | unset($translations[$type][$label]); |
|||
759 | 1 | } |
|||
760 | if (false === Json::save($fileDirectory, $translations)) { |
||||
761 | throw new Exceptions\AppException('ERR_CREATE_FILE_FAILURE'); |
||||
762 | 1 | } |
|||
763 | Cache::delete('LanguageFiles', $language . str_replace('__', \DIRECTORY_SEPARATOR, $fileName)); |
||||
764 | } |
||||
765 | |||||
766 | /** |
||||
767 | * Set locale information. |
||||
768 | */ |
||||
769 | public static function initLocale() |
||||
770 | { |
||||
771 | $original = explode(';', setlocale(LC_ALL, 0)); |
||||
772 | $defaultCharset = strtolower(\App\Config::main('default_charset')); |
||||
773 | setlocale( |
||||
774 | LC_ALL, |
||||
775 | \Locale::acceptFromHttp(self::getLanguage()) . '.' . $defaultCharset, |
||||
776 | \Locale::acceptFromHttp(\App\Config::main('default_language')) . '.' . $defaultCharset, |
||||
777 | \Locale::acceptFromHttp(self::DEFAULT_LANG) . ".$defaultCharset", |
||||
778 | \Locale::acceptFromHttp(self::DEFAULT_LANG) . '.utf8' |
||||
779 | ); |
||||
780 | foreach ($original as $localeSetting) { |
||||
781 | if (false !== strpos($localeSetting, '=')) { |
||||
782 | [$category, $locale] = explode('=', $localeSetting); |
||||
783 | } else { |
||||
784 | $category = 'LC_ALL'; |
||||
785 | $locale = $localeSetting; |
||||
786 | } |
||||
787 | if ('LC_COLLATE' !== $category && 'LC_CTYPE' !== $category && \defined($category)) { |
||||
788 | setlocale(\constant($category), $locale); |
||||
789 | } |
||||
790 | } |
||||
791 | } |
||||
792 | |||||
793 | /** |
||||
794 | * Get display language name. |
||||
795 | * |
||||
796 | * @param string $prefix |
||||
797 | * |
||||
798 | * @return string |
||||
799 | */ |
||||
800 | public static function getDisplayName(string $prefix) |
||||
801 | { |
||||
802 | return Utils::mbUcfirst(locale_get_region($prefix) === strtoupper(locale_get_primary_language($prefix)) ? locale_get_display_language($prefix, $prefix) : locale_get_display_name($prefix, $prefix)); |
||||
803 | } |
||||
804 | |||||
805 | /** |
||||
806 | * Get region from language prefix. |
||||
807 | * |
||||
808 | * @param string $prefix |
||||
809 | * |
||||
810 | * @return mixed |
||||
811 | */ |
||||
812 | public static function getRegion(string $prefix) |
||||
813 | { |
||||
814 | return locale_parse($prefix)['region']; |
||||
815 | } |
||||
816 | } |
||||
817 |