Translator::dngettext()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 4
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Gettext;
4
5
use Gettext\Generators\PhpArray;
6
7
class Translator extends BaseTranslator implements TranslatorInterface
8
{
9
    private $domain;
10
    private $dictionary = [];
11
    private $plurals = [];
12
13
    /**
14
     * Loads translation from a Translations instance, a file on an array.
15
     *
16
     * @param Translations|string|array $translations
17
     *
18
     * @return self
19
     */
20
    public function loadTranslations($translations)
21
    {
22
        if (is_object($translations) && $translations instanceof Translations) {
23
            $translations = PhpArray::generate($translations);
24
        } elseif (is_string($translations) && is_file($translations)) {
25
            $translations = include $translations;
26
        } elseif (!is_array($translations)) {
27
            throw new \InvalidArgumentException(
28
                'Invalid Translator: only arrays, files or instance of Translations are allowed'
29
            );
30
        }
31
32
        $this->addTranslations($translations);
33
34
        return $this;
35
    }
36
37
    /**
38
     * Set the default domain.
39
     *
40
     * @param string $domain
41
     *
42
     * @return self
43
     */
44
    public function defaultDomain($domain)
45
    {
46
        $this->domain = $domain;
47
48
        return $this;
49
    }
50
51
    /**
52
     * @see TranslatorInterface
53
     *
54
     * {@inheritdoc}
55
     */
56
    public function gettext($original)
57
    {
58
        return $this->dpgettext($this->domain, null, $original);
59
    }
60
61
    /**
62
     * @see TranslatorInterface
63
     *
64
     * {@inheritdoc}
65
     */
66
    public function ngettext($original, $plural, $value)
67
    {
68
        return $this->dnpgettext($this->domain, null, $original, $plural, $value);
69
    }
70
71
    /**
72
     * @see TranslatorInterface
73
     *
74
     * {@inheritdoc}
75
     */
76
    public function dngettext($domain, $original, $plural, $value)
77
    {
78
        return $this->dnpgettext($domain, null, $original, $plural, $value);
79
    }
80
81
    /**
82
     * @see TranslatorInterface
83
     *
84
     * {@inheritdoc}
85
     */
86
    public function npgettext($context, $original, $plural, $value)
87
    {
88
        return $this->dnpgettext($this->domain, $context, $original, $plural, $value);
89
    }
90
91
    /**
92
     * @see TranslatorInterface
93
     *
94
     * {@inheritdoc}
95
     */
96
    public function pgettext($context, $original)
97
    {
98
        return $this->dpgettext($this->domain, $context, $original);
99
    }
100
101
    /**
102
     * @see TranslatorInterface
103
     *
104
     * {@inheritdoc}
105
     */
106
    public function dgettext($domain, $original)
107
    {
108
        return $this->dpgettext($domain, null, $original);
109
    }
110
111
    /**
112
     * @see TranslatorInterface
113
     *
114
     * {@inheritdoc}
115
     */
116
    public function dpgettext($domain, $context, $original)
117
    {
118
        $translation = $this->getTranslation($domain, $context, $original);
119
120
        if (isset($translation[0]) && $translation[0] !== '') {
121
            return $translation[0];
122
        }
123
124
        return $original;
125
    }
126
127
    /**
128
     * @see TranslatorInterface
129
     *
130
     * {@inheritdoc}
131
     */
132
    public function dnpgettext($domain, $context, $original, $plural, $value)
133
    {
134
        $translation = $this->getTranslation($domain, $context, $original);
135
        $key = $this->getPluralIndex($domain, $value, $translation === false);
136
137
        if (isset($translation[$key]) && $translation[$key] !== '') {
138
            return $translation[$key];
139
        }
140
141
        return ($key === 0) ? $original : $plural;
142
    }
143
144
    /**
145
     * Set new translations to the dictionary.
146
     *
147
     * @param array $translations
148
     */
149
    protected function addTranslations(array $translations)
150
    {
151
        $domain = isset($translations['domain']) ? $translations['domain'] : '';
152
153
        //Set the first domain loaded as default domain
154
        if ($this->domain === null) {
155
            $this->domain = $domain;
156
        }
157
158
        if (isset($this->dictionary[$domain])) {
159
            $this->dictionary[$domain] = array_replace_recursive($this->dictionary[$domain], $translations['messages']);
160
161
            return;
162
        }
163
164
        if (!empty($translations['plural-forms'])) {
165
            list($count, $code) = array_map('trim', explode(';', $translations['plural-forms'], 2));
166
167
            // extract just the expression turn 'n' into a php variable '$n'.
168
            // Slap on a return keyword and semicolon at the end.
169
            $this->plurals[$domain] = [
170
                'count' => (int) str_replace('nplurals=', '', $count),
171
                'code' => str_replace('plural=', 'return ', str_replace('n', '$n', $code)).';',
172
            ];
173
        }
174
175
        $this->dictionary[$domain] = $translations['messages'];
176
    }
177
178
    /**
179
     * Search and returns a translation.
180
     *
181
     * @param string $domain
182
     * @param string $context
183
     * @param string $original
184
     *
185
     * @return string|false
186
     */
187
    protected function getTranslation($domain, $context, $original)
188
    {
189
        return isset($this->dictionary[$domain][$context][$original])
190
             ? $this->dictionary[$domain][$context][$original]
191
             : false;
192
    }
193
194
    /**
195
     * Executes the plural decision code given the number to decide which
196
     * plural version to take.
197
     *
198
     * @param string $domain
199
     * @param string $n
200
     * @param bool   $fallback set to true to get fallback plural index
201
     *
202
     * @return int
203
     */
204
    protected function getPluralIndex($domain, $n, $fallback)
205
    {
206
        //Not loaded domain or translation, use a fallback
207
        if (!isset($this->plurals[$domain]) || $fallback === true) {
208
            return $n == 1 ? 0 : 1;
209
        }
210
211
        if (!isset($this->plurals[$domain]['function'])) {
212
            $code = self::fixTerseIfs($this->plurals[$domain]['code']);
213
            $this->plurals[$domain]['function'] = eval("return function (\$n) { $code };");
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
214
        }
215
216
        if ($this->plurals[$domain]['count'] <= 2) {
217
            return call_user_func($this->plurals[$domain]['function'], $n) ? 1 : 0;
218
        }
219
220
        return call_user_func($this->plurals[$domain]['function'], $n);
221
    }
222
223
    /**
224
     * This function will recursively wrap failure states in brackets if they contain a nested terse if.
225
     *
226
     * This because PHP can not handle nested terse if's unless they are wrapped in brackets.
227
     *
228
     * This code probably only works for the gettext plural decision codes.
229
     *
230
     * return ($n==1 ? 0 : $n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2);
231
     * becomes
232
     * return ($n==1 ? 0 : ($n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2));
233
     *
234
     * @param string $code  the terse if string
235
     * @param bool   $inner If inner is true we wrap it in brackets
236
     *
237
     * @return string A formatted terse If that PHP can work with.
238
     */
239
    private static function fixTerseIfs($code, $inner = false)
240
    {
241
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
242
         * (?P<expression>[^?]+)   Capture everything up to ? as 'expression'
243
         * \?                      ?
244
         * (?P<success>[^:]+)      Capture everything up to : as 'success'
245
         * :                       :
246
         * (?P<failure>[^;]+)      Capture everything up to ; as 'failure'
247
         */
248
        preg_match('/(?P<expression>[^?]+)\?(?P<success>[^:]+):(?P<failure>[^;]+)/', $code, $matches);
249
250
        // If no match was found then no terse if was present
251
        if (!isset($matches[0])) {
252
            return $code;
253
        }
254
255
        $expression = $matches['expression'];
256
        $success = $matches['success'];
257
        $failure = $matches['failure'];
258
259
        // Go look for another terse if in the failure state.
260
        $failure = self::fixTerseIfs($failure, true);
261
        $code = $expression.' ? '.$success.' : '.$failure;
262
263
        if ($inner) {
264
            return "($code)";
265
        }
266
267
        // note the semicolon. We need that for executing the code.
268
        return "$code;";
269
    }
270
}
271