diff --git a/conlite/classes/class.i18n.php b/conlite/classes/class.i18n.php new file mode 100644 index 0000000..47e507c --- /dev/null +++ b/conlite/classes/class.i18n.php @@ -0,0 +1,376 @@ + + * @copyright four for business AG + * @license http://www.contenido.org/license/LIZENZ.txt + * @link http://www.4fb.de + * @link http://www.contenido.org + */ + +defined('CON_FRAMEWORK') || die('Illegal call: Missing framework initialization - request aborted.'); + +/** + * Internationalization (i18n) class. + * + * @package Core + * @subpackage I18N + */ +class cI18n { + + const i18n_DEFAULT_DOMAIN = "conlite"; + + /** + * i18n related assoziative data cache. + * + * @var array + */ + protected static $_i18nData = array( + 'language' => NULL, + 'domains' => array(), + 'files' => array(), + 'cache' => array() + ); + + /** + * Initializes the i18n. + * + * @param string $localePath + * Path to the locales + * @param string $langCode + * Language code to set + * @param string $domain [optional] + * Language domain + */ + public static function init($localePath, $langCode, $domain = self::i18n_DEFAULT_DOMAIN) { + if (function_exists('bindtextdomain')) { + // Bind the domain 'contenido' to our locale path + bindtextdomain($domain, $localePath); + + // Set the default text domain to 'contenido' + textdomain($domain); + + // Half brute-force to set the locale. + if (!ini_get('safe_mode')) { + putenv("LANG=$langCode"); + } + + if (defined('LC_MESSAGES')) { + setlocale(LC_MESSAGES, $langCode); + } + + if (false === empty($langCode)) { + setlocale(LC_CTYPE, $langCode); + } + } + + self::$_i18nData['domains'][$domain] = $localePath; + self::$_i18nData['language'] = $langCode; + } + + /** + * Returns translation of a specific text, wrapper for translate(). + * + * @param string $string + * The string to translate + * @param string $domain [optional] + * The domain to look up + * @return string + * Returns the translation + */ + public static function __($string, $domain = self::i18n_DEFAULT_DOMAIN) { + return self::translate($string, $domain); + } + + /** + * Returns translation of a specific text + * + * @param string $string + * The string to translate + * @param string $domain [optional] + * The domain to look up + * @throws cException + * if this is the backend mode and the $belang is not set + * @return string + * Returns the translation + */ + public static function translate($string, $domain = self::i18n_DEFAULT_DOMAIN) { + global $cfg, $belang, $contenido; + + // Auto initialization + if (!self::$_i18nData['language']) { + if (!isset($belang)) { + if ($contenido) { + throw new Exception('init $belang is not set'); + } + // Needed - otherwise this won't work + $belang = false; + } + + // CON-2165 + // initialise localisation of plugins correctly in frontend + if ($domain === self::i18n_DEFAULT_DOMAIN) { + self::init($cfg['path']['contenido_locale'], $belang, $domain); + } else { + if (empty($belang)) { + $oApiLang = new cApiLanguage(cRegistry::getLanguageId()); + $language = $oApiLang->getProperty('language', 'code'); + $country = $oApiLang->getProperty('country', 'code'); + + $locale = $language . '_' . strtoupper($country); + self::init($cfg['path']['contenido'] . $cfg['path']['plugins'] . $domain . '/locale/', $locale, $domain); + } else { + self::init($cfg['path']['contenido'] . $cfg['path']['plugins'] . $domain . '/locale/', $belang, $domain); + } + } + } + + // Is emulator to use? + if (!$cfg['native_i18n']) { + $ret = self::emulateGettext($string, $domain); + // hopefully a proper replacement for + // mb_convert_encoding($string, 'HTML-ENTITIES', 'utf-8'); + // see http://stackoverflow.com/q/11974008 + $ret = htmlspecialchars_decode(utf8_decode(conHtmlentities($ret, ENT_COMPAT, 'utf-8', false))); + return $ret; + } + + // Try to use native gettext implementation + if (extension_loaded('gettext')) { + if (function_exists('dgettext')) { + if ($domain != self::i18n_DEFAULT_DOMAIN) { + $translation = dgettext($domain, $string); + return $translation; + } else { + return gettext($string); + } + } + } + + // Emulator as fallback + $ret = self::emulateGettext($string, $domain); + if (self::_isUtf8($ret)) { + $ret = utf8_decode($ret); + } + return $ret; + } + + /** + * Returns the current language (if already defined) + * + * @return string|false + */ + public static function getLanguage() { + return (self::$_i18nData['language']) ? self::$_i18nData['language'] : false; + } + + /** + * Returns list of registered domains + * + * @return array + */ + public static function getDomains() { + return self::$_i18nData['domains']; + } + + /** + * Returns list of cached tranlation files + * + * @return array + */ + public static function getFiles() { + return self::$_i18nData['files']; + } + + /** + * Returns list of cached tranlations + * + * @return array + */ + public static function getCache() { + return self::$_i18nData['cache']; + } + + /** + * Resets cached translation data (language, domains, files, and cache) + */ + public static function reset() { + self::$_i18nData['language'] = NULL; + self::$_i18nData['domains'] = array(); + self::$_i18nData['files'] = array(); + self::$_i18nData['cache'] = array(); + } + + /** + * Emulates GNU gettext + * + * @param string $string + * The string to translate + * @param string $domain [optional] + * The domain to look up + * @return string + * Returns the translation + */ + public static function emulateGettext($string, $domain = self::i18n_DEFAULT_DOMAIN) { + if ($string == '') { + return ''; + } + + if (!isset(self::$_i18nData['cache'][$domain])) { + self::$_i18nData['cache'][$domain] = array(); + } + if (isset(self::$_i18nData['cache'][$domain][$string])) { + return self::$_i18nData['cache'][$domain][$string]; + } + + $translationFile = self::$_i18nData['domains'][$domain] . self::$_i18nData['language'] . '/LC_MESSAGES/' . $domain . '.po'; + if (!cFileHandler::exists($translationFile)) { + return $string; + } + + if (!isset(self::$_i18nData['files'][$domain])) { + self::$_i18nData['files'][$domain] = self::_loadTranslationFile($translationFile); + } + + $stringStart = strpos(self::$_i18nData['files'][$domain], '"' . str_replace(array( + "\n", + "\r", + "\t" + ), array( + '\n', + '\r', + '\t' + ), $string) . '"'); + if ($stringStart === false) { + return $string; + } + + $matches = array(); + $quotedString = preg_quote(str_replace(array( + "\n", + "\r", + "\t" + ), array( + '\n', + '\r', + '\t' + ), $string), '/'); + $result = preg_match("/msgid.*\"(" . $quotedString . ")\"(?:\s*)?\nmsgstr(?:\s*)\"(.*)\"/", self::$_i18nData['files'][$domain], $matches); + // Old: + // preg_match("/msgid.*\"".preg_quote($string,"/")."\".*\nmsgstr(\s*)\"(.*)\"/", + // self::$_i18nData['files'][$domain], $matches); + + if ($result && !empty($matches[2])) { + // Translation found, cache it + self::$_i18nData['cache'][$domain][$string] = stripslashes(str_replace(array( + '\n', + '\r', + '\t' + ), array( + "\n", + "\r", + "\t" + ), $matches[2])); + } else { + // Translation not found, cache original string + self::$_i18nData['cache'][$domain][$string] = $string; + } + + return self::$_i18nData['cache'][$domain][$string]; + } + + /** + * Registers a new i18n domain. + * + * @param string $localePath + * Path to the locales + * @param string $domain + * Domain to bind to + */ + public static function registerDomain($domain, $localePath) { + if (function_exists('bindtextdomain')) { + // Bind the domain 'contenido' to our locale path + bindtextdomain($domain, $localePath); + } + self::$_i18nData['domains'][$domain] = $localePath; + } + + /** + * Loads gettext translation and file does some operations like stripping + * comments on the content. + * + * @param string $translationFile + * @return string + * The preparend translation file content + */ + protected static function _loadTranslationFile($translationFile) { + $content = cFileHandler::read($translationFile); + + // Normalize eol chars + $content = str_replace("\n\r", "\n", $content); + $content = str_replace("\r\n", "\n", $content); + + // Remove comment lines + $content = preg_replace('/^#.+\n/m', '', $content); + + // Prepare for special po edit format + /* + * Something like: #, php-format msgid "" "Hello %s,\n" "\n" "you've got + * a new reminder for the client '%s' at\n" "%s:\n" "\n" "%s" msgstr "" + * "Hallo %s,\n" "\n" "du hast eine Wiedervorlage erhalten für den + * Mandanten '%s' at\n" "%s:\n" "\n" "%s" has to be converted to: msgid + * "Hello %s,\n\nyou've got a new reminder for the client '%s' + * at\n%s:\n\n%s" msgstr "Hallo %s,\n\ndu hast eine Wiedervorlage + * erhalten für den Mandanten '%s' at\n%s:\n\n%s" + */ + // assemble broken long message lines (remove double quotes with a line + // break in between, e.g. "\n") + $content = preg_replace('/(""\\s+")/m', '"', $content); + // replace line breaks followed by a whitespace character against a line + // break + $content = preg_replace('/\\n"\\s+"/m', '\\n', $content); + // remove multiple line breaks + $content = preg_replace('/("\n+")/m', '', $content); + // remove the backslash from double quotes (\"foobar\" -> "foobar") + $content = preg_replace('/(\\\")/m', '"', $content); + + return $content; + } + + private static function _isUtf8($input) { + $len = strlen($input); + + for ($i = 0; $i < $len; $i++) { + $char = ord($input[$i]); + $n = 0; + + if ($char < 0x80) { + // ASCII char + continue; + } else if (($char & 0xE0) === 0xC0 && $char > 0xC1) { + // 2 byte long char + $n = 1; + } else if (($char & 0xF0) === 0xE0) { + // 3 byte long char + $n = 2; + } else if (($char & 0xF8) === 0xF0 && $char < 0xF5) { + // 4 byte long char + $n = 3; + } else { + return false; + } + + for ($j = 0; $j < $n; $j++) { + $i++; + + if ($i == $len || (ord($input[$i]) & 0xC0) !== 0x80) { + return false; + } + } + } + return true; + } +} \ No newline at end of file