1: <?php
2: /**
3: * This file contains the the I18N class.
4: *
5: * @package Core
6: * @subpackage I18N
7: * @author Murat Purc <murat@purc.de>
8: * @copyright four for business AG <www.4fb.de>
9: * @license http://www.contenido.org/license/LIZENZ.txt
10: * @link http://www.4fb.de
11: * @link http://www.contenido.org
12: */
13:
14: defined('CON_FRAMEWORK') || die('Illegal call: Missing framework initialization - request aborted.');
15:
16: /**
17: * Internationalization (i18n) class.
18: *
19: * @package Core
20: * @subpackage I18N
21: */
22: class cI18n {
23:
24: /**
25: * i18n related assoziative data cache.
26: *
27: * @var array
28: */
29: protected static $_i18nData = array(
30: 'language' => NULL,
31: 'domains' => array(),
32: 'files' => array(),
33: 'cache' => array()
34: );
35:
36: /**
37: * Initializes the i18n.
38: *
39: * @param string $localePath
40: * Path to the locales
41: * @param string $langCode
42: * Language code to set
43: * @param string $domain [optional]
44: * Language domain
45: */
46: public static function init($localePath, $langCode, $domain = 'contenido') {
47: if (function_exists('bindtextdomain')) {
48: // Bind the domain 'contenido' to our locale path
49: bindtextdomain($domain, $localePath);
50:
51: // Set the default text domain to 'contenido'
52: textdomain($domain);
53:
54: // Half brute-force to set the locale.
55: if (!ini_get('safe_mode')) {
56: putenv("LANG=$langCode");
57: }
58:
59: if (defined('LC_MESSAGES')) {
60: setlocale(LC_MESSAGES, $langCode);
61: }
62:
63: if (false === empty($langCode)) {
64: setlocale(LC_CTYPE, $langCode);
65: }
66: }
67:
68: self::$_i18nData['domains'][$domain] = $localePath;
69: self::$_i18nData['language'] = $langCode;
70: }
71:
72: /**
73: * Returns translation of a specific text, wrapper for translate().
74: *
75: * @param string $string
76: * The string to translate
77: * @param string $domain [optional]
78: * The domain to look up
79: *
80: * @return string
81: * Returns the translation
82: *
83: * @throws cException
84: */
85: public static function __($string, $domain = 'contenido') {
86: return self::translate($string, $domain);
87: }
88:
89: /**
90: * Returns translation of a specific text
91: *
92: * @param string $string
93: * The string to translate
94: * @param string $domain [optional]
95: * The domain to look up
96: *
97: * @return string
98: * Returns the translation
99: *
100: * @throws cException
101: * if this is the backend mode and the $belang is not set
102: */
103: public static function translate($string, $domain = 'contenido') {
104: global $cfg, $belang, $contenido;
105:
106: // Auto initialization
107: if (!self::$_i18nData['language']) {
108: if (!isset($belang)) {
109: if ($contenido) {
110: throw new cException('init $belang is not set');
111: }
112: // Needed - otherwise this won't work
113: $belang = false;
114: }
115:
116: // CON-2165
117: // initialise localisation of plugins correctly in frontend
118: if ($domain === 'contenido') {
119: self::init($cfg['path']['contenido_locale'], $belang, $domain);
120: } else {
121: if (empty($belang)) {
122: $oApiLang = cRegistry::getLanguage();
123: $language = $oApiLang->getProperty('language', 'code');
124: $country = $oApiLang->getProperty('country', 'code');
125:
126: $locale = $language . '_' . cString::toUpperCase($country);
127: self::init($cfg['path']['contenido'] . $cfg['path']['plugins'] . $domain . '/locale/', $locale, $domain);
128: } else {
129: self::init($cfg['path']['contenido'] . $cfg['path']['plugins'] . $domain . '/locale/', $belang, $domain);
130: }
131: }
132: }
133:
134: // Is emulator to use?
135: if (!$cfg['native_i18n']) {
136: $ret = self::emulateGettext($string, $domain);
137: // hopefully a proper replacement for
138: // mb_convert_encoding($string, 'HTML-ENTITIES', 'utf-8');
139: // see http://stackoverflow.com/q/11974008
140: $ret = htmlspecialchars_decode(utf8_decode(conHtmlentities($ret, ENT_COMPAT, 'utf-8')));
141: return $ret;
142: }
143:
144: // Try to use native gettext implementation
145: if (extension_loaded('gettext')) {
146: if (function_exists('dgettext')) {
147: if ($domain != 'contenido') {
148: $translation = dgettext($domain, $string);
149: return $translation;
150: } else {
151: return gettext($string);
152: }
153: }
154: }
155:
156: // Emulator as fallback
157: $ret = self::emulateGettext($string, $domain);
158: if (cString::isUtf8($ret)) {
159: $ret = utf8_decode($ret);
160: }
161: return $ret;
162: }
163:
164: /**
165: * Returns the current language (if already defined)
166: *
167: * @return string|false
168: */
169: public static function getLanguage() {
170: return (self::$_i18nData['language']) ? self::$_i18nData['language'] : false;
171: }
172:
173: /**
174: * Returns list of registered domains
175: *
176: * @return array
177: */
178: public static function getDomains() {
179: return self::$_i18nData['domains'];
180: }
181:
182: /**
183: * Returns list of cached tranlation files
184: *
185: * @return array
186: */
187: public static function getFiles() {
188: return self::$_i18nData['files'];
189: }
190:
191: /**
192: * Returns list of cached tranlations
193: *
194: * @return array
195: */
196: public static function getCache() {
197: return self::$_i18nData['cache'];
198: }
199:
200: /**
201: * Resets cached translation data (language, domains, files, and cache)
202: */
203: public static function reset() {
204: self::$_i18nData['language'] = NULL;
205: self::$_i18nData['domains'] = array();
206: self::$_i18nData['files'] = array();
207: self::$_i18nData['cache'] = array();
208: }
209:
210: /**
211: * Emulates GNU gettext
212: *
213: * @param string $string
214: * The string to translate
215: * @param string $domain [optional]
216: * The domain to look up
217: *
218: * @return string
219: * Returns the translation
220: *
221: * @throws cInvalidArgumentException
222: */
223: public static function emulateGettext($string, $domain = 'contenido') {
224: if ($string == '') {
225: return '';
226: }
227:
228: if (!isset(self::$_i18nData['cache'][$domain])) {
229: self::$_i18nData['cache'][$domain] = array();
230: }
231: if (isset(self::$_i18nData['cache'][$domain][$string])) {
232: return self::$_i18nData['cache'][$domain][$string];
233: }
234: if (!isset(self::$_i18nData['domains'][$domain])) {
235: return $string;
236: }
237:
238: $translationFile = self::$_i18nData['domains'][$domain] . self::$_i18nData['language'] . '/LC_MESSAGES/' . $domain . '.po';
239: if (!cFileHandler::exists($translationFile)) {
240: return $string;
241: }
242:
243: if (!isset(self::$_i18nData['files'][$domain])) {
244: self::$_i18nData['files'][$domain] = self::_loadTranslationFile($translationFile);
245: }
246:
247: $stringStart = cString::findFirstPos(self::$_i18nData['files'][$domain], '"' . str_replace(array(
248: "\n",
249: "\r",
250: "\t"
251: ), array(
252: '\n',
253: '\r',
254: '\t'
255: ), $string) . '"');
256: if ($stringStart === false) {
257: return $string;
258: }
259:
260: $matches = array();
261: $quotedString = preg_quote(str_replace(array(
262: "\n",
263: "\r",
264: "\t"
265: ), array(
266: '\n',
267: '\r',
268: '\t'
269: ), $string), '/');
270: $result = preg_match("/msgid.*\"(" . $quotedString . ")\"(?:\s*)?\nmsgstr(?:\s*)\"(.*)\"/", self::$_i18nData['files'][$domain], $matches);
271: // Old:
272: // preg_match("/msgid.*\"".preg_quote($string,"/")."\".*\nmsgstr(\s*)\"(.*)\"/",
273: // self::$_i18nData['files'][$domain], $matches);
274:
275: if ($result && !empty($matches[2])) {
276: // Translation found, cache it
277: self::$_i18nData['cache'][$domain][$string] = stripslashes(str_replace(array(
278: '\n',
279: '\r',
280: '\t'
281: ), array(
282: "\n",
283: "\r",
284: "\t"
285: ), $matches[2]));
286: } else {
287: // Translation not found, cache original string
288: self::$_i18nData['cache'][$domain][$string] = $string;
289: }
290:
291: return self::$_i18nData['cache'][$domain][$string];
292: }
293:
294: /**
295: * Registers a new i18n domain.
296: *
297: * @param string $domain
298: * Domain to bind to
299: * @param string $localePath
300: * Path to the locales
301: */
302: public static function registerDomain($domain, $localePath) {
303: if (function_exists('bindtextdomain')) {
304: // Bind the domain 'contenido' to our locale path
305: bindtextdomain($domain, $localePath);
306: }
307: self::$_i18nData['domains'][$domain] = $localePath;
308: }
309:
310: /**
311: * Loads gettext translation and file does some operations like stripping
312: * comments on the content.
313: *
314: * @param string $translationFile
315: *
316: * @return string
317: * The preparend translation file content
318: *
319: * @throws cInvalidArgumentException
320: */
321: protected static function _loadTranslationFile($translationFile) {
322: $content = cFileHandler::read($translationFile);
323:
324: // Normalize eol chars
325: $content = str_replace("\n\r", "\n", $content);
326: $content = str_replace("\r\n", "\n", $content);
327:
328: // Remove comment lines
329: $content = preg_replace('/^#.+\n/m', '', $content);
330:
331: // Prepare for special po edit format
332: /*
333: * Something like: #, php-format msgid "" "Hello %s,\n" "\n" "you've got
334: * a new reminder for the client '%s' at\n" "%s:\n" "\n" "%s" msgstr ""
335: * "Hallo %s,\n" "\n" "du hast eine Wiedervorlage erhalten für den
336: * Mandanten '%s' at\n" "%s:\n" "\n" "%s" has to be converted to: msgid
337: * "Hello %s,\n\nyou've got a new reminder for the client '%s'
338: * at\n%s:\n\n%s" msgstr "Hallo %s,\n\ndu hast eine Wiedervorlage
339: * erhalten für den Mandanten '%s' at\n%s:\n\n%s"
340: */
341: // assemble broken long message lines (remove double quotes with a line
342: // break in between, e.g. "\n")
343: $content = preg_replace('/(""\\s+")/m', '"', $content);
344: // replace line breaks followed by a whitespace character against a line
345: // break
346: $content = preg_replace('/\\n"\\s+"/m', '\\n', $content);
347: // remove multiple line breaks
348: $content = preg_replace('/("\n+")/m', '', $content);
349: // remove the backslash from double quotes (\"foobar\" -> "foobar")
350: $content = preg_replace('/(\\\")/m', '"', $content);
351:
352: return $content;
353: }
354: }