PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.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
307 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\Piwik;
13 use Piwik\Translation\Loader\LoaderInterface;
14 /**
15 * Translates messages.
16 *
17 * @api
18 */
19 class Translator
20 {
21 /**
22 * Contains the translated messages, indexed by the language name.
23 *
24 * @var array
25 */
26 private $translations = [];
27 /**
28 * @var string
29 */
30 private $currentLanguage;
31 /**
32 * @var string
33 */
34 private $fallback = 'en';
35 /**
36 * Directories containing the translations to load.
37 *
38 * @var string[]
39 */
40 private $directories = [];
41 /**
42 * @var LoaderInterface
43 */
44 private $loader;
45 private const LIST_TYPE_AND = 'And';
46 private const LIST_TYPE_OR = 'Or';
47 public function __construct(LoaderInterface $loader, ?array $directories = null)
48 {
49 $this->loader = $loader;
50 $this->currentLanguage = $this->getDefaultLanguage();
51 if ($directories === null) {
52 // TODO should be moved out of this class
53 $directories = [PIWIK_INCLUDE_PATH . '/lang'];
54 }
55 $this->directories = $directories;
56 }
57 /**
58 * Clean a string that may contain HTML special chars, single/double quotes, HTML entities, leading/trailing whitespace
59 *
60 * @param string $s
61 * @return string
62 */
63 public static function clean($s)
64 {
65 return html_entity_decode(trim($s), \ENT_QUOTES, 'UTF-8');
66 }
67 /**
68 * Returns an internationalized string using a translation ID. If a translation
69 * cannot be found for the ID, the ID is returned.
70 *
71 * @param string $translationId Translation ID, eg, `General_Date`.
72 * @param array|string|int $args `sprintf` arguments to be applied to the internationalized
73 * string.
74 * @param string|null $language Optionally force the language.
75 * @return string The translated string or `$translationId`.
76 * @api
77 */
78 public function translate($translationId, $args = [], $language = null)
79 {
80 $args = is_array($args) ? $args : [$args];
81 $translationId = $translationId ?? '';
82 if (strpos($translationId, "_") !== \false) {
83 [$plugin, $key] = explode("_", $translationId, 2);
84 $language = is_string($language) ? $language : $this->currentLanguage;
85 $translationId = $this->getTranslation($translationId, $language, $plugin, $key);
86 }
87 if (count($args) == 0) {
88 return str_replace('%%', '%', $translationId);
89 }
90 return vsprintf($translationId, $args);
91 }
92 /**
93 * Converts the given list of items into a listing (e.g. One, Two, and Three)
94 *
95 * @param array $items
96 * @param string|null $language
97 * @return string
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 * @param string|null $language
108 * @return string
109 */
110 public function createOrListing(array $items, ?string $language = null) : string
111 {
112 return $this->createListing(self::LIST_TYPE_OR, $items, $language);
113 }
114 /**
115 * @param string $listType type of the list (LIST_TYPE_AND or LIST_TYPE_OR)
116 * @param array $items
117 * @param string|null $language
118 * @return string
119 */
120 private function createListing(string $listType, array $items, ?string $language = null) : string
121 {
122 switch (count($items)) {
123 case 0:
124 return '';
125 case 1:
126 return end($items);
127 case 2:
128 $pattern = $this->translate('Intl_ListPattern' . $listType . '2', [], $language);
129 return str_replace(['{0}', '{1}'], [$items[0], $items[1]], $pattern);
130 default:
131 $patternStart = $this->translate('Intl_ListPattern' . $listType . 'Start', [], $language);
132 $patternMiddle = $this->translate('Intl_ListPattern' . $listType . 'Middle', [], $language);
133 $patternEnd = $this->translate('Intl_ListPattern' . $listType . 'End', [], $language);
134 $result = $patternStart;
135 while (count($items) > 2) {
136 $pattern = count($items) > 3 ? $patternMiddle : $patternEnd;
137 $result = str_replace(['{0}', '{1}'], [array_shift($items), $pattern], $result);
138 }
139 return str_replace(['{0}', '{1}'], [$items[0], $items[1]], $result);
140 }
141 }
142 /**
143 * @return string
144 */
145 public function getCurrentLanguage()
146 {
147 return $this->currentLanguage;
148 }
149 /**
150 * @param string $language
151 */
152 public function setCurrentLanguage($language)
153 {
154 if (!$language) {
155 $language = $this->getDefaultLanguage();
156 }
157 $this->currentLanguage = $language;
158 }
159 /**
160 * @return string The default configured language.
161 */
162 public function getDefaultLanguage()
163 {
164 $generalSection = Config::getInstance()->General;
165 // the config may not be available (for example, during environment setup), so we default to 'en'
166 // if the config cannot be found.
167 return @$generalSection['default_language'] ?: 'en';
168 }
169 /**
170 * Generate javascript translations array
171 */
172 public function getJavascriptTranslations()
173 {
174 $clientSideTranslations = array();
175 foreach ($this->getClientSideTranslationKeys() as $id) {
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 * @param string $text
187 * @return string
188 */
189 private function decodeEntitiesSafeForHTML(string $text) : string
190 {
191 // replace encoded html tag entities, as they need to remain encoded
192 $text = str_replace(['&gt;', '&lt;'], ['###gt###', '###lt###'], $text);
193 // decode all remaining entities
194 $text = html_entity_decode($text);
195 // recover encoded html tag entities
196 return str_replace(['###gt###', '###lt###'], ['&gt;', '&lt;'], $text);
197 }
198 /**
199 * Returns the list of client side translations by key. These translations will be outputted
200 * to the translation JavaScript.
201 */
202 private function getClientSideTranslationKeys()
203 {
204 $result = array();
205 /**
206 * Triggered before generating the JavaScript code that allows i18n strings to be used
207 * in the browser.
208 *
209 * Plugins should subscribe to this event to specify which translations
210 * should be available to JavaScript.
211 *
212 * Event handlers should add whole translation keys, ie, keys that include the plugin name.
213 *
214 * **Example**
215 *
216 * public function getClientSideTranslationKeys(&$result)
217 * {
218 * $result[] = "MyPlugin_MyTranslation";
219 * }
220 *
221 * @param array &$result The whole list of client side translation keys.
222 */
223 Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result));
224 $result = array_unique($result);
225 return $result;
226 }
227 /**
228 * Add a directory containing translations.
229 *
230 * @param string $directory
231 */
232 public function addDirectory($directory)
233 {
234 if (isset($this->directories[$directory])) {
235 return;
236 }
237 // index by name to avoid duplicates
238 $this->directories[$directory] = $directory;
239 // clear currently loaded translations to force reloading them
240 $this->translations = array();
241 }
242 /**
243 * Should be used by tests only, and this method should eventually be removed.
244 */
245 public function reset()
246 {
247 $this->currentLanguage = $this->getDefaultLanguage();
248 $this->directories = array(PIWIK_INCLUDE_PATH . '/lang');
249 $this->translations = array();
250 }
251 /**
252 * @param string $translation
253 * @return null|string
254 */
255 public function findTranslationKeyForTranslation($translation)
256 {
257 foreach ($this->getAllTranslations() as $key => $translations) {
258 $possibleKey = array_search($translation, $translations);
259 if (!empty($possibleKey)) {
260 return $key . '_' . $possibleKey;
261 }
262 }
263 return null;
264 }
265 /**
266 * Returns all the translation messages loaded.
267 *
268 * @return array
269 */
270 public function getAllTranslations()
271 {
272 $this->loadTranslations($this->currentLanguage);
273 if (!isset($this->translations[$this->currentLanguage])) {
274 return array();
275 }
276 return $this->translations[$this->currentLanguage];
277 }
278 private function getTranslation($id, $lang, $plugin, $key)
279 {
280 $this->loadTranslations($lang);
281 if (isset($this->translations[$lang][$plugin]) && isset($this->translations[$lang][$plugin][$key])) {
282 return $this->translations[$lang][$plugin][$key];
283 }
284 /**
285 * Fallback for keys moved to new Intl plugin to avoid untranslated string in non core plugins
286 * @todo remove this in Piwik 3.0
287 */
288 if ($plugin != 'Intl') {
289 if (isset($this->translations[$lang]['Intl']) && isset($this->translations[$lang]['Intl'][$key])) {
290 return $this->translations[$lang]['Intl'][$key];
291 }
292 }
293 // fallback
294 if ($lang !== $this->fallback) {
295 return $this->getTranslation($id, $this->fallback, $plugin, $key);
296 }
297 return $id;
298 }
299 private function loadTranslations($language)
300 {
301 if (empty($language) || isset($this->translations[$language])) {
302 return;
303 }
304 $this->translations[$language] = $this->loader->load($language, $this->directories);
305 }
306 }
307