├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── composer.json ├── composer.lock ├── example.php ├── i18n.class.php ├── lang ├── lang_de.ini ├── lang_de.json ├── lang_de.yml ├── lang_en.ini ├── lang_en.json └── lang_en.yml ├── langcache └── README.md └── yml-example.php /.gitignore: -------------------------------------------------------------------------------- 1 | langcache/* 2 | !langcache/README.md 3 | /vendor 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '5.4' 4 | - '5.5' 5 | - '5.6' 6 | - '7.0' 7 | - '7.1' 8 | script: php example.php -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 php-i18n authors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP i18n 2 | 3 | [](https://packagist.org/packages/philipp15b/php-i18n) [](https://travis-ci.org/Philipp15b/php-i18n) 4 | 5 | This is a simple i18n class for PHP. Nothing fancy, but fast, because it uses caching and it is easy to use. Try it out! 6 | 7 | Some of its features: 8 | 9 | * Translation strings in `.ini`/`.properties`, `.json` or `.yaml` format 10 | * Caching 11 | * Simple API: `L::category_stringname` 12 | * Built-in support for [vsprintf](http://php.net/manual/en/function.vsprintf.php) formatting: `L::name($par1)` 13 | * Automatic user language detection 14 | * Simplicity ;) 15 | 16 | ## Requirements 17 | 18 | * Write permissions in cache directory 19 | * PHP 5.2 and above 20 | * PHP SPL extension (installed by default) 21 | 22 | ## Setup 23 | 24 | There's a usable example in the `example.php` file. You just have to follow these easy five steps: 25 | 26 | ### 1. Create language files 27 | 28 | To use this class, you need to create translation files with your translated strings. They can be `.ini`/`.properties`, `.json` or `.yaml` files. This could look like this: 29 | 30 | `lang_en.ini` (English) 31 | 32 | ```ini 33 | greeting = "Hello World!" 34 | 35 | [category] 36 | somethingother = "Something other..." 37 | ``` 38 | 39 | `lang_de.ini` (German) 40 | 41 | ```ini 42 | greeting = "Hallo Welt!" 43 | 44 | [category] 45 | somethingother = "Etwas anderes..." 46 | ``` 47 | 48 | Save both files in the directory you will set in step 4. 49 | The files must be named according to the filePath setting, where '{LANGUAGE}' will be replaced by the user's language, e.g. 'en' or 'de'. 50 | 51 | ### 2. Include the class 52 | 53 | ```php 54 | 57 | ``` 58 | 59 | ### 3. Initialize the class 60 | ```php 61 | 64 | ``` 65 | 66 | ### 4. Set some settings if necessary 67 | 68 | The possible settings are: 69 | 70 | * Language file path (default: `./lang/lang_{LANGUAGE}.ini`) 71 | * Cache file path (default: `./langcache/`) 72 | * Preserve language region variants: if set to true, region variants in language code strings such as en-us and en-gb will be preserved, otherwise will be trimmed to en (default: `false`) 73 | * The fallback language, if no one of the user languages is available (default: `en`) 74 | * A 'prefix', the compiled class name (default `L`) 75 | * A forced language, if you want to force a language (default: none) 76 | * The section separator: this is used to seperate the sections in the language class. If you set the separator to `_abc_` you could access your localized strings via `L::category_abc_stringname` if you use categories in your ini. (default: `_`) 77 | * Merge keys from the fallback language into the current language 78 | 79 | ```php 80 | setCachePath('./tmp/cache'); 82 | $i18n->setFilePath('./langfiles/lang/lang_{LANGUAGE}.ini'); // language file path 83 | $i18n->setLangVariantEnabled(false); // trim region variant in language codes (e.g. en-us -> en) 84 | $i18n->setFallbackLang('en'); 85 | $i18n->setPrefix('I'); 86 | $i18n->setForcedLang('en'); // force english, even if another user language is available 87 | $i18n->setSectionSeparator('_'); 88 | $i18n->setMergeFallback(false); // make keys available from the fallback language 89 | ?> 90 | ``` 91 | 92 | #### Shorthand 93 | 94 | There is also a shorthand for that: you can set all settings in the constructor. 95 | 96 | ```php 97 | 100 | ``` 101 | 102 | The (all optional) parameters are: 103 | 104 | 1. the language file path (the ini files) 105 | 2. the language cache path 106 | 3. fallback language 107 | 4. the prefix/compiled class name 108 | 109 | ### 5. Call the `init()` method to load all files and translations 110 | 111 | Call the `init()` file to instruct the class to load the appropriate language file, load the cache file or generate it if it doesn't exist and make the `L` class available so you can access your localizations. 112 | 113 | ```php 114 | init(); 116 | ?> 117 | ``` 118 | 119 | ### 6. Use the localizations 120 | 121 | To call your localizations, simply use the `L` class and a class constant for the string. 122 | 123 | In this example, we use the translation string seen in step 1. 124 | 125 | ```php 126 | 143 | ``` 144 | 145 | As you can see, you can also call the constant as a function. It will be formatted with [vsprintf](http://php.net/manual/en/function.vsprintf.php). 146 | 147 | Also, like in the two last examples, a helper function with the same name as the class makes it easier to dynamically access the constants if ever needed. 148 | 149 | Thats it! 150 | 151 | ## How the user language detection works 152 | 153 | This class tries to detect the user's language by trying the following sources in this order: 154 | 155 | 1. Forced language (if set) 156 | 2. GET parameter 'lang' (`$_GET['lang']`) 157 | 3. SESSION parameter 'lang' (`$_SESSION['lang']`) 158 | 4. HTTP_ACCEPT_LANGUAGE (can be multiple languages) (`$_SERVER['HTTP_ACCEPT_LANGUAGE']`) 159 | 5. Fallback language 160 | 161 | php-i18n will remove all characters that are not one of the following: A-Z, a-z or 0-9 to prevent [arbitrary file inclusion](https://en.wikipedia.org/wiki/File_inclusion_vulnerability). 162 | After that the class searches for the language files. For example, if you set the GET parameter 'lang' to 'en' without a forced language set, the class would try to find the file `lang/lang_en.ini` (if the setting `langFilePath` was set to default (`lang/lang_{LANGUAGE}.ini`)). 163 | If this file doesn't exist, php-i18n will try to find the language file for the language defined in the session variable and so on. 164 | 165 | ### How to change this implementation 166 | 167 | You can change the user detection by extending the `i18n` class and overriding the `getUserLangs()` method: 168 | 169 | ```php 170 | 189 | ``` 190 | 191 | This very basic extension only uses the GET parameter 'language' and the session parameter 'userlanguage'. 192 | You see that this method must return an array. 193 | 194 | **Note that this example function is insecure**: `getUserLangs()` also has to escape the results or else i18n will [include arbitrary files](https://en.wikipedia.org/wiki/File_inclusion_vulnerability). The default implementation is safe. 195 | 196 | ## Fork it! 197 | 198 | Contributions are always welcome. 199 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | { 4 | "email": "github@philworld.de", 5 | "name": "Philipp Schröer" 6 | } 7 | ], 8 | "autoload": { 9 | "files": [ "i18n.class.php" ] 10 | }, 11 | "description": "Simple i18n class for PHP", 12 | "license": "MIT", 13 | "name": "philipp15b/php-i18n", 14 | "homepage": "https://github.com/Philipp15b/php-i18n", 15 | "support": { 16 | "issues": "https://github.com/Philipp15b/php-i18n/issues", 17 | "source": "https://github.com/Philipp15b/php-i18n" 18 | }, 19 | "type": "library", 20 | "require": { 21 | "mustangostang/spyc": "0.6.2", 22 | "php": ">= 5.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "4abd7a36b21943bdf75959e4a32283f3", 8 | "packages": [ 9 | { 10 | "name": "mustangostang/spyc", 11 | "version": "0.6.2", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/mustangostang/spyc.git", 15 | "reference": "23c35ae854d835f2d7bcc3e3ad743d7e57a8c14d" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/mustangostang/spyc/zipball/23c35ae854d835f2d7bcc3e3ad743d7e57a8c14d", 20 | "reference": "23c35ae854d835f2d7bcc3e3ad743d7e57a8c14d", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.1" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "4.3.*@dev" 28 | }, 29 | "type": "library", 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "0.5.x-dev" 33 | } 34 | }, 35 | "autoload": { 36 | "files": [ 37 | "Spyc.php" 38 | ] 39 | }, 40 | "notification-url": "https://packagist.org/downloads/", 41 | "license": [ 42 | "MIT" 43 | ], 44 | "authors": [ 45 | { 46 | "name": "mustangostang", 47 | "email": "vlad.andersen@gmail.com" 48 | } 49 | ], 50 | "description": "A simple YAML loader/dumper class for PHP", 51 | "homepage": "https://github.com/mustangostang/spyc/", 52 | "keywords": [ 53 | "spyc", 54 | "yaml", 55 | "yml" 56 | ], 57 | "time": "2017-02-24T16:06:33+00:00" 58 | } 59 | ], 60 | "packages-dev": [], 61 | "aliases": [], 62 | "minimum-stability": "stable", 63 | "stability-flags": [], 64 | "prefer-stable": false, 65 | "prefer-lowest": false, 66 | "platform": { 67 | "php": ">= 5.3" 68 | }, 69 | "platform-dev": [] 70 | } 71 | -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | init(); 9 | ?> 10 | 11 | 12 |
Applied Language: getAppliedLang(); ?>
13 | 14 | 15 |Cache path: getCachePath(); ?>
16 | 17 | 18 |A greeting:
19 |Something other:
-------------------------------------------------------------------------------- /i18n.class.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 116 | } 117 | 118 | if ($cachePath != NULL) { 119 | $this->cachePath = $cachePath; 120 | } 121 | 122 | if ($fallbackLang != NULL) { 123 | $this->fallbackLang = $fallbackLang; 124 | } 125 | 126 | if ($prefix != NULL) { 127 | $this->prefix = $prefix; 128 | } 129 | } 130 | 131 | public function init() { 132 | if ($this->isInitialized()) { 133 | throw new BadMethodCallException('This object from class ' . __CLASS__ . ' is already initialized. It is not possible to init one object twice!'); 134 | } 135 | 136 | $this->isInitialized = true; 137 | 138 | $this->userLangs = $this->getUserLangs(); 139 | 140 | // search for language file 141 | $this->appliedLang = NULL; 142 | foreach ($this->userLangs as $priority => $langcode) { 143 | $this->langFilePath = $this->getConfigFilename($langcode); 144 | if (file_exists($this->langFilePath)) { 145 | $this->appliedLang = $langcode; 146 | break; 147 | } 148 | } 149 | if ($this->appliedLang == NULL) { 150 | throw new RuntimeException('No language file was found.'); 151 | } 152 | 153 | // search for cache file 154 | $this->cacheFilePath = $this->cachePath . '/php_i18n_' . md5_file(__FILE__) . '_' . $this->prefix . '_' . $this->appliedLang . '.cache.php'; 155 | 156 | // whether we need to create a new cache file 157 | $outdated = !file_exists($this->cacheFilePath) || 158 | filemtime($this->cacheFilePath) < filemtime($this->langFilePath) || // the language config was updated 159 | ($this->mergeFallback && filemtime($this->cacheFilePath) < filemtime($this->getConfigFilename($this->fallbackLang))); // the fallback language config was updated 160 | 161 | if ($outdated) { 162 | $config = $this->load($this->langFilePath); 163 | if ($this->mergeFallback) 164 | $config = array_replace_recursive($this->load($this->getConfigFilename($this->fallbackLang)), $config); 165 | 166 | $compiled = "prefix . " {\n" 167 | . $this->compile($config) 168 | . 'public static function __callStatic($string, $args) {' . "\n" 169 | . ' return vsprintf(constant("self::" . $string), $args);' 170 | . "\n}\n}\n" 171 | . "function ".$this->prefix .'($string, $args=NULL) {'."\n" 172 | . ' $return = constant("'.$this->prefix.'::".$string);'."\n" 173 | . ' return $args ? vsprintf($return,$args) : $return;' 174 | . "\n}"; 175 | 176 | if( ! is_dir($this->cachePath)) 177 | mkdir($this->cachePath, 0755, true); 178 | 179 | if (file_put_contents($this->cacheFilePath, $compiled) === FALSE) { 180 | throw new Exception("Could not write cache file to path '" . $this->cacheFilePath . "'. Is it writable?"); 181 | } 182 | chmod($this->cacheFilePath, 0755); 183 | 184 | } 185 | 186 | require_once $this->cacheFilePath; 187 | } 188 | 189 | public function isInitialized() { 190 | return $this->isInitialized; 191 | } 192 | 193 | public function getAppliedLang() { 194 | return $this->appliedLang; 195 | } 196 | 197 | public function getCachePath() { 198 | return $this->cachePath; 199 | } 200 | 201 | public function getLangVariantEnabled() { 202 | return $this->isLangVariantEnabled; 203 | } 204 | 205 | public function getFallbackLang() { 206 | return $this->fallbackLang; 207 | } 208 | 209 | public function setFilePath($filePath) { 210 | $this->fail_after_init(); 211 | $this->filePath = $filePath; 212 | } 213 | 214 | public function setCachePath($cachePath) { 215 | $this->fail_after_init(); 216 | $this->cachePath = $cachePath; 217 | } 218 | 219 | public function setLangVariantEnabled($isLangVariantEnabled) { 220 | $this->fail_after_init(); 221 | $this->isLangVariantEnabled = $isLangVariantEnabled; 222 | } 223 | 224 | public function setFallbackLang($fallbackLang) { 225 | $this->fail_after_init(); 226 | $this->fallbackLang = $fallbackLang; 227 | } 228 | 229 | public function setMergeFallback($mergeFallback) { 230 | $this->fail_after_init(); 231 | $this->mergeFallback = $mergeFallback; 232 | } 233 | 234 | public function setPrefix($prefix) { 235 | $this->fail_after_init(); 236 | $this->prefix = $prefix; 237 | } 238 | 239 | public function setForcedLang($forcedLang) { 240 | $this->fail_after_init(); 241 | $this->forcedLang = $forcedLang; 242 | } 243 | 244 | public function setSectionSeparator($sectionSeparator) { 245 | $this->fail_after_init(); 246 | $this->sectionSeparator = $sectionSeparator; 247 | } 248 | 249 | /** 250 | * @deprecated Use setSectionSeparator. 251 | */ 252 | public function setSectionSeperator($sectionSeparator) { 253 | $this->setSectionSeparator($sectionSeparator); 254 | } 255 | 256 | /** 257 | * getUserLangs() 258 | * Returns the user languages 259 | * Normally it returns an array like this: 260 | * 1. Forced language 261 | * 2. Language in $_GET['lang'] 262 | * 3. Language in $_SESSION['lang'] 263 | * 4. HTTP_ACCEPT_LANGUAGE 264 | * 5. Language in $_COOKIE['lang'] 265 | * 6. Fallback language 266 | * Note: duplicate values are deleted. 267 | * 268 | * @return array with the user languages sorted by priority. 269 | */ 270 | public function getUserLangs() { 271 | $userLangs = array(); 272 | 273 | // Highest priority: forced language 274 | if ($this->forcedLang != NULL) { 275 | $userLangs[] = $this->forcedLang; 276 | } 277 | 278 | // 2nd highest priority: GET parameter 'lang' 279 | if (isset($_GET['lang']) && is_string($_GET['lang'])) { 280 | $userLangs[] = $_GET['lang']; 281 | } 282 | 283 | // 3rd highest priority: SESSION parameter 'lang' 284 | if (isset($_SESSION['lang']) && is_string($_SESSION['lang'])) { 285 | $userLangs[] = $_SESSION['lang']; 286 | } 287 | 288 | // 4th highest priority: HTTP_ACCEPT_LANGUAGE 289 | if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { 290 | foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $part) { 291 | $userLang = strtolower(explode(';q=', $part)[0]); 292 | 293 | // Trim language variant section if not configured to allow 294 | if (!$this->isLangVariantEnabled) 295 | $userLang = explode('-', $userLang)[0]; 296 | 297 | $userLangs[] = $userLang; 298 | } 299 | } 300 | 301 | // 5th highest priority: COOKIE 302 | if (isset($_COOKIE['lang'])) { 303 | $userLangs[] = $_COOKIE['lang']; 304 | } 305 | 306 | // Lowest priority: fallback 307 | $userLangs[] = $this->fallbackLang; 308 | 309 | // remove duplicate elements 310 | $userLangs = array_unique($userLangs); 311 | 312 | // remove illegal userLangs 313 | $userLangs2 = array(); 314 | foreach ($userLangs as $key => $value) { 315 | // only allow a-z, A-Z and 0-9 and _ and - 316 | if (preg_match('/^[a-zA-Z0-9_-]+$/', $value) === 1) 317 | $userLangs2[$key] = $value; 318 | } 319 | 320 | return $userLangs2; 321 | } 322 | 323 | protected function getConfigFilename($langcode) { 324 | return str_replace('{LANGUAGE}', $langcode, $this->filePath); 325 | } 326 | 327 | protected function load($filename) { 328 | $ext = substr(strrchr($filename, '.'), 1); 329 | switch ($ext) { 330 | case 'properties': 331 | case 'ini': 332 | $config = parse_ini_file($filename, true); 333 | break; 334 | case 'yml': 335 | case 'yaml': 336 | $config = spyc_load_file($filename); 337 | break; 338 | case 'json': 339 | $config = json_decode(file_get_contents($filename), true); 340 | break; 341 | default: 342 | throw new InvalidArgumentException($ext . " is not a valid extension!"); 343 | } 344 | return $config; 345 | } 346 | 347 | /** 348 | * Recursively compile an associative array to PHP code. 349 | */ 350 | protected function compile($config, $prefix = '') { 351 | $code = ''; 352 | foreach ($config as $key => $value) { 353 | if (is_array($value)) { 354 | $code .= $this->compile($value, $prefix . $key . $this->sectionSeparator); 355 | } else { 356 | $fullName = $prefix . $key; 357 | if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $fullName)) { 358 | throw new InvalidArgumentException(__CLASS__ . ": Cannot compile translation key " . $fullName . " because it is not a valid PHP identifier."); 359 | } 360 | $code .= 'const ' . $fullName . ' = \'' . str_replace('\'', '\\\'', $value) . "';\n"; 361 | } 362 | } 363 | return $code; 364 | } 365 | 366 | protected function fail_after_init() { 367 | if ($this->isInitialized()) { 368 | throw new BadMethodCallException('This ' . __CLASS__ . ' object is already initalized, so you can not change any settings.'); 369 | } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /lang/lang_de.ini: -------------------------------------------------------------------------------- 1 | greeting = "Hallo Welt!" 2 | 3 | [category] 4 | somethingother = "Etwas anderes..." -------------------------------------------------------------------------------- /lang/lang_de.json: -------------------------------------------------------------------------------- 1 | { 2 | "greeting": "Hallo Welt!", 3 | "category": { 4 | "somethingother": "Etwas anderes..." 5 | } 6 | } -------------------------------------------------------------------------------- /lang/lang_de.yml: -------------------------------------------------------------------------------- 1 | greeting: 'Hallo Welt!' 2 | 3 | category: 4 | somethingother: 'Etwas anderes...' -------------------------------------------------------------------------------- /lang/lang_en.ini: -------------------------------------------------------------------------------- 1 | greeting = "Hello World!" 2 | 3 | [category] 4 | somethingother = "Something other..." -------------------------------------------------------------------------------- /lang/lang_en.json: -------------------------------------------------------------------------------- 1 | { 2 | "greeting": "Hello World!", 3 | "category": { 4 | "somethingother": "Something other..." 5 | } 6 | } -------------------------------------------------------------------------------- /lang/lang_en.yml: -------------------------------------------------------------------------------- 1 | greeting: 'Hello World!' 2 | 3 | category: 4 | somethingother: 'Something other...' -------------------------------------------------------------------------------- /langcache/README.md: -------------------------------------------------------------------------------- 1 | # Cache folder 2 | Make sure you have write permissions here! 3 | -------------------------------------------------------------------------------- /yml-example.php: -------------------------------------------------------------------------------- 1 | init(); 15 | ?> 16 | 17 | 18 |Applied Language: getAppliedLang(); ?>
19 | 20 | 21 |Cache path: getCachePath(); ?>
22 | 23 | 24 |A greeting:
25 |Something other:
26 | --------------------------------------------------------------------------------