Localization_Translator::translate()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 16
nc 8
nop 2
dl 0
loc 29
rs 9.7333
c 1
b 0
f 0
1
<?php
2
/**
3
 * File containing the {@link Localization_Translator} class.
4
 * @package Localization
5
 * @subpackage Translator
6
 * @see Localization_Translator
7
 */
8
9
declare(strict_types=1);
10
11
namespace AppLocalize;
12
13
use AppUtils\IniHelper;
14
use AppUtils\IniHelper_Exception;
15
16
/**
17
 * Application translation manager used to handle translating
18
 * application texts. Uses results from the {@link Localization_Parser}
19
 * class to determine which strings can be translated.
20
 *
21
 * Translations for each locale are stored in a separate file
22
 * and loaded as needed. The API allows adding new translations
23
 * and saving them to disk.
24
 *
25
 * @package Localization
26
 * @subpackage Translator
27
 * @author Sebastian Mordziol <[email protected]>
28
 * @link http://www.mistralys.com
29
 */
30
class Localization_Translator
31
{
32
    const ERROR_NO_STRINGS_AVAILABLE_FOR_LOCALE = 333101;
33
    const ERROR_CANNOT_SAVE_LOCALE_FILE = 333102;
34
    const ERROR_CANNOT_PARSE_LOCALE_FILE = 333103;
35
    
36
    /**
37
     * @var Localization_Locale
38
     */
39
    private $targetLocale;
40
41
    /**
42
     * Collection of strings per locale, associative array
43
     * with locale name => string pairs.
44
     *
45
     * @var array<string,string[]>
46
     */
47
    private $strings = array();
48
49
    /**
50
     * @var string|null
51
     */
52
    private $targetLocaleName = null;
53
54
    /**
55
     * @var array<string,string>
56
     */
57
    protected $reverseStrings = array();
58
59
    /**
60
    * @var Localization_Source[]
61
    */
62
    private $sources = array();
63
64
    /**
65
     * Indexed array with locale names for which the strings
66
     * have been loaded, used to avoid loading them repeatedly.
67
     *
68
     * @var array
69
     */
70
    private $loaded = array();
71
72
    public function addSource(Localization_Source $source) : void
73
    {
74
        $this->sources[] = $source;
75
    }
76
77
    /**
78
     * @param Localization_Source[] $sources
79
     */
80
    public function addSources(array $sources) : void
81
    {
82
        foreach($sources as $source) {
83
            $this->addSource($source);
84
        }
85
    }
86
87
    /**
88
     * Sets the locale to translate strings to. If this is
89
     * not the same as the currently selected locale, this
90
     * triggers loading the locale's strings file.
91
     *
92
     * @param Localization_Locale $locale
93
     * @throws Localization_Exception
94
     */
95
    public function setTargetLocale(Localization_Locale $locale) : void
96
    {
97
        // don't do anything if it's the same locale
98
        if (isset($this->targetLocale) && $locale->getName() == $this->targetLocale->getName()) {
99
            return;
100
        }
101
102
        $this->targetLocale = $locale;
103
        $this->targetLocaleName = $locale->getName();
104
        $this->load($locale);
105
    }
106
107
    /**
108
     * Loads the strings for the specified locale if the
109
     * according storage file exists.
110
     *
111
     * @param Localization_Locale $locale
112
     * @return boolean
113
     * @throws Localization_Exception
114
     */
115
    protected function load(Localization_Locale $locale) : bool
116
    {
117
        // initialize the storage array regardless of success
118
        $localeName = $locale->getName();
119
        if (in_array($localeName, $this->loaded)) {
120
            return true;
121
        }
122
123
        if (!isset($this->strings[$localeName])) {
124
            $this->strings[$localeName] = array();
125
        }
126
127
        foreach($this->sources as $source)
128
        {
129
            $file = $this->resolveStorageFile($locale, $source);
130
            if (!file_exists($file)) {
131
                continue;
132
            }
133
            
134
            $data = parse_ini_file($file, false);
135
            
136
            if($data === false) 
137
            {
138
                throw new Localization_Exception(
139
                    'Malformatted localization file',
140
                    sprintf(
141
                        'The localization ini file %1$s cannot be parsed.',
142
                        $file
143
                    ),
144
                    self::ERROR_CANNOT_PARSE_LOCALE_FILE
145
                );
146
            }
147
    
148
            $this->strings[$localeName] = array_merge(
149
                $this->strings[$localeName],
150
                $data
151
            );
152
        }
153
        
154
        $this->loaded[] = $localeName;
155
        
156
        return true;
157
    }
158
159
    /**
160
     * Saves the current string collections for the target
161
     * locale to disk. The format is the regular PHP .ini
162
     * format with string hash => text pairs without sections.
163
     *
164
     * These files may be edited manually as well if needed,
165
     * but new strings can only be added via the UI because
166
     * the hashes have to be created.
167
     *
168
     * @param Localization_Source $source
169
     * @param Localization_Scanner_StringsCollection $collection
170
     */
171
    public function save(Localization_Source $source, Localization_Scanner_StringsCollection $collection) : void
172
    {
173
        // the serverside strings file gets all available hashes,
174
        // which are filtered by source.
175
        $this->renderStringsFile(
176
            'Serverside',
177
            $source,
178
            $collection->getHashes(),
179
            $this->resolveStorageFile($this->targetLocale, $source),
180
            $this->targetLocale
181
        );
182
        
183
        // the clientside strings file only gets the JS hashes.
184
        $this->renderStringsFile(
185
            'Clientside',
186
            $source,
187
            $collection->getHashesByLanguageID('Javascript'),
188
            $this->getClientStorageFile($this->targetLocale, $source),
189
            $this->targetLocale,
190
            false
191
        );
192
    }
193
    
194
   /**
195
    * Retrieves all available strings for the specified locale,
196
    * as hash => text value pairs.
197
    * 
198
    * @param Localization_Locale $locale
199
    * @throws Localization_Exception
200
    * @return string[]
201
    */
202
    public function getStrings(Localization_Locale $locale) : array
203
    {
204
        $this->load($locale);
205
        
206
        $name = $locale->getName();
207
        
208
        if(isset($this->strings[$name])) {
209
            return $this->strings[$name];
210
        }
211
        
212
        throw new Localization_Exception(
213
            'No strings available for '.$name,
214
            sprintf(
215
                'Tried getting strings for the locale [%s], but it has no strings. Available locales are: [%s].',
216
                $name,
217
                implode(', ', array_keys($this->strings))
218
            ),
219
            self::ERROR_NO_STRINGS_AVAILABLE_FOR_LOCALE
220
        );
221
    }
222
223
    /**
224
     * @param Localization_Locale $locale
225
     * @return bool
226
     * @throws Localization_Exception
227
     */
228
    public function hasStrings(Localization_Locale $locale) : bool
229
    {
230
        $this->load($locale);
231
        
232
        return !empty($this->strings[$locale->getName()]);
233
    }
234
235
    /**
236
     * @param string $type
237
     * @param Localization_Source $source
238
     * @param Localization_Scanner_StringHash[] $hashes
239
     * @param string $file
240
     * @param Localization_Locale $locale
241
     * @param boolean $editable
242
     */
243
    protected function renderStringsFile(string $type, Localization_Source $source, array $hashes, string $file, Localization_Locale $locale, bool $editable=true) : void
244
    {
245
        $writer = new Localization_Writer($locale, $type, $file);
246
247
        if($editable)
248
        {
249
            $writer->makeEditable();
250
        }
251
        
252
        $sourceID = $source->getID();
253
        
254
        foreach ($hashes as $hash) 
255
        {
256
            if(!$hash->hasSourceID($sourceID)) {
257
                continue;
258
            }
259
            
260
            $text = $this->getHashTranslation($hash->getHash(), $locale);
261
262
            // skip any empty strings
263
            if($text === null || trim($text) == '') {
264
                continue;
265
            }
266
            
267
            $writer->addHash($hash->getHash(), $text);
268
        }
269
        
270
        $writer->writeFile();
271
    }
272
273
    /**
274
     * Retrieves the full path to the strings storage ini file.
275
     *
276
     * @param Localization_Locale $locale
277
     * @param Localization_Source $source
278
     * @return string
279
     */
280
    protected function resolveStorageFile(Localization_Locale $locale, Localization_Source $source) : string
281
    {
282
        return sprintf(
283
            '%1$s/%2$s-%3$s-server.ini',
284
            $source->getStorageFolder(),
285
            $locale->getName(),
286
            $source->getAlias()
287
        );
288
    }
289
290
    /**
291
     * Retrieves the full path to the strings storage ini file
292
     * for the clientside strings.
293
     *
294
     * @param Localization_Locale $locale
295
     * @param Localization_Source $source
296
     * @return string
297
     */
298
    protected function getClientStorageFile(Localization_Locale $locale, Localization_Source $source) : string
299
    {
300
        return sprintf(
301
            '%1$s/%2$s-%3$s-client.ini',
302
            $source->getStorageFolder(),
303
            $locale->getName(),
304
            $source->getAlias()
305
        );
306
    }
307
308
    /**
309
     * Sets the translation for a specific string hash
310
     * @param string $hash
311
     * @param string $text
312
     */
313
    public function setTranslation(string $hash, string $text) : void
314
    {
315
        $this->strings[$this->targetLocale->getName()][$hash] = $text;
316
    }
317
318
    /**
319
     * Clears a translation for the specified string hash
320
     * @param string $hash
321
     */
322
    public function clearTranslation(string $hash) : void
323
    {
324
        unset($this->strings[$this->targetLocale->getName()][$hash]);
325
    }
326
327
    /**
328
     * Translates a string. The first parameter has to be the string
329
     * to translate, additional parameters are variables to insert
330
     * into the string.
331
     *
332
     * @param string $text
333
     * @param array $args
334
     * @return string
335
     * @throws Localization_Exception
336
     */
337
    public function translate(string $text, array $args) : string
338
    {
339
        // to avoid re-creating the hash for the same texts over and over,
340
        // we keep track of the hashes we created, and re-use them.
341
        if(isset($this->reverseStrings[$text])) {
342
            $hash = $this->reverseStrings[$text];
343
        } else {
344
            $hash = md5($text);
345
            $this->reverseStrings[$text] = $hash;
346
        }
347
348
        // replace the text with the one we have on record, otherwise
349
        // simply leave the original unchanged.
350
        if (isset($this->strings[$this->targetLocaleName][$hash])) {
351
            $text = $this->strings[$this->targetLocaleName][$hash];
352
        }
353
354
        array_unshift($args, $text);
355
356
        $result = call_user_func('sprintf', ...$args);
357
        if (is_string($result)) {
358
            return $result;
359
        }
360
361
        throw new Localization_Exception(
362
            'Incorrectly translated string or erroneous localized string',
363
            sprintf(
364
                'The string %1$s seems to have too many or too few arguments.',
365
                $text
366
            )
367
        );
368
    }
369
370
    /**
371
     * Checks if the specified string hash exists.
372
     * @param string $hash
373
     * @return boolean
374
     */
375
    public function hashExists(string $hash) : bool
376
    {
377
        return array_key_exists($hash, $this->strings[$this->targetLocale->getName()]);
378
    }
379
380
    /**
381
     * Checks if a translation for the specified string hash exists.
382
     * @param string $text
383
     * @return boolean
384
     */
385
    public function translationExists(string $text) : bool
386
    {
387
        return array_key_exists(md5($text), $this->strings[$this->targetLocale->getName()]);
388
    }
389
390
    /**
391
     * Retrieves the locale into which texts are currently translated.
392
     * @return Localization_Locale
393
     */
394
    public function getTargetLocale() : Localization_Locale
395
    {
396
        return $this->targetLocale;
397
    }
398
399
    /**
400
     * Retrieves the translation for the specified string hash.
401
     *
402
     * @param string $hash
403
     * @param Localization_Locale|null $locale
404
     * @return string|NULL
405
     */
406
    public function getHashTranslation(string $hash, ?Localization_Locale $locale=null) : ?string
407
    {
408
        if(!$locale) {
409
            $locale = $this->targetLocale;
410
        }
411
        
412
        $localeName = $locale->getName();
413
        
414
        if(isset($this->strings[$localeName]) && isset($this->strings[$localeName][$hash])) {
415
            return $this->strings[$localeName][$hash];
416
        }
417
418
        return null;
419
    }
420
421
    /**
422
     * Retrieves only the strings that are available clientside.
423
     *
424
     * @param Localization_Locale $locale
425
     * @return array<string,string>
426
     * @throws IniHelper_Exception
427
     */
428
    public function getClientStrings(Localization_Locale $locale) : array
429
    {
430
        $result = array();
431
        
432
        foreach($this->sources as $source) 
433
        {
434
            $localeFile = self::getClientStorageFile($locale, $source);
0 ignored issues
show
Bug Best Practice introduced by
The method AppLocalize\Localization...:getClientStorageFile() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

434
            /** @scrutinizer ignore-call */ 
435
            $localeFile = self::getClientStorageFile($locale, $source);
Loading history...
435
            if(!file_exists($localeFile)) {
436
                continue;
437
            }
438
439
            $strings = IniHelper::createFromFile($localeFile)->toArray();
440
441
            $result = array_merge($result, $strings);
442
        }
443
        
444
        return $result;
445
    }
446
}