Completed
Push — master ( 7082f0...55a2b2 )
by Siad
15:25
created

TranslateGettext::initialize()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 16
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 13
nc 6
nop 0
dl 0
loc 16
ccs 0
cts 11
cp 0
crap 42
rs 9.2222
c 0
b 0
f 0
1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
/**
21
 * Replaces gettext("message id") and _("message id") with the translated string.
22
 *
23
 * Gettext is great for creating multi-lingual sites, but in some cases (e.g. for
24
 * performance reasons) you may wish to replace the gettext calls with the translations
25
 * of the strings; that's what this task is for.  Note that this is similar to
26
 * ReplaceTokens, but both the find and the replace aspect is more complicated -- hence
27
 * this is a separate, stand-alone filter.
28
 *
29
 * <p>
30
 * Example:<br>
31
 * <pre>
32
 * <translategettext locale="en_US" domain="messages" dir="${webroot}/local"/>
33
 * </pre>
34
 *
35
 * @author  Hans Lellelid <[email protected]>
36
 * @see     BaseFilterReader
37
 * @package phing.filters
38
 */
39
class TranslateGettext extends BaseParamFilterReader implements ChainableReader
40
{
41
42
    // constants for specifying keys to expect
43
    // when this is called using <filterreader ... />
44
    const DOMAIN_KEY = "domain";
45
    const DIR_KEY = "dir";
46
    const LOCALE_KEY = "locale";
47
48
    /**
49
     * The domain to use
50
     */
51
    private $domain = 'messages';
52
53
    /**
54
     * The dir containing LC_MESSAGES
55
     */
56
    private $dir;
57
58
    /**
59
     * The locale to use
60
     */
61
    private $locale;
62
63
    /**
64
     * The system locale before it was changed for this filter.
65
     */
66
    private $storedLocale;
67
68
    /**
69
     * Set the text domain to use.
70
     * The text domain must correspond to the name of the compiled .mo files.
71
     * E.g. "messages" ==> $dir/LC_MESSAGES/messages.mo
72
     *         "mydomain" ==> $dir/LC_MESSAGES/mydomain.mo
73
     *
74
     * @param string $domain
75
     */
76
    public function setDomain($domain)
77
    {
78
        $this->domain = $domain;
79
    }
80
81
    /**
82
     * Get the current domain.
83
     *
84
     * @return string
85
     */
86
    public function getDomain()
87
    {
88
        return $this->domain;
89
    }
90
91
    /**
92
     * Sets the root locale directory.
93
     *
94
     * @param PhingFile $dir
95
     */
96
    public function setDir(PhingFile $dir)
97
    {
98
        $this->dir = $dir;
99
    }
100
101
    /**
102
     * Gets the root locale directory.
103
     *
104
     * @return PhingFile
105
     */
106
    public function getDir()
107
    {
108
        return $this->dir;
109
    }
110
111
    /**
112
     * Sets the locale to use for translation.
113
     * Note that for gettext() to work, you have to make sure this locale
114
     * is specific enough for your system (e.g. some systems may allow an 'en' locale,
115
     * but others will require 'en_US', etc.).
116
     *
117
     * @param string $locale
118
     */
119
    public function setLocale($locale)
120
    {
121
        $this->locale = $locale;
122
    }
123
124
    /**
125
     * Gets the locale to use for translation.
126
     *
127
     * @return string
128
     */
129
    public function getLocale()
130
    {
131
        return $this->locale;
132
    }
133
134
    /**
135
     * Make sure that required attributes are set.
136
     *
137
     * @throws BuldException - if any required attribs aren't set.
138
     */
139
    protected function checkAttributes()
140
    {
141
        if (!$this->domain || !$this->locale || !$this->dir) {
142
            throw new BuildException("You must specify values for domain, locale, and dir attributes.");
143
        }
144
    }
145
146
    /**
147
     * Initialize the gettext/locale environment.
148
     * This method will change some env vars and locale settings; the
149
     * restoreEnvironment should put them all back :)
150
     *
151
     * @return void
152
     * @throws BuildException - if locale cannot be set.
153
     * @see    restoreEnvironment()
154
     */
155
    protected function initEnvironment()
156
    {
157
        $this->storedLocale = getenv("LANG");
158
159
        $this->log("Setting locale to " . $this->locale, Project::MSG_DEBUG);
160
        putenv("LANG=" . $this->locale);
161
        $ret = setlocale(LC_ALL, $this->locale);
162
        if ($ret === false) {
163
            $msg = "Could not set locale to " . $this->locale
164
                . ". You may need to use fully qualified name"
165
                . " (e.g. en_US instead of en).";
166
            throw new BuildException($msg);
167
        }
168
169
        $this->log("Binding domain '" . $this->domain . "' to " . $this->dir, Project::MSG_DEBUG);
170
        bindtextdomain($this->domain, $this->dir->getAbsolutePath());
171
        textdomain($this->domain);
172
    }
173
174
    /**
175
     * Restores environment settings and locale.
176
     * This does _not_ restore any gettext-specific settings
177
     * (e.g. textdomain()).
178
     *
179
     * @return void
180
     */
181
    protected function restoreEnvironment()
182
    {
183
        putenv("LANG=" . $this->storedLocale);
184
        setlocale(LC_ALL, $this->storedLocale);
185
    }
186
187
    /**
188
     * Performs gettext translation of msgid and returns translated text.
189
     *
190
     * This function simply wraps gettext() call, but provides ability to log
191
     * string replacements.  (alternative would be using preg_replace with /e which
192
     * would probably be faster, but no ability to debug/log.)
193
     *
194
     * @param  array $matches Array of matches; we're interested in $matches[2].
195
     * @return string Translated text
196
     */
197
    private function xlateStringCallback($matches)
198
    {
199
        $charbefore = $matches[1];
200
        $msgid = $matches[2];
201
        $translated = gettext($msgid);
202
        $this->log("Translating \"$msgid\" => \"$translated\"", Project::MSG_DEBUG);
203
204
        return $charbefore . '"' . $translated . '"';
205
    }
206
207
    /**
208
     * Returns the filtered stream.
209
     * The original stream is first read in fully, and then translation is performed.
210
     *
211
     * @param  int $len
212
     * @throws BuildException
213
     * @return mixed the filtered stream, or -1 if the end of the resulting stream has been reached.
214
     */
215
    public function read($len = null)
216
    {
217
        if (!$this->getInitialized()) {
218
            $this->initialize();
219
            $this->setInitialized(true);
220
        }
221
222
        // Make sure correct params/attribs have been set
223
        $this->checkAttributes();
224
225
        $buffer = $this->in->read($len);
226
        if ($buffer === -1) {
227
            return -1;
228
        }
229
230
        // Setup the locale/gettext environment
231
        $this->initEnvironment();
232
233
234
        // replace any occurrences of _("") or gettext("") with
235
        // the translated value.
236
        //
237
        // ([^\w]|^)_\("((\\"|[^"])*)"\)
238
        //  --$1---      -----$2----
239
        //                 ---$3--  [match escaped quotes or any char that's not a quote]
240
        //
241
        // also match gettext() -- same as above
242
243
        $buffer = preg_replace_callback(
244
            '/(\W|^)_\("((\\\"|[^"])*)"\)/',
245
            [$this, 'xlateStringCallback'],
246
            $buffer
247
        );
248
        $buffer = preg_replace_callback(
249
            '/(\W|^)gettext\("((\\\"|[^"])*)"\)/',
250
            [$this, 'xlateStringCallback'],
251
            $buffer
252
        );
253
254
        // Check to see if there are any _('') calls and flag an error
255
256
        // Check to see if there are any unmatched gettext() calls -- and flag an error
257
258
        $matches = [];
259
        if (preg_match('/(\W|^)(gettext\([^\)]+\))/', $buffer, $matches)) {
260
            $this->log("Unable to perform translation on: " . $matches[2], Project::MSG_WARN);
261
        }
262
263
        $this->restoreEnvironment();
264
265
        return $buffer;
266
    }
267
268
    /**
269
     * Creates a new TranslateGettext filter using the passed in
270
     * Reader for instantiation.
271
     *
272
     * @param Reader $reader A Reader object providing the underlying stream.
273
     *                       Must not be <code>null</code>.
274
     *
275
     * @return TranslateGettext A new filter based on this configuration, but filtering
276
     *                          the specified reader
277
     */
278
    public function chain(Reader $reader): Reader
279
    {
280
        $newFilter = new TranslateGettext($reader);
281
        $newFilter->setProject($this->getProject());
282
        $newFilter->setDomain($this->getDomain());
283
        $newFilter->setLocale($this->getLocale());
284
        $newFilter->setDir($this->getDir());
285
286
        return $newFilter;
287
    }
288
289
    /**
290
     * Parses the parameters if this filter is being used in "generic" mode.
291
     */
292
    private function initialize()
293
    {
294
        $params = $this->getParameters();
295
        if ($params !== null) {
0 ignored issues
show
introduced by
The condition $params !== null is always true.
Loading history...
296
            foreach ($params as $param) {
297
                switch ($param->getType()) {
298
                    case self::DOMAIN_KEY:
299
                        $this->setDomain($param->getValue());
300
                        break;
301
                    case self::DIR_KEY:
302
                        $this->setDir($this->project->resolveFile($param->getValue()));
303
                        break;
304
305
                    case self::LOCALE_KEY:
306
                        $this->setLocale($param->getValue());
307
                        break;
308
                } // switch
309
            }
310
        } // if params !== null
311
    }
312
}
313