PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.3.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.3.1
5.13.0 5.12.1 5.12.0 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 year ago Weblate 1 year ago Translator.php 1 year ago
Translator.php
313 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 * @param string|null $language
99 * @return string
100 */
101 public function createAndListing(array $items, ?string $language = null) : string
102 {
103 return $this->createListing(self::LIST_TYPE_AND, $items, $language);
104 }
105 /**
106 * Converts the given list of items into a or listing (e.g. One, Two, or Three)
107 *
108 * @param array $items
109 * @param string|null $language
110 * @return string
111 */
112 public function createOrListing(array $items, ?string $language = null) : string
113 {
114 return $this->createListing(self::LIST_TYPE_OR, $items, $language);
115 }
116 /**
117 * @param string $listType type of the list (LIST_TYPE_AND or LIST_TYPE_OR)
118 * @param array $items
119 * @param string|null $language
120 * @return string
121 */
122 private function createListing(string $listType, array $items, ?string $language = null) : string
123 {
124 switch (count($items)) {
125 case 0:
126 return '';
127 case 1:
128 return end($items);
129 case 2:
130 $pattern = $this->translate('Intl_ListPattern' . $listType . '2', [], $language);
131 return str_replace(['{0}', '{1}'], [$items[0], $items[1]], $pattern);
132 default:
133 $patternStart = $this->translate('Intl_ListPattern' . $listType . 'Start', [], $language);
134 $patternMiddle = $this->translate('Intl_ListPattern' . $listType . 'Middle', [], $language);
135 $patternEnd = $this->translate('Intl_ListPattern' . $listType . 'End', [], $language);
136 $result = $patternStart;
137 while (count($items) > 2) {
138 $pattern = count($items) > 3 ? $patternMiddle : $patternEnd;
139 $result = str_replace(['{0}', '{1}'], [array_shift($items), $pattern], $result);
140 }
141 return str_replace(['{0}', '{1}'], [$items[0], $items[1]], $result);
142 }
143 }
144 /**
145 * @return string
146 */
147 public function getCurrentLanguage()
148 {
149 return $this->currentLanguage;
150 }
151 /**
152 * @param string $language
153 */
154 public function setCurrentLanguage($language)
155 {
156 if (!$language) {
157 $language = $this->getDefaultLanguage();
158 }
159 $this->currentLanguage = $language;
160 }
161 /**
162 * @return string The default configured language.
163 */
164 public function getDefaultLanguage()
165 {
166 $generalSection = Config::getInstance()->General;
167 // the config may not be available (for example, during environment setup), so we default to 'en'
168 // if the config cannot be found.
169 return @$generalSection['default_language'] ?: 'en';
170 }
171 /**
172 * Generate javascript translations array
173 */
174 public function getJavascriptTranslations()
175 {
176 $clientSideTranslations = array();
177 foreach ($this->getClientSideTranslationKeys() as $id) {
178 if (strpos($id, '_') === \false) {
179 StaticContainer::get(LoggerInterface::class)->warning('Unexpected translation key found in client side translations: {translation_key}', ['translation_key' => $id]);
180 continue;
181 }
182 [$plugin, $key] = explode('_', $id, 2);
183 $clientSideTranslations[$id] = $this->decodeEntitiesSafeForHTML($this->getTranslation($id, $this->currentLanguage, $plugin, $key));
184 }
185 $js = 'var translations = ' . json_encode($clientSideTranslations) . ';';
186 $js .= "\n" . 'if (typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' . 'for(var i in translations) { piwik_translations[i] = translations[i];} ';
187 return $js;
188 }
189 /**
190 * Decodes all entities in the given string except of &gt; and &lt;
191 *
192 * @param string $text
193 * @return string
194 */
195 private function decodeEntitiesSafeForHTML(string $text) : string
196 {
197 // replace encoded html tag entities, as they need to remain encoded
198 $text = str_replace(['&gt;', '&lt;'], ['###gt###', '###lt###'], $text);
199 // decode all remaining entities
200 $text = html_entity_decode($text);
201 // recover encoded html tag entities
202 return str_replace(['###gt###', '###lt###'], ['&gt;', '&lt;'], $text);
203 }
204 /**
205 * Returns the list of client side translations by key. These translations will be outputted
206 * to the translation JavaScript.
207 */
208 private function getClientSideTranslationKeys()
209 {
210 $result = array();
211 /**
212 * Triggered before generating the JavaScript code that allows i18n strings to be used
213 * in the browser.
214 *
215 * Plugins should subscribe to this event to specify which translations
216 * should be available to JavaScript.
217 *
218 * Event handlers should add whole translation keys, ie, keys that include the plugin name.
219 *
220 * **Example**
221 *
222 * public function getClientSideTranslationKeys(&$result)
223 * {
224 * $result[] = "MyPlugin_MyTranslation";
225 * }
226 *
227 * @param array &$result The whole list of client side translation keys.
228 */
229 Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result));
230 $result = array_unique($result);
231 return $result;
232 }
233 /**
234 * Add a directory containing translations.
235 *
236 * @param string $directory
237 */
238 public function addDirectory($directory)
239 {
240 if (isset($this->directories[$directory])) {
241 return;
242 }
243 // index by name to avoid duplicates
244 $this->directories[$directory] = $directory;
245 // clear currently loaded translations to force reloading them
246 $this->translations = array();
247 }
248 /**
249 * Should be used by tests only, and this method should eventually be removed.
250 */
251 public function reset()
252 {
253 $this->currentLanguage = $this->getDefaultLanguage();
254 $this->directories = array(PIWIK_INCLUDE_PATH . '/lang');
255 $this->translations = array();
256 }
257 /**
258 * @param string $translation
259 * @return null|string
260 */
261 public function findTranslationKeyForTranslation($translation)
262 {
263 foreach ($this->getAllTranslations() as $key => $translations) {
264 $possibleKey = array_search($translation, $translations);
265 if (!empty($possibleKey)) {
266 return $key . '_' . $possibleKey;
267 }
268 }
269 return null;
270 }
271 /**
272 * Returns all the translation messages loaded.
273 *
274 * @return array
275 */
276 public function getAllTranslations()
277 {
278 $this->loadTranslations($this->currentLanguage);
279 if (!isset($this->translations[$this->currentLanguage])) {
280 return array();
281 }
282 return $this->translations[$this->currentLanguage];
283 }
284 private function getTranslation($id, $lang, $plugin, $key)
285 {
286 $this->loadTranslations($lang);
287 if (isset($this->translations[$lang][$plugin]) && isset($this->translations[$lang][$plugin][$key])) {
288 return $this->translations[$lang][$plugin][$key];
289 }
290 /**
291 * Fallback for keys moved to new Intl plugin to avoid untranslated string in non core plugins
292 * @todo remove this in Piwik 3.0
293 */
294 if ($plugin != 'Intl') {
295 if (isset($this->translations[$lang]['Intl']) && isset($this->translations[$lang]['Intl'][$key])) {
296 return $this->translations[$lang]['Intl'][$key];
297 }
298 }
299 // fallback
300 if ($lang !== $this->fallback) {
301 return $this->getTranslation($id, $this->fallback, $plugin, $key);
302 }
303 return $id;
304 }
305 private function loadTranslations($language)
306 {
307 if (empty($language) || isset($this->translations[$language])) {
308 return;
309 }
310 $this->translations[$language] = $this->loader->load($language, $this->directories);
311 }
312 }
313