Passed
Push — master ( be6fbd...8a140a )
by Andreas
24:16
created

midcom_services_i18n_l10n   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 306
Duplicated Lines 0 %

Test Coverage

Coverage 88.29%

Importance

Changes 0
Metric Value
eloc 104
dl 0
loc 306
ccs 98
cts 111
cp 0.8829
rs 9.2
c 0
b 0
f 0
wmc 40

12 Methods

Rating   Name   Duplication   Size   Complexity  
A set_fallback_language() 0 3 1
A _load_language() 0 29 6
A error() 0 4 1
C parse_data() 0 78 17
A __construct() 0 15 2
A get_formatter() 0 3 1
A _check_for_language() 0 4 2
A string_exists() 0 9 2
A set_language() 0 3 1
A get() 0 17 4
A show() 0 3 1
A string_available() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like midcom_services_i18n_l10n 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 midcom_services_i18n_l10n, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package midcom.services
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * This is the L10n main interface class, used by the components. It
11
 * allows you to get entries from the l10n string tables in the current
12
 * language with an automatic conversion to the destination character
13
 * set.
14
 *
15
 * <b>L10n language database file format specification:</b>
16
 *
17
 * Lines starting with --- are considered command lines and treated specially,
18
 * unless they occur within string data. All commands are separated with at
19
 * least a single space from their content, unless they don't have an argument.
20
 *
21
 * Empty lines are ignored, unless within string data.
22
 *
23
 * All keys and values will be trim'ed when encountered, so leading and trailing
24
 * whitespace will be eliminated completely.
25
 *
26
 * Windows-style line endings (\r\n) will be silently converted to the UNIX
27
 * \n style.
28
 *
29
 * Commented example:
30
 *
31
 * <pre>
32
 * ---# Lines starting with a # command are ignored.
33
 *
34
 * ---# File format version
35
 * ---VERSION 2.1.0
36
 *
37
 * ---# Language of the table
38
 * ---LANGUAGE en
39
 *
40
 * ---STRING string identifier
41
 * TRANSLATED STRING taken literally until ---STRINGEND, which is the
42
 * only reserved value at the beginning of the line, everything else is
43
 * fine. Linebreaks within the translation are preserved.
44
 * \r\n sequences are translated into \n
45
 * ---STRINGEND
46
 * </pre>
47
 *
48
 * File naming scheme: {$component_directory}/locale/default.{$lang}.txt
49
 *
50
 * @package midcom.services
51
 */
52
class midcom_services_i18n_l10n
53
{
54
    /**
55
     * The name of the locale library we use, this is usually
56
     * a component's name.
57
     *
58
     * @var string
59
     */
60
    private $_library;
61
62
    /**
63
     * The full path basename to the active library files. The individual
64
     * files are ending with .$lang.txt.
65
     *
66
     * @var string
67
     */
68
    private $_library_filename;
69
70
    /**
71
     * Fallback language, in case the selected language is not available.
72
     *
73
     * @var string
74
     */
75
    private $_fallback_language;
76
77
    /**
78
     * Current language.
79
     *
80
     * @var string
81
     */
82
    private $_language;
83
84
    /**
85
     * Global string table cache, it stores the string tables
86
     * loaded during runtime.
87
     *
88
     * @var array
89
     */
90
    private static $_localedb = [];
91
92
    /**
93
     * The string database, a reference into the global cache.
94
     *
95
     * @var array
96
     */
97
    private $_stringdb;
98
99
    /**
100
     * The current L10n DB file format number
101
     *
102
     * @var string
103
     */
104
    private $_version = '2.1.0';
105
106
    /**
107
     * The constructor loads the translation library indicated by the snippetdir
108
     * path $component and initializes the system completely. The output character
109
     * set will be initialized to the language's default.
110
     */
111 8
    public function __construct(string $component)
112
    {
113 8
        $path = midcom::get()->componentloader->path_to_snippetpath($component) . "/locale/default";
114 8
        $this->_library_filename = $path;
115 8
        $this->_library = $component;
116
117 8
        $this->_fallback_language = midcom::get()->i18n->get_fallback_language();
118
119 8
        if (!isset(self::$_localedb[$this->_library])) {
120 5
            self::$_localedb[$this->_library] = [];
121
        }
122
123 8
        $this->_stringdb =& self::$_localedb[$this->_library];
124
125 8
        $this->set_language(midcom::get()->i18n->get_current_language());
126 8
    }
127
128
    /**
129
     * Load a language database
130
     *
131
     * - Leading and trailing whitespace will be eliminated
132
     */
133 7
    private function _load_language(string $lang)
134
    {
135 7
        $this->_stringdb[$lang] = [];
136 7
        $filename = "{$this->_library_filename}.{$lang}.txt";
137
138 7
        if (midcom::get()->config->get('cache_module_memcache_backend') != 'flatfile') {
139 7
            $stringtable = midcom::get()->cache->memcache->get('L10N', $filename);
0 ignored issues
show
Bug introduced by
The property memcache does not seem to exist on midcom_services_cache.
Loading history...
140 7
            if (is_array($stringtable)) {
141
                $this->_stringdb[$lang] = $stringtable;
142
                return;
143
            }
144
        }
145
146 7
        if (!file_exists($filename)) {
147 2
            return;
148
        }
149
150 6
        $data = $this->parse_data(file($filename), $lang, $filename);
151
152
        // get site-specific l10n
153 6
        $component_locale = midcom_helper_misc::get_snippet_content_graceful("conf:/" . $this->_library . '/l10n/default.' . $lang . '.txt');
154 6
        if (!empty($component_locale)) {
155
            $data = array_merge($data, $this->parse_data(explode("\n", $component_locale), $lang, $component_locale));
156
        }
157
158 6
        $this->_stringdb[$lang] = array_merge($this->_stringdb[$lang], $data);
159
160 6
        if (midcom::get()->config->get('cache_module_memcache_backend') != 'flatfile') {
161 6
            midcom::get()->cache->memcache->put('L10N', $filename, $this->_stringdb[$lang]);
162
        }
163 6
    }
164
165 6
    private function parse_data(array $data, string $lang, string $filename) : array
166
    {
167 6
        $stringtable = [];
168 6
        $version = '';
169 6
        $language = '';
170 6
        $instring = false;
171 6
        $string_data = '';
172 6
        $string_key = '';
173
174 6
        foreach ($data as $line => $string) {
175
            // Kill any excess whitespace first.
176 6
            $string = trim($string);
177
178 6
            if (!$instring) {
179
                // outside of a string value
180 6
                if ($string == '') {
181 6
                    continue;
182
                }
183 6
                if (!str_starts_with($string, '---')) {
184
                    throw $this->error("Invalid line", $filename, $line);
185
                }
186
                // this is a command
187 6
                if (strlen($string) < 4) {
188
                    throw $this->error("An incorrect command was detected", $filename, $line);
189
                }
190
191 6
                $command = preg_replace('/^---(.+?) .+/', '$1', $string);
192
193 6
                switch ($command) {
194 6
                    case '#':
195
                        // Skip
196 6
                        break;
197
198 6
                    case 'VERSION':
199 6
                        if ($version != '') {
200
                            throw $this->error("A second VERSION tag has been detected", $filename, $line);
201
                        }
202 6
                        $version = substr($string, 11);
203 6
                        break;
204
205 6
                    case 'LANGUAGE':
206 6
                        if ($language != '') {
207
                            throw $this->error("A second LANGUAGE tag has been detected", $filename, $line);
208
                        }
209 6
                        $language = substr($string, 12);
210 6
                        break;
211
212 6
                    case 'STRING':
213 6
                        $string_data = '';
214 6
                        $string_key = substr($string, 10);
215 6
                        $instring = true;
216 6
                        break;
217
218
                    default:
219 6
                        throw $this->error("Unknown command '{$command}'", $filename, $line);
220
                }
221 6
            } elseif ($string == '---STRINGEND') {
222 6
                $instring = false;
223 6
                $stringtable[$string_key] = $string_data;
224 6
            } elseif ($string_data == '') {
225 6
                $string_data .= $string;
226
            } else {
227 1
                $string_data .= "\n{$string}";
228
            }
229
        }
230
231 6
        if ($instring) {
232
            throw new midcom_error("L10n DB SYNTAX ERROR: String constant exceeds end of file.");
233
        }
234 6
        if (version_compare($version, $this->_version, "<")) {
235
            throw new midcom_error("L10n DB ERROR: File format version of {$filename} is too old, no update available at the moment.");
236
        }
237 6
        if ($lang != $language) {
238
            throw new midcom_error("L10n DB ERROR: The DB language version {$language} did not match the requested {$lang}.");
239
        }
240
241 6
        ksort($stringtable, SORT_STRING);
242 6
        return $stringtable;
243
    }
244
245
    private function error(string $message, string $filename, int $line) : midcom_error
246
    {
247
        $line++; // Array is 0-indexed
248
        return new midcom_error('L10n DB SYNTAX ERROR: ' .  $message . ' at ' . $filename . ' ' . $line);
249
    }
250
251
    /**
252
     * Checks, whether the referenced language is already loaded. If not,
253
     * it is automatically made available.
254
     */
255 383
    private function _check_for_language(string $lang)
256
    {
257 383
        if (!array_key_exists($lang, $this->_stringdb)) {
258 7
            $this->_load_language($lang);
259
        }
260 383
    }
261
262
    /**
263
     * Set output language.
264
     *
265
     * This is usually set through midcom_services_i18n.
266
     */
267 8
    public function set_language(string $lang)
268
    {
269 8
        $this->_language = $lang;
270 8
    }
271
272
    /**
273
     * Set the fallback language.
274
     *
275
     * This is usually set through midcom_services_i18n.
276
     */
277 6
    public function set_fallback_language(string $lang)
278
    {
279 6
        $this->_fallback_language = $lang;
280 6
    }
281
282 57
    public function get_formatter() : midcom_services_i18n_formatter
283
    {
284 57
        return new midcom_services_i18n_formatter($this->_language);
285
    }
286
287
    /**
288
     * Checks if a localized string for $string exists. If $language is unset,
289
     * the current language is used.
290
     *
291
     * @param string $language The language to search in.
292
     */
293 383
    function string_exists(string $string, $language = null) : bool
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
294
    {
295 383
        if ($language === null) {
296 1
            $language = $this->_language;
297
        }
298
299 383
        $this->_check_for_language($language);
300
301 383
        return isset($this->_stringdb[$language][$string]);
302
    }
303
304
    /**
305
     * Checks whether the given string is available in either the current
306
     * or the fallback language. Use this to determine if an actually processed
307
     * result is returned by get. This is helpful especially if you want to
308
     * "catch" cases where a string might translate to itself in some languages.
309
     */
310 128
    function string_available(string $string)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
311
    {
312 128
        return $this->string_exists($string, $this->_language)
313 128
            || $this->string_exists($string, $this->_fallback_language);
314
    }
315
316
    /**
317
     * Retrieves a localized string from the database using $language as
318
     * destination. If $language is unset, the currently set default language is
319
     * used. If the string is not found in the selected language, the fallback
320
     * is checked. If even the fallback cannot be found, then $string is
321
     * returned and the event is logged to MidCOMs Debugging system.
322
     *
323
     * L10n DB loads are done through string_exists.
324
     *
325
     * @param string $language The language to search in, uses the current language as default.
326
     */
327 381
    public function get(string $string, $language = null) : string
328
    {
329 381
        if ($language === null) {
330 381
            $language = $this->_language;
331
        }
332
333 381
        if (!$this->string_exists($string, $language)) {
334
            // Go for Fallback
335 166
            $language = $this->_fallback_language;
336
337 166
            if (!$this->string_exists($string, $language)) {
338
                // Nothing found, log is produced by string_exists.
339 165
                return $string;
340
            }
341
        }
342
343 375
        return midcom::get()->i18n->convert_from_utf8($this->_stringdb[$language][$string]);
344
    }
345
346
    /**
347
     * This is a shortcut for "echo $this->get(...);", useful in style code.
348
     *
349
     * Note, that due to the stupidity of the Zend engine, it is not possible to call
350
     * this function echo, like it should have been called.
351
     *
352
     * @param string $language The language to search in, uses the current language as default.
353
     * @see get()
354
     */
355 14
    public function show(string $string, $language = null)
356
    {
357 14
        echo $this->get($string, $language);
358 14
    }
359
}
360