Passed
Push — master ( 8a140a...6eed6d )
by Andreas
26:05
created

midcom_services_i18n_l10n   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Test Coverage

Coverage 88.39%

Importance

Changes 0
Metric Value
eloc 105
dl 0
loc 307
ccs 99
cts 112
cp 0.8839
rs 9.2
c 0
b 0
f 0
wmc 40

12 Methods

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

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 7
        $identifier = str_replace('/', '-', $filename);
138
139 7
        if (midcom::get()->config->get('cache_module_memcache_backend') != 'flatfile') {
140 7
            $stringtable = midcom::get()->cache->memcache->get('L10N', $identifier);
0 ignored issues
show
Bug introduced by
The property memcache does not seem to exist on midcom_services_cache.
Loading history...
141 7
            if (is_array($stringtable)) {
142
                $this->_stringdb[$lang] = $stringtable;
143
                return;
144
            }
145
        }
146
147 7
        if (!file_exists($filename)) {
148 2
            return;
149
        }
150
151 6
        $data = $this->parse_data(file($filename), $lang, $filename);
152
153
        // get site-specific l10n
154 6
        $component_locale = midcom_helper_misc::get_snippet_content_graceful("conf:/" . $this->_library . '/l10n/default.' . $lang . '.txt');
155 6
        if (!empty($component_locale)) {
156
            $data = array_merge($data, $this->parse_data(explode("\n", $component_locale), $lang, $component_locale));
157
        }
158
159 6
        $this->_stringdb[$lang] = array_merge($this->_stringdb[$lang], $data);
160
161 6
        if (midcom::get()->config->get('cache_module_memcache_backend') != 'flatfile') {
162 6
            midcom::get()->cache->memcache->put('L10N', $identifier, $this->_stringdb[$lang]);
163
        }
164 6
    }
165
166 6
    private function parse_data(array $data, string $lang, string $filename) : array
167
    {
168 6
        $stringtable = [];
169 6
        $version = '';
170 6
        $language = '';
171 6
        $instring = false;
172 6
        $string_data = '';
173 6
        $string_key = '';
174
175 6
        foreach ($data as $line => $string) {
176
            // Kill any excess whitespace first.
177 6
            $string = trim($string);
178
179 6
            if (!$instring) {
180
                // outside of a string value
181 6
                if ($string == '') {
182 6
                    continue;
183
                }
184 6
                if (!str_starts_with($string, '---')) {
185
                    throw $this->error("Invalid line", $filename, $line);
186
                }
187
                // this is a command
188 6
                if (strlen($string) < 4) {
189
                    throw $this->error("An incorrect command was detected", $filename, $line);
190
                }
191
192 6
                $command = preg_replace('/^---(.+?) .+/', '$1', $string);
193
194 6
                switch ($command) {
195 6
                    case '#':
196
                        // Skip
197 6
                        break;
198
199 6
                    case 'VERSION':
200 6
                        if ($version != '') {
201
                            throw $this->error("A second VERSION tag has been detected", $filename, $line);
202
                        }
203 6
                        $version = substr($string, 11);
204 6
                        break;
205
206 6
                    case 'LANGUAGE':
207 6
                        if ($language != '') {
208
                            throw $this->error("A second LANGUAGE tag has been detected", $filename, $line);
209
                        }
210 6
                        $language = substr($string, 12);
211 6
                        break;
212
213 6
                    case 'STRING':
214 6
                        $string_data = '';
215 6
                        $string_key = substr($string, 10);
216 6
                        $instring = true;
217 6
                        break;
218
219
                    default:
220 6
                        throw $this->error("Unknown command '{$command}'", $filename, $line);
221
                }
222 6
            } elseif ($string == '---STRINGEND') {
223 6
                $instring = false;
224 6
                $stringtable[$string_key] = $string_data;
225 6
            } elseif ($string_data == '') {
226 6
                $string_data .= $string;
227
            } else {
228 1
                $string_data .= "\n{$string}";
229
            }
230
        }
231
232 6
        if ($instring) {
233
            throw new midcom_error("L10n DB SYNTAX ERROR: String constant exceeds end of file.");
234
        }
235 6
        if (version_compare($version, $this->_version, "<")) {
236
            throw new midcom_error("L10n DB ERROR: File format version of {$filename} is too old, no update available at the moment.");
237
        }
238 6
        if ($lang != $language) {
239
            throw new midcom_error("L10n DB ERROR: The DB language version {$language} did not match the requested {$lang}.");
240
        }
241
242 6
        ksort($stringtable, SORT_STRING);
243 6
        return $stringtable;
244
    }
245
246
    private function error(string $message, string $filename, int $line) : midcom_error
247
    {
248
        $line++; // Array is 0-indexed
249
        return new midcom_error('L10n DB SYNTAX ERROR: ' .  $message . ' at ' . $filename . ' ' . $line);
250
    }
251
252
    /**
253
     * Checks, whether the referenced language is already loaded. If not,
254
     * it is automatically made available.
255
     */
256 383
    private function _check_for_language(string $lang)
257
    {
258 383
        if (!array_key_exists($lang, $this->_stringdb)) {
259 7
            $this->_load_language($lang);
260
        }
261 383
    }
262
263
    /**
264
     * Set output language.
265
     *
266
     * This is usually set through midcom_services_i18n.
267
     */
268 8
    public function set_language(string $lang)
269
    {
270 8
        $this->_language = $lang;
271 8
    }
272
273
    /**
274
     * Set the fallback language.
275
     *
276
     * This is usually set through midcom_services_i18n.
277
     */
278 6
    public function set_fallback_language(string $lang)
279
    {
280 6
        $this->_fallback_language = $lang;
281 6
    }
282
283 57
    public function get_formatter() : midcom_services_i18n_formatter
284
    {
285 57
        return new midcom_services_i18n_formatter($this->_language);
286
    }
287
288
    /**
289
     * Checks if a localized string for $string exists. If $language is unset,
290
     * the current language is used.
291
     *
292
     * @param string $language The language to search in.
293
     */
294 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...
295
    {
296 383
        if ($language === null) {
297 1
            $language = $this->_language;
298
        }
299
300 383
        $this->_check_for_language($language);
301
302 383
        return isset($this->_stringdb[$language][$string]);
303
    }
304
305
    /**
306
     * Checks whether the given string is available in either the current
307
     * or the fallback language. Use this to determine if an actually processed
308
     * result is returned by get. This is helpful especially if you want to
309
     * "catch" cases where a string might translate to itself in some languages.
310
     */
311 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...
312
    {
313 128
        return $this->string_exists($string, $this->_language)
314 128
            || $this->string_exists($string, $this->_fallback_language);
315
    }
316
317
    /**
318
     * Retrieves a localized string from the database using $language as
319
     * destination. If $language is unset, the currently set default language is
320
     * used. If the string is not found in the selected language, the fallback
321
     * is checked. If even the fallback cannot be found, then $string is
322
     * returned and the event is logged to MidCOMs Debugging system.
323
     *
324
     * L10n DB loads are done through string_exists.
325
     *
326
     * @param string $language The language to search in, uses the current language as default.
327
     */
328 381
    public function get(string $string, $language = null) : string
329
    {
330 381
        if ($language === null) {
331 381
            $language = $this->_language;
332
        }
333
334 381
        if (!$this->string_exists($string, $language)) {
335
            // Go for Fallback
336 166
            $language = $this->_fallback_language;
337
338 166
            if (!$this->string_exists($string, $language)) {
339
                // Nothing found, log is produced by string_exists.
340 165
                return $string;
341
            }
342
        }
343
344 375
        return midcom::get()->i18n->convert_from_utf8($this->_stringdb[$language][$string]);
345
    }
346
347
    /**
348
     * This is a shortcut for "echo $this->get(...);", useful in style code.
349
     *
350
     * Note, that due to the stupidity of the Zend engine, it is not possible to call
351
     * this function echo, like it should have been called.
352
     *
353
     * @param string $language The language to search in, uses the current language as default.
354
     * @see get()
355
     */
356 14
    public function show(string $string, $language = null)
357
    {
358 14
        echo $this->get($string, $language);
359 14
    }
360
}
361