Passed
Branch master (05c266)
by Andreas
09:55
created

midcom_services_i18n_l10n::set_fallback_language()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
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 \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
     * The string database
86
     *
87
     * @var array
88
     */
89
    private $_stringdb = [];
90
91
    /**
92
     * The current L10n DB file format number
93
     *
94
     * @var string
95
     */
96
    private $_version = '2.1.0';
97
98
    /**
99
     * The constructor loads the translation library indicated by the snippetdir
100
     * path $component and initializes the system completely. The output character
101
     * set will be initialized to the language's default.
102
     */
103 8
    public function __construct(string $component, string $language, string $fallback_language)
104
    {
105 8
        $this->_library = $component;
106 8
        $this->_fallback_language = $fallback_language;
107 8
        $this->_language = $language;
108 8
        $this->_library_filename = midcom::get()->componentloader->path_to_snippetpath($component) . "/locale/default";
109
    }
110
111
    /**
112
     * Load a language database
113
     *
114
     * - Leading and trailing whitespace will be eliminated
115
     */
116 8
    private function _load_language(string $lang) : array
117
    {
118 8
        $filename = "{$this->_library_filename}.{$lang}.txt";
119 8
        $identifier = str_replace('/', '-', $filename);
120
121 8
        if (midcom::get()->config->get('cache_module_memcache_backend') != 'flatfile') {
122 8
            $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...
123 8
            if (is_array($stringtable)) {
124 3
                return $stringtable;
125
            }
126
        }
127
128 8
        if (!file_exists($filename)) {
129 3
            return [];
130
        }
131
132 6
        $data = $this->parse_data(file($filename), $lang, $filename);
133
134
        // get site-specific l10n
135 6
        $snippet_path = "conf:/" . $this->_library . '/l10n/default.' . $lang . '.txt';
136 6
        if ($snippet_data = midcom_helper_misc::get_snippet_content_graceful($snippet_path)) {
137
            $data = array_merge($data, $this->parse_data(explode("\n", $snippet_data), $lang, $snippet_path));
138
        }
139
140 6
        if (midcom::get()->config->get('cache_module_memcache_backend') != 'flatfile') {
141 6
            midcom::get()->cache->memcache->put('L10N', $identifier, $data);
142
        }
143 6
        return $data;
144
    }
145
146 6
    private function parse_data(array $data, string $lang, string $filename) : array
147
    {
148 6
        $stringtable = [];
149 6
        $version = '';
150 6
        $language = '';
151 6
        $instring = false;
152 6
        $string_data = '';
153 6
        $string_key = '';
154
155 6
        foreach ($data as $line => $string) {
156
            // Kill any excess whitespace first.
157 6
            $string = trim($string);
158
159 6
            if (!$instring) {
160
                // outside of a string value
161 6
                if ($string == '') {
162 6
                    continue;
163
                }
164 6
                if (!str_starts_with($string, '---')) {
165
                    throw $this->error("Invalid line", $filename, $line);
166
                }
167
                // this is a command
168 6
                if (strlen($string) < 4) {
169
                    throw $this->error("An incorrect command was detected", $filename, $line);
170
                }
171
172 6
                $command = preg_replace('/^---(.+?) .+/', '$1', $string);
173
174 6
                switch ($command) {
175 6
                    case '#':
176
                        // Skip
177 6
                        break;
178
179 6
                    case 'VERSION':
180 6
                        if ($version != '') {
181
                            throw $this->error("A second VERSION tag has been detected", $filename, $line);
182
                        }
183 6
                        $version = substr($string, 11);
184 6
                        break;
185
186 6
                    case 'LANGUAGE':
187 6
                        if ($language != '') {
188
                            throw $this->error("A second LANGUAGE tag has been detected", $filename, $line);
189
                        }
190 6
                        $language = substr($string, 12);
191 6
                        break;
192
193 6
                    case 'STRING':
194 6
                        $string_data = '';
195 6
                        $string_key = substr($string, 10);
196 6
                        $instring = true;
197 6
                        break;
198
199
                    default:
200 6
                        throw $this->error("Unknown command '{$command}'", $filename, $line);
201
                }
202 6
            } elseif ($string == '---STRINGEND') {
203 6
                $instring = false;
204 6
                $stringtable[$string_key] = $string_data;
205 6
            } elseif ($string_data == '') {
206 6
                $string_data .= $string;
207
            } else {
208 1
                $string_data .= "\n{$string}";
209
            }
210
        }
211
212 6
        if ($instring) {
213
            throw new midcom_error("L10n DB SYNTAX ERROR: String constant exceeds end of file.");
214
        }
215 6
        if (version_compare($version, $this->_version, "<")) {
216
            throw new midcom_error("L10n DB ERROR: File format version of {$filename} is too old, no update available at the moment.");
217
        }
218 6
        if ($lang != $language) {
219
            throw new midcom_error("L10n DB ERROR: The DB language version {$language} did not match the requested {$lang}.");
220
        }
221
222 6
        ksort($stringtable, SORT_STRING);
223 6
        return $stringtable;
224
    }
225
226
    private function error(string $message, string $filename, int $line) : midcom_error
227
    {
228
        $line++; // Array is 0-indexed
229
        return new midcom_error('L10n DB SYNTAX ERROR: ' .  $message . ' at ' . $filename . ' ' . $line);
230
    }
231
232
    /**
233
     * Checks, whether the referenced language is already loaded. If not,
234
     * it is automatically made available.
235
     */
236 389
    private function _check_for_language(string $lang)
237
    {
238 389
        if (!array_key_exists($lang, $this->_stringdb)) {
239 8
            $this->_stringdb[$lang] = $this->_load_language($lang);
240
        }
241
    }
242
243
    /**
244
     * Set output language.
245
     *
246
     * This is usually set through midcom_services_i18n.
247
     */
248 3
    public function set_language(string $lang)
249
    {
250 3
        $this->_language = $lang;
251
    }
252
253
    /**
254
     * Set the fallback language.
255
     *
256
     * This is usually set through midcom_services_i18n.
257
     */
258
    public function set_fallback_language(string $lang)
259
    {
260
        $this->_fallback_language = $lang;
261
    }
262
263 61
    public function get_formatter() : midcom_services_i18n_formatter
264
    {
265 61
        return new midcom_services_i18n_formatter($this->_language);
266
    }
267
268
    /**
269
     * Checks if a localized string for $string exists. If $language is unset,
270
     * the current language is used.
271
     *
272
     * @param string $language The language to search in.
273
     */
274 389
    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...
275
    {
276 389
        if ($language === null) {
277 1
            $language = $this->_language;
278
        }
279
280 389
        $this->_check_for_language($language);
281
282 389
        return isset($this->_stringdb[$language][$string]);
283
    }
284
285
    /**
286
     * Checks whether the given string is available in either the current
287
     * or the fallback language. Use this to determine if an actually processed
288
     * result is returned by get. This is helpful especially if you want to
289
     * "catch" cases where a string might translate to itself in some languages.
290
     */
291 130
    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...
292
    {
293 130
        return $this->string_exists($string, $this->_language)
294 130
            || $this->string_exists($string, $this->_fallback_language);
295
    }
296
297
    /**
298
     * Retrieves a localized string from the database using $language as
299
     * destination. If $language is unset, the currently set default language is
300
     * used. If the string is not found in the selected language, the fallback
301
     * is checked. If even the fallback cannot be found, then $string is
302
     * returned and the event is logged to MidCOMs Debugging system.
303
     *
304
     * L10n DB loads are done through string_exists.
305
     *
306
     * @param string $language The language to search in, uses the current language as default.
307
     */
308 387
    public function get(string $string, $language = null) : string
309
    {
310 387
        if ($language === null) {
311 387
            $language = $this->_language;
312
        }
313
314 387
        if (!$this->string_exists($string, $language)) {
315
            // Go for Fallback
316 169
            $language = $this->_fallback_language;
317
318 169
            if (!$this->string_exists($string, $language)) {
319
                // Nothing found, log is produced by string_exists.
320 168
                return $string;
321
            }
322
        }
323
324 381
        return midcom::get()->i18n->convert_from_utf8($this->_stringdb[$language][$string]);
325
    }
326
327
    /**
328
     * This is a shortcut for "echo $this->get(...);", useful in style code.
329
     *
330
     * Note, that due to the stupidity of the Zend engine, it is not possible to call
331
     * this function echo, like it should have been called.
332
     *
333
     * @param string $language The language to search in, uses the current language as default.
334
     * @see get()
335
     */
336 14
    public function show(string $string, $language = null)
337
    {
338 14
        echo $this->get($string, $language);
339
    }
340
}
341