0.3 First Public Versiob
[smail.git/.git] / i18n.class.php
1 <?php
2
3 /*
4  * Fork this project on GitHub!
5  * https://github.com/Philipp15b/php-i18n
6  *
7  * License: MIT
8  */
9
10 class i18n {
11
12     /**
13      * Language file path
14      * This is the path for the language files. You must use the '{LANGUAGE}' placeholder for the language or the script wont find any language files.
15      *
16      * @var string
17      */
18     protected $filePath = './lang/lang_{LANGUAGE}.ini';
19
20     /**
21      * Cache file path
22      * This is the path for all the cache files. Best is an empty directory with no other files in it.
23      *
24      * @var string
25      */
26     protected $cachePath = './langcache/';
27
28     /**
29      * Fallback language
30      * This is the language which is used when there is no language file for all other user languages. It has the lowest priority.
31      * Remember to create a language file for the fallback!!
32      *
33      * @var string
34      */
35     protected $fallbackLang = 'en';
36
37     /**
38      * Merge in fallback language
39      * Whether to merge current language's strings with the strings of the fallback language ($fallbackLang).
40      *
41      * @var bool
42      */
43     protected $mergeFallback = false;
44
45     /**
46      * The class name of the compiled class that contains the translated texts.
47      * @var string
48      */
49     protected $prefix = 'L';
50
51     /**
52      * Forced language
53      * If you want to force a specific language define it here.
54      *
55      * @var string
56      */
57     protected $forcedLang = NULL;
58
59     /**
60      * This is the separator used if you use sections in your ini-file.
61      * For example, if you have a string 'greeting' in a section 'welcomepage' you will can access it via 'L::welcomepage_greeting'.
62      * If you changed it to 'ABC' you could access your string via 'L::welcomepageABCgreeting'
63      *
64      * @var string
65      */
66     protected $sectionSeparator = '_';
67
68
69     /*
70      * The following properties are only available after calling init().
71      */
72
73     /**
74      * User languages
75      * These are the languages the user uses.
76      * Normally, if you use the getUserLangs-method this array will be filled in like this:
77      * 1. Forced language
78      * 2. Language in $_GET['lang']
79      * 3. Language in $_SESSION['lang']
80      * 4. HTTP_ACCEPT_LANGUAGE
81      * 5. Language in $_COOKIE['lang']
82      * 6. Fallback language
83      *
84      * @var array
85      */
86     protected $userLangs = array();
87
88     protected $appliedLang = NULL;
89     protected $langFilePath = NULL;
90     protected $cacheFilePath = NULL;
91     protected $isInitialized = false;
92
93
94     /**
95      * Constructor
96      * The constructor sets all important settings. All params are optional, you can set the options via extra functions too.
97      *
98      * @param string [$filePath] This is the path for the language files. You must use the '{LANGUAGE}' placeholder for the language.
99      * @param string [$cachePath] This is the path for all the cache files. Best is an empty directory with no other files in it. No placeholders.
100      * @param string [$fallbackLang] This is the language which is used when there is no language file for all other user languages. It has the lowest priority.
101      * @param string [$prefix] The class name of the compiled class that contains the translated texts. Defaults to 'L'.
102      */
103     public function __construct($filePath = NULL, $cachePath = NULL, $fallbackLang = NULL, $prefix = NULL) {
104         // Apply settings
105         if ($filePath != NULL) {
106             $this->filePath = $filePath;
107         }
108
109         if ($cachePath != NULL) {
110             $this->cachePath = $cachePath;
111         }
112
113         if ($fallbackLang != NULL) {
114             $this->fallbackLang = $fallbackLang;
115         }
116
117         if ($prefix != NULL) {
118             $this->prefix = $prefix;
119         }
120     }
121
122     public function init() {
123         if ($this->isInitialized()) {
124             throw new BadMethodCallException('This object from class ' . __CLASS__ . ' is already initialized. It is not possible to init one object twice!');
125         }
126
127         $this->isInitialized = true;
128
129         $this->userLangs = $this->getUserLangs();
130
131         // search for language file
132         $this->appliedLang = NULL;
133         foreach ($this->userLangs as $priority => $langcode) {
134             $this->langFilePath = $this->getConfigFilename($langcode);
135             if (file_exists($this->langFilePath)) {
136                 $this->appliedLang = $langcode;
137                 break;
138             }
139         }
140         if ($this->appliedLang == NULL) {
141             throw new RuntimeException('No language file was found.');
142         }
143
144         // search for cache file
145         $this->cacheFilePath = $this->cachePath . '/php_i18n_' . md5_file(__FILE__) . '_' . $this->prefix . '_' . $this->appliedLang . '.cache.php';
146
147         // whether we need to create a new cache file
148         $outdated = !file_exists($this->cacheFilePath) ||
149             filemtime($this->cacheFilePath) < filemtime($this->langFilePath) || // the language config was updated
150             ($this->mergeFallback && filemtime($this->cacheFilePath) < filemtime($this->getConfigFilename($this->fallbackLang))); // the fallback language config was updated
151
152         if ($outdated) {
153             $config = $this->load($this->langFilePath);
154             if ($this->mergeFallback)
155                 $config = array_replace_recursive($this->load($this->getConfigFilename($this->fallbackLang)), $config);
156
157             $compiled = "<?php class " . $this->prefix . " {\n"
158                 . $this->compile($config)
159                 . 'public static function __callStatic($string, $args) {' . "\n"
160                 . '    return vsprintf(constant("self::" . $string), $args);'
161                 . "\n}\n}\n"
162                 . "function ".$this->prefix .'($string, $args=NULL) {'."\n"
163                 . '    $return = constant("'.$this->prefix.'::".$string);'."\n"
164                 . '    return $args ? vsprintf($return,$args) : $return;'
165                 . "\n}";
166
167                         if( ! is_dir($this->cachePath))
168                                 mkdir($this->cachePath, 0755, true);
169
170             if (file_put_contents($this->cacheFilePath, $compiled) === FALSE) {
171                 throw new Exception("Could not write cache file to path '" . $this->cacheFilePath . "'. Is it writable?");
172             }
173             chmod($this->cacheFilePath, 0755);
174
175         }
176
177         require_once $this->cacheFilePath;
178     }
179
180     public function isInitialized() {
181         return $this->isInitialized;
182     }
183
184     public function getAppliedLang() {
185         return $this->appliedLang;
186     }
187
188     public function getCachePath() {
189         return $this->cachePath;
190     }
191
192     public function getFallbackLang() {
193         return $this->fallbackLang;
194     }
195
196     public function setFilePath($filePath) {
197         $this->fail_after_init();
198         $this->filePath = $filePath;
199     }
200
201     public function setCachePath($cachePath) {
202         $this->fail_after_init();
203         $this->cachePath = $cachePath;
204     }
205
206     public function setFallbackLang($fallbackLang) {
207         $this->fail_after_init();
208         $this->fallbackLang = $fallbackLang;
209     }
210
211     public function setMergeFallback($mergeFallback) {
212         $this->fail_after_init();
213         $this->mergeFallback = $mergeFallback;
214     }
215
216     public function setPrefix($prefix) {
217         $this->fail_after_init();
218         $this->prefix = $prefix;
219     }
220
221     public function setForcedLang($forcedLang) {
222         $this->fail_after_init();
223         $this->forcedLang = $forcedLang;
224     }
225
226     public function setSectionSeparator($sectionSeparator) {
227         $this->fail_after_init();
228         $this->sectionSeparator = $sectionSeparator;
229     }
230
231     /**
232      * @deprecated Use setSectionSeparator.
233      */
234     public function setSectionSeperator($sectionSeparator) {
235         $this->setSectionSeparator($sectionSeparator);
236     }
237
238     /**
239      * getUserLangs()
240      * Returns the user languages
241      * Normally it returns an array like this:
242      * 1. Forced language
243      * 2. Language in $_GET['lang']
244      * 3. Language in $_SESSION['lang']
245      * 4. HTTP_ACCEPT_LANGUAGE
246      * 5. Language in $_COOKIE['lang']
247      * 6. Fallback language
248      * Note: duplicate values are deleted.
249      *
250      * @return array with the user languages sorted by priority.
251      */
252     public function getUserLangs() {
253         $userLangs = array();
254
255         // Highest priority: forced language
256         if ($this->forcedLang != NULL) {
257             $userLangs[] = $this->forcedLang;
258         }
259
260         // 2nd highest priority: GET parameter 'lang'
261         if (isset($_GET['lang']) && is_string($_GET['lang'])) {
262             $userLangs[] = $_GET['lang'];
263         }
264
265         // 3rd highest priority: SESSION parameter 'lang'
266         if (isset($_SESSION['lang']) && is_string($_SESSION['lang'])) {
267             $userLangs[] = $_SESSION['lang'];
268         }
269
270         // 4th highest priority: HTTP_ACCEPT_LANGUAGE
271         if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
272             foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $part) {
273                 $userLangs[] = strtolower(substr($part, 0, 2));
274             }
275         }
276
277         // 5th highest priority: COOKIE
278         if (isset($_COOKIE['lang'])) {
279           $userLangs[] = $_COOKIE['lang'];
280         }
281
282         // Lowest priority: fallback
283         $userLangs[] = $this->fallbackLang;
284
285         // remove duplicate elements
286         $userLangs = array_unique($userLangs);
287
288         // remove illegal userLangs
289         $userLangs2 = array();
290         foreach ($userLangs as $key => $value) {
291             // only allow a-z, A-Z and 0-9 and _ and -
292             if (preg_match('/^[a-zA-Z0-9_-]*$/', $value) === 1)
293                 $userLangs2[$key] = $value;
294         }
295
296         return $userLangs2;
297     }
298
299     protected function getConfigFilename($langcode) {
300         return str_replace('{LANGUAGE}', $langcode, $this->filePath);
301     }
302
303     protected function load($filename) {
304         $ext = substr(strrchr($filename, '.'), 1);
305         switch ($ext) {
306             case 'properties':
307             case 'ini':
308                 $config = parse_ini_file($filename, true);
309                 break;
310             case 'yml':
311             case 'yaml':
312                 $config = spyc_load_file($filename);
313                 break;
314             case 'json':
315                 $config = json_decode(file_get_contents($filename), true);
316                 break;
317             default:
318                 throw new InvalidArgumentException($ext . " is not a valid extension!");
319         }
320         return $config;
321     }
322
323     /**
324      * Recursively compile an associative array to PHP code.
325      */
326     protected function compile($config, $prefix = '') {
327         $code = '';
328         foreach ($config as $key => $value) {
329             if (is_array($value)) {
330                 $code .= $this->compile($value, $prefix . $key . $this->sectionSeparator);
331             } else {
332                 $fullName = $prefix . $key;
333                 if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $fullName)) {
334                     throw new InvalidArgumentException(__CLASS__ . ": Cannot compile translation key " . $fullName . " because it is not a valid PHP identifier.");
335                 }
336                 $code .= 'const ' . $fullName . ' = \'' . str_replace('\'', '\\\'', $value) . "';\n";
337             }
338         }
339         return $code;
340     }
341
342     protected function fail_after_init() {
343         if ($this->isInitialized()) {
344             throw new BadMethodCallException('This ' . __CLASS__ . ' object is already initalized, so you can not change any settings.');
345         }
346     }
347 }