Languages.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <?php
  2. namespace Shaarli;
  3. use Gettext\GettextTranslator;
  4. use Gettext\Merge;
  5. use Gettext\Translations;
  6. use Gettext\Translator;
  7. use Gettext\TranslatorInterface;
  8. use Shaarli\Config\ConfigManager;
  9. /**
  10. * Class Languages
  11. *
  12. * Load Shaarli translations using 'gettext/gettext'.
  13. * This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
  14. * with a fixed language, or dynamically using autoLocale().
  15. *
  16. * Translation files PO/MO files follow gettext standard and must be placed under:
  17. * <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
  18. *
  19. * Pros/cons:
  20. * - gettext extension is faster
  21. * - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
  22. *
  23. * Settings:
  24. * - translation.mode:
  25. * - auto: use default setting (PHP implementation)
  26. * - php: use PHP implementation
  27. * - gettext: use gettext wrapper
  28. * - translation.language:
  29. * - auto: use autoLocale() and the language change according to user HTTP headers
  30. * - fixed language: e.g. 'fr'
  31. * - translation.extensions:
  32. * - domain => translation_path: allow plugins and themes to extend the defaut extension
  33. * The domain must be unique, and translation path must be relative, and contains the tree mentioned above.
  34. *
  35. * @package Shaarli
  36. */
  37. class Languages
  38. {
  39. /**
  40. * Core translations domain
  41. */
  42. const DEFAULT_DOMAIN = 'shaarli';
  43. /**
  44. * @var TranslatorInterface
  45. */
  46. protected $translator;
  47. /**
  48. * @var string
  49. */
  50. protected $language;
  51. /**
  52. * @var ConfigManager
  53. */
  54. protected $conf;
  55. /**
  56. * Languages constructor.
  57. *
  58. * @param string $language lang determined by autoLocale(), can be overridden.
  59. * @param ConfigManager $conf instance.
  60. */
  61. public function __construct($language, $conf)
  62. {
  63. $this->conf = $conf;
  64. $confLanguage = $this->conf->get('translation.language', 'auto');
  65. // Auto mode or invalid parameter, use the detected language.
  66. // If the detected language is invalid, it doesn't matter, it will use English.
  67. if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) {
  68. $this->language = substr($language, 0, 5);
  69. } else {
  70. $this->language = $confLanguage;
  71. }
  72. if (! extension_loaded('gettext')
  73. || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
  74. ) {
  75. $this->initPhpTranslator();
  76. } else {
  77. $this->initGettextTranslator();
  78. }
  79. // Register default functions (e.g. '__()') to use our Translator
  80. $this->translator->register();
  81. }
  82. /**
  83. * Initialize the translator using php gettext extension (gettext dependency act as a wrapper).
  84. */
  85. protected function initGettextTranslator()
  86. {
  87. $this->translator = new GettextTranslator();
  88. $this->translator->setLanguage($this->language);
  89. $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
  90. // Default extension translation from the current theme
  91. $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $this->conf->get('theme') .'/language';
  92. if (is_dir($themeTransFolder)) {
  93. $this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false);
  94. }
  95. foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
  96. if ($domain !== self::DEFAULT_DOMAIN) {
  97. $this->translator->loadDomain($domain, $translationPath, false);
  98. }
  99. }
  100. }
  101. /**
  102. * Initialize the translator using a PHP implementation of gettext.
  103. *
  104. * Note that if language po file doesn't exist, errors are ignored (e.g. not installed language).
  105. */
  106. protected function initPhpTranslator()
  107. {
  108. $this->translator = new Translator();
  109. $translations = new Translations();
  110. // Core translations
  111. try {
  112. $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
  113. $translations->setDomain('shaarli');
  114. $this->translator->loadTranslations($translations);
  115. } catch (\InvalidArgumentException $e) {
  116. }
  117. // Default extension translation from the current theme
  118. $theme = $this->conf->get('theme');
  119. $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $theme .'/language';
  120. if (is_dir($themeTransFolder)) {
  121. try {
  122. $translations = Translations::fromPoFile(
  123. $themeTransFolder .'/'. $this->language .'/LC_MESSAGES/'. $theme .'.po'
  124. );
  125. $translations->setDomain($theme);
  126. $this->translator->loadTranslations($translations);
  127. } catch (\InvalidArgumentException $e) {
  128. }
  129. }
  130. // Extension translations (plugins, themes, etc.).
  131. foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
  132. if ($domain === self::DEFAULT_DOMAIN) {
  133. continue;
  134. }
  135. try {
  136. $extension = Translations::fromPoFile(
  137. $translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po'
  138. );
  139. $extension->setDomain($domain);
  140. $this->translator->loadTranslations($extension);
  141. } catch (\InvalidArgumentException $e) {
  142. }
  143. }
  144. }
  145. /**
  146. * Checks if a language string is valid.
  147. *
  148. * @param string $language e.g. 'fr' or 'en_US'
  149. *
  150. * @return bool true if valid, false otherwise
  151. */
  152. protected function isValidLanguage($language)
  153. {
  154. return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1;
  155. }
  156. /**
  157. * Get the list of available languages for Shaarli.
  158. *
  159. * @return array List of available languages, with their label.
  160. */
  161. public static function getAvailableLanguages()
  162. {
  163. return [
  164. 'auto' => t('Automatic'),
  165. 'en' => t('English'),
  166. 'fr' => t('French'),
  167. 'de' => t('German'),
  168. ];
  169. }
  170. }