PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Translation / Translator.php
matomo / app / core / Translation Last commit date
Loader 1 month ago Weblate 1 year ago Translator.php 1 month ago
Translator.php
304 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Translation;
10
11 use Piwik\Config;
12 use Piwik\Container\StaticContainer;
13 use Piwik\Log\LoggerInterface;
14 use Piwik\Piwik;
15 use Piwik\Translation\Loader\LoaderInterface;
16 /**
17 * Translates messages.
18 *
19 * @api
20 */
21 class Translator
22 {
23 /**
24 * Contains the translated messages, indexed by the language name.
25 *
26 * @var array
27 */
28 private $translations = [];
29 /**
30 * @var string
31 */
32 private $currentLanguage;
33 /**
34 * @var string
35 */
36 private $fallback = 'en';
37 /**
38 * Directories containing the translations to load.
39 *
40 * @var string[]
41 */
42 private $directories = [];
43 /**
44 * @var LoaderInterface
45 */
46 private $loader;
47 private const LIST_TYPE_AND = 'And';
48 private const LIST_TYPE_OR = 'Or';
49 public function __construct(LoaderInterface $loader, ?array $directories = null)
50 {
51 $this->loader = $loader;
52 $this->currentLanguage = $this->getDefaultLanguage();
53 if ($directories === null) {
54 // TODO should be moved out of this class
55 $directories = [PIWIK_INCLUDE_PATH . '/lang'];
56 }
57 $this->directories = $directories;
58 }
59 /**
60 * Clean a string that may contain HTML special chars, single/double quotes, HTML entities, leading/trailing whitespace
61 *
62 * @param string $s
63 * @return string
64 */
65 public static function clean($s)
66 {
67 return html_entity_decode(trim($s), \ENT_QUOTES, 'UTF-8');
68 }
69 /**
70 * Returns an internationalized string using a translation ID. If a translation
71 * cannot be found for the ID, the ID is returned.
72 *
73 * @param string $translationId Translation ID, eg, `General_Date`.
74 * @param array|string|int $args `sprintf` arguments to be applied to the internationalized
75 * string.
76 * @param string|null $language Optionally force the language.
77 * @return string The translated string or `$translationId`.
78 * @api
79 */
80 public function translate($translationId, $args = [], $language = null)
81 {
82 $args = is_array($args) ? $args : [$args];
83 $translationId = $translationId ?? '';
84 if (strpos($translationId, "_") !== \false) {
85 [$plugin, $key] = explode("_", $translationId, 2);
86 $language = is_string($language) ? $language : $this->currentLanguage;
87 $translationId = $this->getTranslation($translationId, $language, $plugin, $key);
88 }
89 if (count($args) == 0) {
90 return str_replace('%%', '%', $translationId);
91 }
92 return vsprintf($translationId, $args);
93 }
94 /**
95 * Converts the given list of items into a listing (e.g. One, Two, and Three)
96 *
97 * @param array $items
98 */
99 public function createAndListing(array $items, ?string $language = null) : string
100 {
101 return $this->createListing(self::LIST_TYPE_AND, $items, $language);
102 }
103 /**
104 * Converts the given list of items into a or listing (e.g. One, Two, or Three)
105 *
106 * @param array $items
107 */
108 public function createOrListing(array $items, ?string $language = null) : string
109 {
110 return $this->createListing(self::LIST_TYPE_OR, $items, $language);
111 }
112 /**
113 * @param string $listType type of the list (LIST_TYPE_AND or LIST_TYPE_OR)
114 * @param array $items
115 */
116 private function createListing(string $listType, array $items, ?string $language = null) : string
117 {
118 switch (count($items)) {
119 case 0:
120 return '';
121 case 1:
122 return end($items);
123 case 2:
124 $pattern = $this->translate('Intl_ListPattern' . $listType . '2', [], $language);
125 return str_replace(['{0}', '{1}'], [$items[0], $items[1]], $pattern);
126 default:
127 $patternStart = $this->translate('Intl_ListPattern' . $listType . 'Start', [], $language);
128 $patternMiddle = $this->translate('Intl_ListPattern' . $listType . 'Middle', [], $language);
129 $patternEnd = $this->translate('Intl_ListPattern' . $listType . 'End', [], $language);
130 $result = $patternStart;
131 while (count($items) > 2) {
132 $pattern = count($items) > 3 ? $patternMiddle : $patternEnd;
133 $result = str_replace(['{0}', '{1}'], [array_shift($items), $pattern], $result);
134 }
135 return str_replace(['{0}', '{1}'], [$items[0], $items[1]], $result);
136 }
137 }
138 /**
139 * @return string
140 */
141 public function getCurrentLanguage()
142 {
143 return $this->currentLanguage;
144 }
145 /**
146 * @param string $language
147 */
148 public function setCurrentLanguage($language)
149 {
150 if (!$language) {
151 $language = $this->getDefaultLanguage();
152 }
153 $this->currentLanguage = $language;
154 }
155 /**
156 * @return string The default configured language.
157 */
158 public function getDefaultLanguage()
159 {
160 $generalSection = Config::getInstance()->General;
161 // the config may not be available (for example, during environment setup), so we default to 'en'
162 // if the config cannot be found.
163 return @$generalSection['default_language'] ?: 'en';
164 }
165 /**
166 * Generate javascript translations array
167 */
168 public function getJavascriptTranslations()
169 {
170 $clientSideTranslations = array();
171 foreach ($this->getClientSideTranslationKeys() as $id) {
172 if (strpos($id, '_') === \false) {
173 StaticContainer::get(LoggerInterface::class)->warning('Unexpected translation key found in client side translations: {translation_key}', ['translation_key' => $id]);
174 continue;
175 }
176 [$plugin, $key] = explode('_', $id, 2);
177 $clientSideTranslations[$id] = $this->decodeEntitiesSafeForHTML($this->getTranslation($id, $this->currentLanguage, $plugin, $key));
178 }
179 $js = 'var translations = ' . json_encode($clientSideTranslations) . ';';
180 $js .= "\n" . 'if (typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' . 'for(var i in translations) { piwik_translations[i] = translations[i];} ';
181 return $js;
182 }
183 /**
184 * Decodes all entities in the given string except of &gt; and &lt;
185 */
186 private function decodeEntitiesSafeForHTML(string $text) : string
187 {
188 // replace encoded html tag entities, as they need to remain encoded
189 $text = str_replace(['&gt;', '&lt;'], ['###gt###', '###lt###'], $text);
190 // decode all remaining entities
191 $text = html_entity_decode($text);
192 // recover encoded html tag entities
193 return str_replace(['###gt###', '###lt###'], ['&gt;', '&lt;'], $text);
194 }
195 /**
196 * Returns the list of client side translations by key. These translations will be outputted
197 * to the translation JavaScript.
198 */
199 private function getClientSideTranslationKeys()
200 {
201 $result = array();
202 /**
203 * Triggered before generating the JavaScript code that allows i18n strings to be used
204 * in the browser.
205 *
206 * Plugins should subscribe to this event to specify which translations
207 * should be available to JavaScript.
208 *
209 * Event handlers should add whole translation keys, ie, keys that include the plugin name.
210 *
211 * **Example**
212 *
213 * public function getClientSideTranslationKeys(&$result)
214 * {
215 * $result[] = "MyPlugin_MyTranslation";
216 * }
217 *
218 * @param array &$result The whole list of client side translation keys.
219 */
220 Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result));
221 $result = array_unique($result);
222 return $result;
223 }
224 /**
225 * Add a directory containing translations.
226 *
227 * @param string $directory
228 */
229 public function addDirectory($directory)
230 {
231 if (isset($this->directories[$directory])) {
232 return;
233 }
234 // index by name to avoid duplicates
235 $this->directories[$directory] = $directory;
236 // clear currently loaded translations to force reloading them
237 $this->translations = array();
238 }
239 /**
240 * Should be used by tests only, and this method should eventually be removed.
241 */
242 public function reset()
243 {
244 $this->currentLanguage = $this->getDefaultLanguage();
245 $this->directories = array(PIWIK_INCLUDE_PATH . '/lang');
246 $this->translations = array();
247 }
248 /**
249 * @param string $translation
250 * @return null|string
251 */
252 public function findTranslationKeyForTranslation($translation)
253 {
254 foreach ($this->getAllTranslations() as $key => $translations) {
255 $possibleKey = array_search($translation, $translations);
256 if (!empty($possibleKey)) {
257 return $key . '_' . $possibleKey;
258 }
259 }
260 return null;
261 }
262 /**
263 * Returns all the translation messages loaded.
264 *
265 * @return array
266 */
267 public function getAllTranslations()
268 {
269 $this->loadTranslations($this->currentLanguage);
270 if (!isset($this->translations[$this->currentLanguage])) {
271 return array();
272 }
273 return $this->translations[$this->currentLanguage];
274 }
275 private function getTranslation($id, $lang, $plugin, $key)
276 {
277 $this->loadTranslations($lang);
278 if (isset($this->translations[$lang][$plugin]) && isset($this->translations[$lang][$plugin][$key])) {
279 return $this->translations[$lang][$plugin][$key];
280 }
281 /**
282 * Fallback for keys moved to new Intl plugin to avoid untranslated string in non core plugins
283 * @todo remove this in Piwik 3.0
284 */
285 if ($plugin != 'Intl') {
286 if (isset($this->translations[$lang]['Intl']) && isset($this->translations[$lang]['Intl'][$key])) {
287 return $this->translations[$lang]['Intl'][$key];
288 }
289 }
290 // fallback
291 if ($lang !== $this->fallback) {
292 return $this->getTranslation($id, $this->fallback, $plugin, $key);
293 }
294 return $id;
295 }
296 private function loadTranslations($language)
297 {
298 if (empty($language) || isset($this->translations[$language])) {
299 return;
300 }
301 $this->translations[$language] = $this->loader->load($language, $this->directories);
302 }
303 }
304