Passed
Pull Request — master (#204)
by
unknown
23:41
created

midcom_services_i18n_l10n::string_available()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
ccs 3
cts 3
cp 1
crap 2
rs 10
c 0
b 0
f 0
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 to \n
45
 * ---STRINGEND
46
 * </pre>
47
 *
48
 * File naming scheme: {$component_directory}/locale/{$database_name}.{$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
     * The name of the current component
72
     *
73
     * @var string
74
     */
75
    private $_component_name;
76
77
    /**
78
     * Fallback language, in case the selected language is not available.
79
     *
80
     * @var string
81
     */
82
    private $_fallback_language;
83
84
    /**
85
     * Current language.
86
     *
87
     * @var string
88
     */
89
    private $_language;
90
91
    /**
92
     * Global string table cache, it stores the string tables
93
     * loaded during runtime.
94
     *
95
     * @var Array
96
     */
97
    private static $_localedb = [];
98
99
    /**
100
     * The string database, a reference into the global cache.
101
     *
102
     * @var Array
103
     */
104
    private $_stringdb;
105
106
    /**
107
     * The current L10n DB file format number
108
     *
109
     * @var string
110
     */
111
    private $_version = '2.1.0';
112
113
    private $database;
114
115
    /**
116
     * The constructor loads the translation library indicated by the snippetdir
117
     * path $library and initializes the system completely. The output character
118
     * set will be initialized to the language's default.
119
     *
120
     * @param string $library    Name of the locale library to use.
121
     * @param string $database    Name of the database in the library to load.
122
     */
123 10
    public function __construct($library, $database)
124
    {
125 10
        $path = midcom::get()->componentloader->path_to_snippetpath($library) . "/locale/" . $database;
126 10
        $this->database = $database;
127 10
        $this->_library_filename = $path;
128 10
        $this->_library = $library . $database;
129 10
        $this->_component_name = $library;
130
131 10
        $this->_fallback_language = midcom::get()->i18n->get_fallback_language();
132
133 10
        if (!isset(self::$_localedb[$this->_library])) {
134 7
            self::$_localedb[$this->_library] = [];
135
        }
136
137 10
        $this->_stringdb =& self::$_localedb[$this->_library];
138
139 10
        $this->set_language(midcom::get()->i18n->get_current_language());
140 10
    }
141
142
    /**
143
     * Load a language database
144
     *
145
     * - Leading and trailing whitespace will be eliminated
146
     */
147 10
    private function _load_language(string $lang)
148
    {
149 10
        $this->_stringdb[$lang] = [];
150 10
        $filename = "{$this->_library_filename}.{$lang}.txt";
151
152 10
        if (midcom::get()->config->get('cache_module_memcache_backend') != 'flatfile') {
153 10
            $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...
154 10
            if (is_array($stringtable)) {
155
                $this->_stringdb[$lang] = $stringtable;
156
                return;
157
            }
158
        }
159
160 10
        if (!empty(midcom::get()->componentloader->manifests[$this->_component_name]->extends)) {
161
            $parent_l10n = new self(midcom::get()->componentloader->manifests[$this->_component_name]->extends, $this->database);
162
            $this->_stringdb[$lang] = $parent_l10n->get_stringdb($lang);
163
        }
164
165 10
        if (!file_exists($filename)) {
166 3
            return;
167
        }
168
169 8
        $data = $this->parse_data(file($filename), $lang, $filename);
0 ignored issues
show
Bug introduced by
It seems like file($filename) can also be of type false; however, parameter $data of midcom_services_i18n_l10n::parse_data() does only seem to accept array, 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

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