Completed
Push — stable8.2 ( ed3d97...c12577 )
by
unknown
11:08
created

OC_L10N   D

Complexity

Total Complexity 82

Size/Duplication

Total Lines 504
Duplicated Lines 6.75 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 82.16%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 82
c 1
b 0
f 0
lcom 1
cbo 8
dl 34
loc 504
ccs 175
cts 213
cp 0.8216
rs 4.8717

19 Methods

Rating   Name   Duplication   Size   Complexity  
C init() 0 42 12
C findAvailableLanguages() 18 36 12
A get() 0 3 1
A __construct() 0 4 1
D setLanguageFromRequest() 16 34 9
A load() 0 19 3
B createPluralFormFunction() 0 47 6
A t() 0 3 1
A n() 0 13 3
A getTranslations() 0 4 1
A getPluralFormFunction() 0 7 2
D l() 0 39 10
A getLanguageCode() 0 3 2
D findLanguage() 0 29 9
A findI18nDir() 0 14 3
A languageExists() 0 10 3
A getDateFormat() 0 5 1
A getFirstWeekDay() 0 5 1
A transformToCLDRLocale() 0 7 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like OC_L10N often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OC_L10N, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Andreas Ergenzinger <[email protected]>
4
 * @author Andreas Fischer <[email protected]>
5
 * @author Bart Visscher <[email protected]>
6
 * @author Bernhard Posselt <[email protected]>
7
 * @author Felix Moeller <[email protected]>
8
 * @author Jakob Sack <[email protected]>
9
 * @author Jan-Christoph Borchardt <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author Jörn Friedrich Dreyer <[email protected]>
12
 * @author Lennart Rosam <[email protected]>
13
 * @author Lukas Reschke <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Robin McCorkell <[email protected]>
17
 * @author Scrutinizer Auto-Fixer <[email protected]>
18
 * @author Thomas Müller <[email protected]>
19
 * @author Thomas Tanghus <[email protected]>
20
 * @author Vincent Petry <[email protected]>
21
 *
22
 * @copyright Copyright (c) 2015, ownCloud, Inc.
23
 * @license AGPL-3.0
24
 *
25
 * This code is free software: you can redistribute it and/or modify
26
 * it under the terms of the GNU Affero General Public License, version 3,
27
 * as published by the Free Software Foundation.
28
 *
29
 * This program is distributed in the hope that it will be useful,
30
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
 * GNU Affero General Public License for more details.
33
 *
34
 * You should have received a copy of the GNU Affero General Public License, version 3,
35
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
36
 *
37
 */
38
39
/**
40
 * This class is for i18n and l10n
41
 */
42
class OC_L10N implements \OCP\IL10N {
43
	/**
44
	 * cache
45
	 */
46
	protected static $cache = array();
47
	protected static $availableLanguages = array();
48
49
	/**
50
	 * The best language
51
	 */
52
	protected static $language = '';
53
54
	/**
55
	 * App of this object
56
	 */
57
	protected $app;
58
59
	/**
60
	 * Language of this object
61
	 */
62
	protected $lang;
63
64
	/**
65
	 * Translations
66
	 */
67
	private $translations = array();
68
69
	/**
70
	 * Plural forms (string)
71
	 */
72
	private $pluralFormString = 'nplurals=2; plural=(n != 1);';
73
74
	/**
75
	 * Plural forms (function)
76
	 */
77
	private $pluralFormFunction = null;
78
79
	/**
80
	 * get an L10N instance
81
	 * @param string $app
82
	 * @param string|null $lang
83
	 * @return \OCP\IL10N
84
	 * @deprecated Use \OC::$server->getL10NFactory()->get() instead
85
	 */
86 1
	public static function get($app, $lang=null) {
87 1
		return \OC::$server->getL10NFactory()->get($app, $lang);
88
	}
89
90
	/**
91
	 * The constructor
92
	 * @param string $app app requesting l10n
93
	 * @param string $lang default: null Language
94
	 *
95
	 * If language is not set, the constructor tries to find the right
96
	 * language.
97
	 */
98 121
	public function __construct($app, $lang = null) {
99 121
		$this->app = $app;
100 121
		$this->lang = $lang;
101 121
	}
102
103
	/**
104
	 * @return string
105
	 */
106 6
	public static function setLanguageFromRequest() {
107 6
		if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
108 5
			$available = self::findAvailableLanguages();
109
110
			// E.g. make sure that 'de' is before 'de_DE'.
111 5
			sort($available);
112
113 5
			$preferences = preg_split('/,\s*/', strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']));
114 5
			foreach ($preferences as $preference) {
115 5
				list($preferred_language) = explode(';', $preference);
116 5
				$preferred_language = str_replace('-', '_', $preferred_language);
117 5 View Code Duplication
				foreach ($available as $available_language) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
118 5
					if ($preferred_language === strtolower($available_language)) {
119 2
						if (!self::$language) {
120 2
							self::$language = $available_language;
121 2
						}
122 2
						return $available_language;
123
					}
124 5
				}
125 3 View Code Duplication
				foreach ($available as $available_language) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
126 3
					if (substr($preferred_language, 0, 2) === $available_language) {
127 2
						if (!self::$language) {
128 2
							self::$language = $available_language;
129 2
						}
130 2
						return $available_language;
131
					}
132 3
				}
133 1
			}
134 1
		}
135
136 2
		self::$language = 'en';
137
		// Last try: English
138 2
		return 'en';
139
	}
140
141
	/**
142
	 * @param $transFile
143
	 * @param bool $mergeTranslations
144
	 * @return bool
145
	 */
146 3
	public function load($transFile, $mergeTranslations = false) {
147 3
		$this->app = true;
148
149 3
		$json = json_decode(file_get_contents($transFile), true);
150 3
		if (!is_array($json)) {
151
			return false;
152
		}
153
154 3
		$this->pluralFormString = $json['pluralForm'];
155 3
		$translations = $json['translations'];
156
157 3
		if ($mergeTranslations) {
158
			$this->translations = array_merge($this->translations, $translations);
159
		} else {
160 3
			$this->translations = $translations;
161
		}
162
163 3
		return true;
164
	}
165
166 170
	protected function init() {
167 170
		if ($this->app === true) {
168 112
			return;
169
		}
170 82
		$app = OC_App::cleanAppId($this->app);
171 82
		$lang = str_replace(array('\0', '/', '\\', '..'), '', $this->lang);
172 82
		$this->app = true;
173
		// Find the right language
174 82
		if(is_null($lang) || $lang == '') {
175 3
			$lang = self::findLanguage($app);
176 3
		}
177
178
		// Use cache if possible
179 82
		if(array_key_exists($app.'::'.$lang, self::$cache)) {
180 74
			$this->translations = self::$cache[$app.'::'.$lang]['t'];
181 74
		} else{
182 8
			$i18nDir = self::findI18nDir($app);
183 8
			$transFile = strip_tags($i18nDir).strip_tags($lang).'.json';
184
			// Texts are in $i18ndir
185
			// (Just no need to define date/time format etc. twice)
186 8
			if((OC_Helper::isSubDirectory($transFile, OC::$SERVERROOT.'/core/l10n/')
187 8
				|| OC_Helper::isSubDirectory($transFile, OC::$SERVERROOT.'/lib/l10n/')
188 8
				|| OC_Helper::isSubDirectory($transFile, OC::$SERVERROOT.'/settings')
189 8
				|| OC_Helper::isSubDirectory($transFile, OC_App::getAppPath($app).'/l10n/')
190 8
				)
191 8
				&& file_exists($transFile)) {
192
				// load the translations file
193
				$this->load($transFile);
194
			}
195
196
			//merge with translations from theme
197
			$theme = \OC::$server->getConfig()->getSystemValue('theme');
198
			if (!empty($theme)) {
199
				$transFile = OC::$SERVERROOT.'/themes/'.$theme.substr($transFile, strlen(OC::$SERVERROOT));
200
				if (file_exists($transFile)) {
201
					$this->load($transFile, true);
202
				}
203
			}
204
205 8
			self::$cache[$app.'::'.$lang]['t'] = $this->translations;
206
		}
207 82
	}
208
209
	/**
210
	 * Creates a function that The constructor
211
	 *
212
	 * If language is not set, the constructor tries to find the right
213
	 * language.
214
	 *
215
	 * Parts of the code is copied from Habari:
216
	 * https://github.com/habari/system/blob/master/classes/locale.php
217
	 * @param string $string
218
	 * @return string
219
	 */
220 5
	protected function createPluralFormFunction($string){
221 5
		if(preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
222
			// sanitize
223 5
			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
224 5
			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
225
226 5
			$body = str_replace(
227 5
				array( 'plural', 'n', '$n$plurals', ),
228 5
				array( '$plural', '$n', '$nplurals', ),
229 5
				'nplurals='. $nplurals . '; plural=' . $plural
230 5
			);
231
232
			// add parents
233
			// important since PHP's ternary evaluates from left to right
234 5
			$body .= ';';
235 5
			$res = '';
236 5
			$p = 0;
237 5
			for($i = 0; $i < strlen($body); $i++) {
238 5
				$ch = $body[$i];
239
				switch ( $ch ) {
240 5
				case '?':
241 2
					$res .= ' ? (';
242 2
					$p++;
243 2
					break;
244 5
				case ':':
245 2
					$res .= ') : (';
246 2
					break;
247 5
				case ';':
248 5
					$res .= str_repeat( ')', $p ) . ';';
249 5
					$p = 0;
250 5
					break;
251 5
				default:
252 5
					$res .= $ch;
253 5
				}
254 5
			}
255
256 5
			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
257 5
			return create_function('$n', $body);
258
		}
259
		else {
260
			// default: one plural form for all cases but n==1 (english)
261
			return create_function(
262
				'$n',
263
				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
264
			);
265
		}
266
	}
267
268
	/**
269
	 * Translating
270
	 * @param string $text The text we need a translation for
271
	 * @param array $parameters default:array() Parameters for sprintf
272
	 * @return \OC_L10N_String Translation or the same text
273
	 *
274
	 * Returns the translation. If no translation is found, $text will be
275
	 * returned.
276
	 */
277 143
	public function t($text, $parameters = array()) {
278 143
		return new OC_L10N_String($this, $text, $parameters);
279
	}
280
281
	/**
282
	 * Translating
283
	 * @param string $text_singular the string to translate for exactly one object
284
	 * @param string $text_plural the string to translate for n objects
285
	 * @param integer $count Number of objects
286
	 * @param array $parameters default:array() Parameters for sprintf
287
	 * @return \OC_L10N_String Translation or the same text
288
	 *
289
	 * Returns the translation. If no translation is found, $text will be
290
	 * returned. %n will be replaced with the number of objects.
291
	 *
292
	 * The correct plural is determined by the plural_forms-function
293
	 * provided by the po file.
294
	 *
295
	 */
296 40
	public function n($text_singular, $text_plural, $count, $parameters = array()) {
297 40
		$this->init();
298 40
		$identifier = "_${text_singular}_::_${text_plural}_";
299 40
		if( array_key_exists($identifier, $this->translations)) {
300 11
			return new OC_L10N_String( $this, $identifier, $parameters, $count );
301
		}else{
302 29
			if($count === 1) {
303 4
				return new OC_L10N_String($this, $text_singular, $parameters, $count);
304
			}else{
305 25
				return new OC_L10N_String($this, $text_plural, $parameters, $count);
306
			}
307
		}
308
	}
309
310
	/**
311
	 * getTranslations
312
	 * @return array Fetch all translations
313
	 *
314
	 * Returns an associative array with all translations
315
	 */
316
	public function getTranslations() {
317 131
		$this->init();
318 131
		return $this->translations;
319
	}
320
321
	/**
322
	 * getPluralFormFunction
323
	 * @return string the plural form function
324
	 *
325
	 * returned function accepts the argument $n
326
	 */
327
	public function getPluralFormFunction() {
328 11
		$this->init();
329 11
		if(is_null($this->pluralFormFunction)) {
330 5
			$this->pluralFormFunction = $this->createPluralFormFunction($this->pluralFormString);
331 5
		}
332 11
		return $this->pluralFormFunction;
333
	}
334
335
	/**
336
	 * Localization
337
	 * @param string $type Type of localization
338
	 * @param array|int|string $data parameters for this localization
339
	 * @param array $options
340
	 * @return string|false
341
	 *
342
	 * Returns the localized data.
343
	 *
344
	 * Implemented types:
345
	 *  - date
346
	 *    - Creates a date
347
	 *    - params: timestamp (int/string)
348
	 *  - datetime
349
	 *    - Creates date and time
350
	 *    - params: timestamp (int/string)
351
	 *  - time
352
	 *    - Creates a time
353
	 *    - params: timestamp (int/string)
354
	 */
355
	public function l($type, $data, $options = array()) {
356 41
		if ($type === 'firstday') {
357 2
			return $this->getFirstWeekDay();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getFirstWeekDay(); (integer) is incompatible with the return type declared by the interface OCP\IL10N::l of type string|false.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
358
		}
359 39
		if ($type === 'jsdate') {
360
			return $this->getDateFormat();
361
		}
362
363 39
		$this->init();
364 39
		$value = new DateTime();
365 39
		if($data instanceof DateTime) {
366 27
			$value = $data;
367 39
		} elseif(is_string($data) && !is_numeric($data)) {
368
			$data = strtotime($data);
369
			$value->setTimestamp($data);
370
		} else {
371 12
			$value->setTimestamp($data);
372
		}
373
374
		// Use the language of the instance, before falling back to the current user's language
375 39
		$locale = $this->lang;
376 39
		if ($locale === null) {
377 6
			$locale = self::findLanguage();
378 6
		}
379 39
		$locale = $this->transformToCLDRLocale($locale);
380
381 39
		$options = array_merge(array('width' => 'long'), $options);
382 39
		$width = $options['width'];
383
		switch($type) {
384 39
			case 'date':
385 10
				return Punic\Calendar::formatDate($value, $width, $locale);
386 30
			case 'datetime':
387 22
				return Punic\Calendar::formatDatetime($value, $width, $locale);
388 8
			case 'time':
389 8
				return Punic\Calendar::formatTime($value, $width, $locale);
390
			default:
391
				return false;
392
		}
393
	}
394
395
	/**
396
	 * The code (en, de, ...) of the language that is used for this OC_L10N object
397
	 *
398
	 * @return string language
399
	 */
400
	public function getLanguageCode() {
401 5
		return $this->lang ? $this->lang : self::findLanguage();
402
	}
403
404
	/**
405
	 * find the best language
406
	 * @param string $app
407
	 * @return string language
408
	 *
409
	 * If nothing works it returns 'en'
410
	 */
411
	public static function findLanguage($app = null) {
412 18
		if (self::$language != '' && self::languageExists($app, self::$language)) {
413 10
			return self::$language;
414
		}
415
416 8
		$config = \OC::$server->getConfig();
417 8
		$userId = \OC_User::getUser();
418
419 8
		if($userId && $config->getUserValue($userId, 'core', 'lang')) {
420
			$lang = $config->getUserValue($userId, 'core', 'lang');
421
			self::$language = $lang;
422
			if(self::languageExists($app, $lang)) {
423
				return $lang;
424
			}
425
		}
426
427 8
		$default_language = $config->getSystemValue('default_language', false);
428
429 8
		if($default_language !== false) {
430 2
			return $default_language;
431
		}
432
433 6
		$lang = self::setLanguageFromRequest();
434 6
		if($userId && !$config->getUserValue($userId, 'core', 'lang')) {
435
			$config->setUserValue($userId, 'core', 'lang', $lang);
436
		}
437
438 6
		return $lang;
439
	}
440
441
	/**
442
	 * find the l10n directory
443
	 * @param string $app App that needs to be translated
444
	 * @return string directory
445
	 */
446
	protected static function findI18nDir($app) {
447
		// find the i18n dir
448 9
		$i18nDir = OC::$SERVERROOT.'/core/l10n/';
449 9
		if($app != '') {
450
			// Check if the app is in the app folder
451 8
			if(file_exists(OC_App::getAppPath($app).'/l10n/')) {
452 2
				$i18nDir = OC_App::getAppPath($app).'/l10n/';
453 2
			}
454
			else{
455 6
				$i18nDir = OC::$SERVERROOT.'/'.$app.'/l10n/';
456
			}
457 8
		}
458 9
		return $i18nDir;
459
	}
460
461
	/**
462
	 * find all available languages for an app
463
	 * @param string $app App that needs to be translated
464
	 * @return array an array of available languages
465
	 */
466
	public static function findAvailableLanguages($app=null) {
467
		// also works with null as key
468 5
		if(isset(self::$availableLanguages[$app]) && !empty(self::$availableLanguages[$app])) {
469 4
			return self::$availableLanguages[$app];
470
		}
471 1
		$available=array('en');//english is always available
472 1
		$dir = self::findI18nDir($app);
473 1 View Code Duplication
		if(is_dir($dir)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
474 1
			$files=scandir($dir);
475 1
			foreach($files as $file) {
476 1
				if(substr($file, -5, 5) === '.json' && substr($file, 0, 4) !== 'l10n') {
477 1
					$i = substr($file, 0, -5);
478 1
					$available[] = $i;
479 1
				}
480 1
			}
481 1
		}
482
483 1
		$config = \OC::$server->getConfig();
484 1
		// merge with translations from theme
485
		$theme = $config->getSystemValue('theme');
486
		if(!empty($theme)) {
487
			$themeDir = \OC::$SERVERROOT . '/themes/' . $theme . substr($dir, strlen(\OC::$SERVERROOT));
488 View Code Duplication
			if(is_dir($themeDir)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
489
				$files=scandir($dir);
490
				foreach($files as $file) {
491
					if(substr($file, -5, 5) === '.json' && substr($file, 0, 4) !== 'l10n') {
492
						$i = substr($file, 0, -5);
493 10
						$available[] = $i;
494 10
					}
495
				}
496
			}
497
		}
498
499
		self::$availableLanguages[$app] = $available;
500
		return $available;
501
	}
502
503
	/**
504
	 * @param string $app
505
	 * @param string $lang
506
	 * @return bool
507
	 */
508
	public static function languageExists($app, $lang) {
509
		if ($lang === 'en') {//english is always available
510
			return true;
511
		}
512
		$dir = self::findI18nDir($app);
513
		if(is_dir($dir)) {
514
			return file_exists($dir.'/'.$lang.'.json');
515
		}
516
		return false;
517 2
	}
518 2
519 2
	/**
520
	 * @return string
521
	 * @throws \Punic\Exception\ValueNotInList
522
	 */
523 41
	public function getDateFormat() {
524
		$locale = $this->getLanguageCode();
525
		$locale = $this->transformToCLDRLocale($locale);
526
		return Punic\Calendar::getDateFormat('short', $locale);
527 41
	}
528
529
	/**
530
	 * @return int
531
	 */
532
	public function getFirstWeekDay() {
533
		$locale = $this->getLanguageCode();
534
		$locale = $this->transformToCLDRLocale($locale);
535
		return Punic\Calendar::getFirstWeekday($locale);
536
	}
537
538
	private function transformToCLDRLocale($locale) {
539
		if ($locale === 'sr@latin') {
540
			return 'sr_latn';
541
		}
542
543
		return $locale;
544
	}
545
}
546