Passed
Push — master ( 5a0590...0c08dd )
by Andreas
49:13 queued 24:34
created

midcom_services_i18n_l10n::string_exists()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 9
ccs 5
cts 5
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 9
    public function __construct(string $library, string $database)
124
    {
125 9
        $path = midcom::get()->componentloader->path_to_snippetpath($library) . "/locale/" . $database;
126 9
        $this->database = $database;
127 9
        $this->_library_filename = $path;
128 9
        $this->_library = $library . $database;
129 9
        $this->_component_name = $library;
130
131 9
        $this->_fallback_language = midcom::get()->i18n->get_fallback_language();
132
133 9
        if (!isset(self::$_localedb[$this->_library])) {
134 6
            self::$_localedb[$this->_library] = [];
135
        }
136
137 9
        $this->_stringdb =& self::$_localedb[$this->_library];
138
139 9
        $this->set_language(midcom::get()->i18n->get_current_language());
140 9
    }
141
142
    /**
143
     * Load a language database
144
     *
145
     * - Leading and trailing whitespace will be eliminated
146
     */
147 9
    private function _load_language(string $lang)
148
    {
149 9
        $this->_stringdb[$lang] = [];
150 9
        $filename = "{$this->_library_filename}.{$lang}.txt";
151
152 9
        if (midcom::get()->config->get('cache_module_memcache_backend') != 'flatfile') {
153 9
            $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 9
            if (is_array($stringtable)) {
155
                $this->_stringdb[$lang] = $stringtable;
156
                return;
157
            }
158
        }
159
160 9
        if (!file_exists($filename)) {
161 2
            return;
162
        }
163
164 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

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