Passed
Push — master ( eaf67c...b24cdf )
by Sebastian
04:59
created

Localization_Translator   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 394
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 124
c 2
b 0
f 0
dl 0
loc 394
rs 9.2
wmc 40

18 Methods

Rating   Name   Duplication   Size   Complexity  
A setTargetLocale() 0 10 3
A addSources() 0 4 2
A addSource() 0 3 1
B load() 0 42 6
A hasStrings() 0 5 1
A save() 0 20 1
A getStrings() 0 18 2
A clearTranslation() 0 3 1
A hashExists() 0 3 1
A getClientStorageFile() 0 7 1
A getHashTranslation() 0 13 4
A translate() 0 32 4
A setTranslation() 0 3 1
A translationExists() 0 3 1
A resolveStorageFile() 0 7 1
A getClientStrings() 0 17 3
A getTargetLocale() 0 3 1
A renderStringsFile() 0 28 6

How to fix   Complexity   

Complex Class

Complex classes like Localization_Translator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Localization_Translator, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * File containing the {@link Localization_Translator} class.
4
 * @package Localization
5
 * @subpackage Translator
6
 * @see Localization_Translator
7
 */
8
9
namespace AppLocalize;
10
11
/**
12
 * Application translation manager used to handle translating
13
 * application texts. Uses results from the {@link Localization_Parser}
14
 * class to determine which strings can be translated.
15
 *
16
 * Translations for each locale are stored in a separate file
17
 * and loaded as needed. The API allows adding new translations
18
 * and saving them to disk.
19
 *
20
 * @package Localization
21
 * @subpackage Translator
22
 * @author Sebastian Mordziol <[email protected]>
23
 * @link http://www.mistralys.com
24
 */
25
class Localization_Translator
26
{
27
    const ERROR_NO_STRINGS_AVAILABLE_FOR_LOCALE = 333101;
28
    
29
    const ERROR_CANNOT_SAVE_LOCALE_FILE = 333102;
30
    
31
    const ERROR_CANNOT_PARSE_LOCALE_FILE = 333103;
32
    
33
    /**
34
     * @var Localization_Locale
35
     */
36
    private $targetLocale;
37
38
    /**
39
     * Collection of strings per locale, associative array
40
     * with locale name => string pairs.
41
     *
42
     * @var array
43
     */
44
    private $strings = array();
45
    
46
    private $targetLocaleName = null;
47
    
48
   /**
49
    * @var Localization_Source[]
50
    */
51
    private $sources = array();
52
53
    public function addSource(Localization_Source $source)
54
    {
55
        $this->sources[] = $source;
56
    }
57
    
58
    public function addSources($sources)
59
    {
60
        foreach($sources as $source) {
61
            $this->addSource($source);
62
        }
63
    }
64
65
    /**
66
     * Sets the locale to translate strings to. If this is
67
     * not the same as the currently selected locale, this
68
     * triggers loading the locale's strings file.
69
     *
70
     * @param Localization_Locale $locale
71
     */
72
    public function setTargetLocale(Localization_Locale $locale)
73
    {
74
        // don't do anything if it's the same locale
75
        if (isset($this->targetLocale) && $locale->getName() == $this->targetLocale->getName()) {
76
            return;
77
        }
78
79
        $this->targetLocale = $locale;
80
        $this->targetLocaleName = $locale->getName();
81
        $this->load($locale);
82
    }
83
84
    /**
85
     * Indexed array with locale names for which the strings
86
     * have been loaded, used to avoid loading them repatedly.
87
     *
88
     * @var array
89
     */
90
    private $loaded = array();
91
92
    /**
93
     * Loads the strings for the specified locale if the
94
     * according storage file exists.
95
     *
96
     * @param Localization_Locale $locale
97
     * @return boolean
98
     */
99
    protected function load(Localization_Locale $locale)
100
    {
101
        // initialize the storage array regardless of success
102
        $localeName = $locale->getName();
103
        if (in_array($localeName, $this->loaded)) {
104
            return true;
105
        }
106
107
        if (!isset($this->strings[$localeName])) {
108
            $this->strings[$localeName] = array();
109
        }
110
111
        foreach($this->sources as $source)
112
        {
113
            $file = $this->resolveStorageFile($locale, $source);
114
            if (!file_exists($file)) {
115
                continue;
116
            }
117
            
118
            $data = parse_ini_file($file, false);
119
            
120
            if($data === false) 
121
            {
122
                throw new Localization_Exception(
123
                    'Malformatted localization file',
124
                    sprintf(
125
                        'The localization ini file %1$s cannot be parsed.',
126
                        $file
127
                    ),
128
                    self::ERROR_CANNOT_PARSE_LOCALE_FILE
129
                );
130
            }
131
    
132
            $this->strings[$localeName] = array_merge(
133
                $this->strings[$localeName],
134
                $data
135
            );
136
        }
137
        
138
        $this->loaded[] = $localeName;
139
        
140
        return true;
141
    }
142
143
    /**
144
     * Saves the current string collections for the target
145
     * locale to disk. The format is the regular PHP .ini
146
     * format with string hash => text pairs without sections.
147
     *
148
     * These files may be edited manually as well if needed,
149
     * but new strings can only be added via the UI because
150
     * the hashes have to be created.
151
     *
152
     * @param Localization_Source $source
153
     * @param Localization_Scanner_StringsCollection $collection
154
     */
155
    public function save(Localization_Source $source, Localization_Scanner_StringsCollection $collection) : void
156
    {
157
        // the serverside strings file gets all available hashes,
158
        // which are filtered by source.
159
        $this->renderStringsFile(
160
            'Serverside',
161
            $source,
162
            $collection->getHashes(),
163
            $this->resolveStorageFile($this->targetLocale, $source),
164
            $this->targetLocale
165
        );
166
        
167
        // the clientside strings file only gets the JS hashes.
168
        $this->renderStringsFile(
169
            'Clientside',
170
            $source,
171
            $collection->getHashesByLanguageID('Javascript'),
172
            $this->getClientStorageFile($this->targetLocale, $source),
173
            $this->targetLocale,
174
            false
175
        );
176
    }
177
    
178
   /**
179
    * Retrieves all available strings for the specified locale,
180
    * as hash => text value pairs.
181
    * 
182
    * @param Localization_Locale $locale
183
    * @throws Localization_Exception
184
    * @return string[]
185
    */
186
    public function getStrings(Localization_Locale $locale)
187
    {
188
        $this->load($locale);
189
        
190
        $name = $locale->getName();
191
        
192
        if(isset($this->strings[$name])) {
193
            return $this->strings[$name];
194
        }
195
        
196
        throw new Localization_Exception(
197
            'No strings available for '.$name,
198
            sprintf(
199
                'Tried getting strings for the locale [%s], but it has no strings. Available locales are: [%s].',
200
                $name,
201
                implode(', ', array_keys($this->strings))
202
            ),
203
            self::ERROR_NO_STRINGS_AVAILABLE_FOR_LOCALE
204
        );
205
    }
206
    
207
    public function hasStrings(Localization_Locale $locale) : bool
208
    {
209
        $this->load($locale);
210
        
211
        return !empty($this->strings[$locale->getName()]);
212
    }
213
    
214
   /**
215
    * @param string $type
216
    * @param Localization_Scanner_StringHash[] $hashes
217
    * @param string $file
218
    * @param Localization_Locale $locale
219
    * @param boolean $editable
220
    * @throws Localization_Exception
221
    */
222
    protected function renderStringsFile($type, Localization_Source $source, $hashes, $file, Localization_Locale $locale, $editable=true)
223
    {
224
        $writer = new Localization_Writer($locale, $type, $file);
225
226
        if($editable)
227
        {
228
            $writer->makeEditable();
229
        }
230
        
231
        $sourceID = $source->getID();
232
        
233
        foreach ($hashes as $hash) 
234
        {
235
            if(!$hash->hasSourceID($sourceID)) {
236
                continue;
237
            }
238
            
239
            $text = $this->getHashTranslation($hash->getHash(), $locale);
240
241
            // skip any empty strings
242
            if($text === null || trim($text) == '') {
243
                continue;
244
            }
245
            
246
            $writer->addHash($hash, $text);
0 ignored issues
show
Bug introduced by
$hash of type AppLocalize\Localization_Scanner_StringHash is incompatible with the type string expected by parameter $hash of AppLocalize\Localization_Writer::addHash(). ( Ignorable by Annotation )

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

246
            $writer->addHash(/** @scrutinizer ignore-type */ $hash, $text);
Loading history...
247
        }
248
        
249
        $writer->writeFile();
250
    }
251
    
252
    /**
253
     * Retrieves the full path to the strings storage ini file.
254
     * 
255
     * @param Localization_Locale $locale
256
     * @return string
257
     */
258
    protected function resolveStorageFile(Localization_Locale $locale, Localization_Source $source)
259
    {
260
        return sprintf(
261
            '%1$s/%2$s-%3$s-server.ini',
262
            $source->getStorageFolder(),
263
            $locale->getName(),
264
            $source->getAlias()
265
        );
266
    }
267
    
268
   /**
269
    * Retrieves the full path to the strings storage ini file 
270
    * for the clientside strings.
271
    * 
272
    * @param Localization_Locale $locale
273
    * @return string
274
    */
275
    protected function getClientStorageFile(Localization_Locale $locale, Localization_Source $source)
276
    {
277
        return sprintf(
278
            '%1$s/%2$s-%3$s-client.ini',
279
            $source->getStorageFolder(),
280
            $locale->getName(),
281
            $source->getAlias()
282
        );
283
    }
284
285
    /**
286
     * Sets the translation for a specific string hash
287
     * @param string $hash
288
     * @param string $text
289
     */
290
    public function setTranslation($hash, $text)
291
    {
292
        $this->strings[$this->targetLocale->getName()][$hash] = $text;
293
    }
294
295
    /**
296
     * Clears a translation for the specified string hash
297
     * @param string $hash
298
     */
299
    public function clearTranslation($hash)
300
    {
301
        unset($this->strings[$this->targetLocale->getName()][$hash]);
302
    }
303
    
304
    protected $reverseStrings = array();
305
306
    /**
307
     * Translates a string. The first parameter has to be the string
308
     * to translate, additional parameters are variables to insert
309
     * into the string.
310
     *
311
     * @return string|null
312
     */
313
    public function translate()
314
    {
315
        $args = func_get_args();
316
        $text = $args[0];
317
        
318
        // to avoid re-creating the hash for the same texts over and over,
319
        // we keep track of the hashes we created, and re-use them.
320
        if(isset($this->reverseStrings[$text])) {
321
            $hash = $this->reverseStrings[$text];
322
        } else {
323
            $hash = md5($text);
324
            $this->reverseStrings[$text] = $hash;
325
        }
326
327
        // replace the text with the one we have on record, otherwise
328
        // simply leave the original unchanged.
329
        if (isset($this->strings[$this->targetLocaleName][$hash])) {
330
            $args[0] = $this->strings[$this->targetLocaleName][$hash];
331
        }
332
333
        $result = call_user_func_array('sprintf', $args);
334
        if ($result === false) {
335
            throw new Localization_Exception(
336
                'Incorrectly translated string or erroneous localized string',
337
                sprintf(
338
                    'The string %1$s seems to have too many or too few arguments.',
339
                    $text
340
                )
341
            );
342
        }
343
344
        return $result;
345
    }
346
347
    /**
348
     * Checks if the specified string hash exists.
349
     * @param string $hash
350
     * @return boolean
351
     */
352
    public function hashExists($hash)
353
    {
354
        return array_key_exists($hash, $this->strings[$this->targetLocale->getName()]);
355
    }
356
357
    /**
358
     * Checks if a translation for the specified string hash exists.
359
     * @param string $text
360
     * @return boolean
361
     */
362
    public function translationExists($text)
363
    {
364
        return array_key_exists(md5($text), $this->strings[$this->targetLocale->getName()]);
365
    }
366
367
    /**
368
     * Retrieves the locale into which texts are currently translated.
369
     * @return Localization_Locale
370
     */
371
    public function getTargetLocale()
372
    {
373
        return $this->targetLocale;
374
    }
375
376
    /**
377
     * Retrieves the translation for the specified string hash.
378
     * @param string $hash
379
     * @return string|NULL
380
     */
381
    public function getHashTranslation($hash, Localization_Locale $locale=null)
382
    {
383
        if(!$locale) {
384
            $locale = $this->targetLocale;
385
        }
386
        
387
        $localeName = $locale->getName();
388
        
389
        if(isset($this->strings[$localeName]) && isset($this->strings[$localeName][$hash])) {
390
            return $this->strings[$localeName][$hash];
391
        }
392
393
        return null;
394
    }
395
396
   /**
397
    * Retrieves only the strings that are available clientside.
398
    * 
399
    * @param Localization_Locale $locale
400
    * @return array
401
    */
402
    public function getClientStrings(Localization_Locale $locale)
403
    {
404
        $result = array();
405
        
406
        foreach($this->sources as $source) 
407
        {
408
            $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

408
            /** @scrutinizer ignore-call */ 
409
            $localeFile = self::getClientStorageFile($locale, $source);
Loading history...
409
            if(!file_exists($localeFile)) {
410
                continue;
411
            }
412
            
413
            $strings = parse_ini_file($localeFile);
414
            
415
            $result = array_merge($result, $strings);
0 ignored issues
show
Bug introduced by
It seems like $strings can also be of type false; however, parameter $array2 of array_merge() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

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

415
            $result = array_merge($result, /** @scrutinizer ignore-type */ $strings);
Loading history...
416
        }
417
        
418
        return $result;
419
    }
420
}