You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

l10n.php 15KB

13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
11 years ago
13 years ago
13 years ago
11 years ago
11 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. <?php
  2. /**
  3. * @author Andreas Ergenzinger <andreas.ergenzinger@gmx.de>
  4. * @author Andreas Fischer <bantu@owncloud.com>
  5. * @author Bart Visscher <bartv@thisnet.nl>
  6. * @author Bernhard Posselt <dev@bernhard-posselt.com>
  7. * @author Christopher Schäpers <kondou@ts.unde.re>
  8. * @author Felix Moeller <mail@felixmoeller.de>
  9. * @author Jakob Sack <mail@jakobsack.de>
  10. * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
  11. * @author Joas Schilling <nickvergessen@owncloud.com>
  12. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  13. * @author Lennart Rosam <lennart.rosam@medien-systempartner.de>
  14. * @author Lukas Reschke <lukas@owncloud.com>
  15. * @author Morris Jobke <hey@morrisjobke.de>
  16. * @author Robin Appelman <icewind@owncloud.com>
  17. * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
  18. * @author Scrutinizer Auto-Fixer <auto-fixer@scrutinizer-ci.com>
  19. * @author Thomas Müller <thomas.mueller@tmit.eu>
  20. * @author Thomas Tanghus <thomas@tanghus.net>
  21. * @author Vincent Petry <pvince81@owncloud.com>
  22. *
  23. * @copyright Copyright (c) 2015, ownCloud, Inc.
  24. * @license AGPL-3.0
  25. *
  26. * This code is free software: you can redistribute it and/or modify
  27. * it under the terms of the GNU Affero General Public License, version 3,
  28. * as published by the Free Software Foundation.
  29. *
  30. * This program is distributed in the hope that it will be useful,
  31. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  32. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  33. * GNU Affero General Public License for more details.
  34. *
  35. * You should have received a copy of the GNU Affero General Public License, version 3,
  36. * along with this program. If not, see <http://www.gnu.org/licenses/>
  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. * The best language
  50. */
  51. protected static $language = '';
  52. /**
  53. * App of this object
  54. */
  55. protected $app;
  56. /**
  57. * Language of this object
  58. */
  59. protected $lang;
  60. /**
  61. * Translations
  62. */
  63. private $translations = array();
  64. /**
  65. * Plural forms (string)
  66. */
  67. private $pluralFormString = 'nplurals=2; plural=(n != 1);';
  68. /**
  69. * Plural forms (function)
  70. */
  71. private $pluralFormFunction = null;
  72. /**
  73. * get an L10N instance
  74. * @param string $app
  75. * @param string|null $lang
  76. * @return \OCP\IL10N
  77. * @deprecated Use \OC::$server->getL10NFactory()->get() instead
  78. */
  79. public static function get($app, $lang=null) {
  80. return \OC::$server->getL10NFactory()->get($app, $lang);
  81. }
  82. /**
  83. * The constructor
  84. * @param string $app app requesting l10n
  85. * @param string $lang default: null Language
  86. *
  87. * If language is not set, the constructor tries to find the right
  88. * language.
  89. */
  90. public function __construct($app, $lang = null) {
  91. $this->app = $app;
  92. $this->lang = $lang;
  93. }
  94. /**
  95. * @param $app
  96. * @return string
  97. */
  98. public static function setLanguageFromRequest($app = null) {
  99. if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
  100. if (is_array($app)) {
  101. $available = $app;
  102. } else {
  103. $available = self::findAvailableLanguages($app);
  104. }
  105. // E.g. make sure that 'de' is before 'de_DE'.
  106. sort($available);
  107. $preferences = preg_split('/,\s*/', strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']));
  108. foreach ($preferences as $preference) {
  109. list($preferred_language) = explode(';', $preference);
  110. $preferred_language = str_replace('-', '_', $preferred_language);
  111. foreach ($available as $available_language) {
  112. if ($preferred_language === strtolower($available_language)) {
  113. if (!is_array($app)) {
  114. self::$language = $available_language;
  115. }
  116. return $available_language;
  117. }
  118. }
  119. foreach ($available as $available_language) {
  120. if (substr($preferred_language, 0, 2) === $available_language) {
  121. if (!is_array($app)) {
  122. self::$language = $available_language;
  123. }
  124. return $available_language;
  125. }
  126. }
  127. }
  128. }
  129. self::$language = 'en';
  130. // Last try: English
  131. return 'en';
  132. }
  133. /**
  134. * @param $transFile
  135. * @param bool $mergeTranslations
  136. * @return bool
  137. */
  138. public function load($transFile, $mergeTranslations = false) {
  139. $this->app = true;
  140. $json = json_decode(file_get_contents($transFile), true);
  141. if (!is_array($json)) {
  142. return false;
  143. }
  144. $this->pluralFormString = $json['pluralForm'];
  145. $translations = $json['translations'];
  146. if ($mergeTranslations) {
  147. $this->translations = array_merge($this->translations, $translations);
  148. } else {
  149. $this->translations = $translations;
  150. }
  151. return true;
  152. }
  153. protected function init() {
  154. if ($this->app === true) {
  155. return;
  156. }
  157. $app = OC_App::cleanAppId($this->app);
  158. $lang = str_replace(array('\0', '/', '\\', '..'), '', $this->lang);
  159. $this->app = true;
  160. // Find the right language
  161. if(is_null($lang) || $lang == '') {
  162. $lang = self::findLanguage($app);
  163. }
  164. // Use cache if possible
  165. if(array_key_exists($app.'::'.$lang, self::$cache)) {
  166. $this->translations = self::$cache[$app.'::'.$lang]['t'];
  167. } else{
  168. $i18nDir = self::findI18nDir($app);
  169. $transFile = strip_tags($i18nDir).strip_tags($lang).'.json';
  170. // Texts are in $i18ndir
  171. // (Just no need to define date/time format etc. twice)
  172. if((OC_Helper::isSubDirectory($transFile, OC::$SERVERROOT.'/core/l10n/')
  173. || OC_Helper::isSubDirectory($transFile, OC::$SERVERROOT.'/lib/l10n/')
  174. || OC_Helper::isSubDirectory($transFile, OC::$SERVERROOT.'/settings')
  175. || OC_Helper::isSubDirectory($transFile, OC_App::getAppPath($app).'/l10n/')
  176. )
  177. && file_exists($transFile)) {
  178. // load the translations file
  179. if($this->load($transFile)) {
  180. //merge with translations from theme
  181. $theme = \OC::$server->getConfig()->getSystemValue('theme');
  182. if (!empty($theme)) {
  183. $transFile = OC::$SERVERROOT.'/themes/'.$theme.substr($transFile, strlen(OC::$SERVERROOT));
  184. if (file_exists($transFile)) {
  185. $this->load($transFile, true);
  186. }
  187. }
  188. }
  189. }
  190. self::$cache[$app.'::'.$lang]['t'] = $this->translations;
  191. }
  192. }
  193. /**
  194. * Creates a function that The constructor
  195. *
  196. * If language is not set, the constructor tries to find the right
  197. * language.
  198. *
  199. * Parts of the code is copied from Habari:
  200. * https://github.com/habari/system/blob/master/classes/locale.php
  201. * @param string $string
  202. * @return string
  203. */
  204. protected function createPluralFormFunction($string){
  205. if(preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
  206. // sanitize
  207. $nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
  208. $plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
  209. $body = str_replace(
  210. array( 'plural', 'n', '$n$plurals', ),
  211. array( '$plural', '$n', '$nplurals', ),
  212. 'nplurals='. $nplurals . '; plural=' . $plural
  213. );
  214. // add parents
  215. // important since PHP's ternary evaluates from left to right
  216. $body .= ';';
  217. $res = '';
  218. $p = 0;
  219. for($i = 0; $i < strlen($body); $i++) {
  220. $ch = $body[$i];
  221. switch ( $ch ) {
  222. case '?':
  223. $res .= ' ? (';
  224. $p++;
  225. break;
  226. case ':':
  227. $res .= ') : (';
  228. break;
  229. case ';':
  230. $res .= str_repeat( ')', $p ) . ';';
  231. $p = 0;
  232. break;
  233. default:
  234. $res .= $ch;
  235. }
  236. }
  237. $body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
  238. return create_function('$n', $body);
  239. }
  240. else {
  241. // default: one plural form for all cases but n==1 (english)
  242. return create_function(
  243. '$n',
  244. '$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
  245. );
  246. }
  247. }
  248. /**
  249. * Translating
  250. * @param string $text The text we need a translation for
  251. * @param array $parameters default:array() Parameters for sprintf
  252. * @return \OC_L10N_String Translation or the same text
  253. *
  254. * Returns the translation. If no translation is found, $text will be
  255. * returned.
  256. */
  257. public function t($text, $parameters = array()) {
  258. return new OC_L10N_String($this, $text, $parameters);
  259. }
  260. /**
  261. * Translating
  262. * @param string $text_singular the string to translate for exactly one object
  263. * @param string $text_plural the string to translate for n objects
  264. * @param integer $count Number of objects
  265. * @param array $parameters default:array() Parameters for sprintf
  266. * @return \OC_L10N_String Translation or the same text
  267. *
  268. * Returns the translation. If no translation is found, $text will be
  269. * returned. %n will be replaced with the number of objects.
  270. *
  271. * The correct plural is determined by the plural_forms-function
  272. * provided by the po file.
  273. *
  274. */
  275. public function n($text_singular, $text_plural, $count, $parameters = array()) {
  276. $this->init();
  277. $identifier = "_${text_singular}_::_${text_plural}_";
  278. if( array_key_exists($identifier, $this->translations)) {
  279. return new OC_L10N_String( $this, $identifier, $parameters, $count );
  280. }else{
  281. if($count === 1) {
  282. return new OC_L10N_String($this, $text_singular, $parameters, $count);
  283. }else{
  284. return new OC_L10N_String($this, $text_plural, $parameters, $count);
  285. }
  286. }
  287. }
  288. /**
  289. * getTranslations
  290. * @return array Fetch all translations
  291. *
  292. * Returns an associative array with all translations
  293. */
  294. public function getTranslations() {
  295. $this->init();
  296. return $this->translations;
  297. }
  298. /**
  299. * getPluralFormFunction
  300. * @return string the plural form function
  301. *
  302. * returned function accepts the argument $n
  303. */
  304. public function getPluralFormFunction() {
  305. $this->init();
  306. if(is_null($this->pluralFormFunction)) {
  307. $this->pluralFormFunction = $this->createPluralFormFunction($this->pluralFormString);
  308. }
  309. return $this->pluralFormFunction;
  310. }
  311. /**
  312. * Localization
  313. * @param string $type Type of localization
  314. * @param array|int|string $data parameters for this localization
  315. * @param array $options
  316. * @return string|false
  317. *
  318. * Returns the localized data.
  319. *
  320. * Implemented types:
  321. * - date
  322. * - Creates a date
  323. * - params: timestamp (int/string)
  324. * - datetime
  325. * - Creates date and time
  326. * - params: timestamp (int/string)
  327. * - time
  328. * - Creates a time
  329. * - params: timestamp (int/string)
  330. */
  331. public function l($type, $data, $options = array()) {
  332. if ($type === 'firstday') {
  333. return $this->getFirstWeekDay();
  334. }
  335. if ($type === 'jsdate') {
  336. return $this->getDateFormat();
  337. }
  338. $this->init();
  339. $value = new DateTime();
  340. if($data instanceof DateTime) {
  341. $value = $data;
  342. } elseif(is_string($data) && !is_numeric($data)) {
  343. $data = strtotime($data);
  344. $value->setTimestamp($data);
  345. } else {
  346. $value->setTimestamp($data);
  347. }
  348. // Use the language of the instance, before falling back to the current user's language
  349. $locale = $this->lang;
  350. if ($locale === null) {
  351. $locale = self::findLanguage();
  352. }
  353. $locale = $this->transformToCLDRLocale($locale);
  354. $options = array_merge(array('width' => 'long'), $options);
  355. $width = $options['width'];
  356. switch($type) {
  357. case 'date':
  358. return Punic\Calendar::formatDate($value, $width, $locale);
  359. case 'datetime':
  360. return Punic\Calendar::formatDatetime($value, $width, $locale);
  361. case 'time':
  362. return Punic\Calendar::formatTime($value, $width, $locale);
  363. default:
  364. return false;
  365. }
  366. }
  367. /**
  368. * Choose a language
  369. * @param array $text Associative Array with possible strings
  370. * @return String
  371. *
  372. * $text is an array 'de' => 'hallo welt', 'en' => 'hello world', ...
  373. *
  374. * This function is useful to avoid loading thousands of files if only one
  375. * simple string is needed, for example in appinfo.php
  376. */
  377. public static function selectLanguage($text) {
  378. $lang = self::findLanguage(array_keys($text));
  379. return $text[$lang];
  380. }
  381. /**
  382. * The given language is forced to be used while executing the current request
  383. * @param string $lang
  384. */
  385. public static function forceLanguage($lang) {
  386. self::$language = $lang;
  387. }
  388. /**
  389. * The code (en, de, ...) of the language that is used for this OC_L10N object
  390. *
  391. * @return string language
  392. */
  393. public function getLanguageCode() {
  394. return $this->lang ? $this->lang : self::findLanguage();
  395. }
  396. /**
  397. * find the best language
  398. * @param array|string $app details below
  399. * @return string language
  400. *
  401. * If $app is an array, ownCloud assumes that these are the available
  402. * languages. Otherwise ownCloud tries to find the files in the l10n
  403. * folder.
  404. *
  405. * If nothing works it returns 'en'
  406. */
  407. public static function findLanguage($app = null) {
  408. if(!is_array($app) && self::$language != '') {
  409. return self::$language;
  410. }
  411. $config = \OC::$server->getConfig();
  412. $userId = \OC_User::getUser();
  413. if($userId && $config->getUserValue($userId, 'core', 'lang')) {
  414. $lang = $config->getUserValue($userId, 'core', 'lang');
  415. self::$language = $lang;
  416. if(is_array($app)) {
  417. $available = $app;
  418. $lang_exists = array_search($lang, $available) !== false;
  419. } else {
  420. $lang_exists = self::languageExists($app, $lang);
  421. }
  422. if($lang_exists) {
  423. return $lang;
  424. }
  425. }
  426. $default_language = $config->getSystemValue('default_language', false);
  427. if($default_language !== false) {
  428. return $default_language;
  429. }
  430. $lang = self::setLanguageFromRequest($app);
  431. if($userId && !$config->getUserValue($userId, 'core', 'lang')) {
  432. $config->setUserValue($userId, 'core', 'lang', $lang);
  433. }
  434. return $lang;
  435. }
  436. /**
  437. * find the l10n directory
  438. * @param string $app App that needs to be translated
  439. * @return string directory
  440. */
  441. protected static function findI18nDir($app) {
  442. // find the i18n dir
  443. $i18nDir = OC::$SERVERROOT.'/core/l10n/';
  444. if($app != '') {
  445. // Check if the app is in the app folder
  446. if(file_exists(OC_App::getAppPath($app).'/l10n/')) {
  447. $i18nDir = OC_App::getAppPath($app).'/l10n/';
  448. }
  449. else{
  450. $i18nDir = OC::$SERVERROOT.'/'.$app.'/l10n/';
  451. }
  452. }
  453. return $i18nDir;
  454. }
  455. /**
  456. * find all available languages for an app
  457. * @param string $app App that needs to be translated
  458. * @return array an array of available languages
  459. */
  460. public static function findAvailableLanguages($app=null) {
  461. if(!empty(self::$availableLanguages)) {
  462. return self::$availableLanguages;
  463. }
  464. $available=array('en');//english is always available
  465. $dir = self::findI18nDir($app);
  466. if(is_dir($dir)) {
  467. $files=scandir($dir);
  468. foreach($files as $file) {
  469. if(substr($file, -5, 5) === '.json' && substr($file, 0, 4) !== 'l10n') {
  470. $i = substr($file, 0, -5);
  471. $available[] = $i;
  472. }
  473. }
  474. }
  475. self::$availableLanguages = $available;
  476. return $available;
  477. }
  478. /**
  479. * @param string $app
  480. * @param string $lang
  481. * @return bool
  482. */
  483. public static function languageExists($app, $lang) {
  484. if ($lang === 'en') {//english is always available
  485. return true;
  486. }
  487. $dir = self::findI18nDir($app);
  488. if(is_dir($dir)) {
  489. return file_exists($dir.'/'.$lang.'.json');
  490. }
  491. return false;
  492. }
  493. /**
  494. * @return string
  495. * @throws \Punic\Exception\ValueNotInList
  496. */
  497. public function getDateFormat() {
  498. $locale = $this->getLanguageCode();
  499. $locale = $this->transformToCLDRLocale($locale);
  500. return Punic\Calendar::getDateFormat('short', $locale);
  501. }
  502. /**
  503. * @return int
  504. */
  505. public function getFirstWeekDay() {
  506. $locale = $this->getLanguageCode();
  507. $locale = $this->transformToCLDRLocale($locale);
  508. return Punic\Calendar::getFirstWeekday($locale);
  509. }
  510. private function transformToCLDRLocale($locale) {
  511. if ($locale === 'sr@latin') {
  512. return 'sr_latn';
  513. }
  514. return $locale;
  515. }
  516. }