* @license BSD */ namespace Domframework; /** * Language class : change the messages */ class Language { // Language.php // Use Gettext, so the locales must be available in the system // Check the output of the command 'locale -a' : // en_GB.utf8 // en_US.utf8 // [...] // fr_FR.utf8 // The directories must be of this format but with a UTF8 in capital ! // Example : ./locale/en_US.UTF8/LC_MESSAGES/programme.mo // The only available codeset is UTF8 // The languages are always in the format fr_FR (without the codeset) /** * Language cache directory */ public $cacheDir = "data/locale"; /** * Choose the best language in the browser list and which is available in * locale path * @param string|null $repLocale Directory where are stored the translations * @param string|null $languageCode The coding langugage of the soft * @return string The choosed locale whithout charset (like fr_FR) */ public function languageSelection($repLocale = "./locale", $languageCode = "fr_FR") { $arrAccept = []; if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) { // Analyse de HTTP_ACCEPT_LANGUAGE // HTTP_ACCEPT_LANGUAGE est de la forme : fr,en;q=0.7,en-us;q=0.3 // Recuperation de la liste des languages souhaitees $arrAccept = explode(",", strtolower($_SERVER["HTTP_ACCEPT_LANGUAGE"])); // Pre-traitement des choix de l'utilisateur foreach ($arrAccept as $key => $value) { // Suppression des poids (l'ordre est donne dans la pile) if (($pos = strpos($value, ";")) !== false) { $arrAccept[$key] = substr($value, 0, $pos); } // Si la language proposee est du style en-us, convertit en en-US if (($pos = strpos($value, "-")) !== false) { $arrAccept[$key] = substr($arrAccept[$key], 0, $pos) . strtoupper(substr($arrAccept[$key], $pos)); } // Remplacement des tirets par des soulignes $arrAccept[$key] = str_replace("-", "_", $arrAccept[$key]); } } if (isset($_SERVER["LC_MESSAGES"])) { // La ligne ci-dessous permet de récupérer la language sans le codeset si // il est fourni en_US.UTF8 -> en_US @list($languageCodetmp, $codeset) = explode( ".", $_SERVER["LC_MESSAGES"] ); $arrAccept[] = $languageCodetmp; } if (isset($_SERVER["LANG"])) { // La ligne ci-dessous permet de récupérer la language sans le codeset si // il est fourni en_US.UTF8 -> en_US @list($languageCodetmp, $codeset) = explode(".", $_SERVER["LANG"]); $arrAccept[] = $languageCodetmp; } // Si l'utilisateur n'a defini aucune language, on met la language par // defaut if (empty($arrAccept)) { $arrAccept[] = $languageCode; } // Le tableau $arrAccept est trié par priorité de language souhaité par // l'utilisateur (0=>le plus important) // Recherche des languages disponibles dans le repertoire $repLocale $arrLanguageAvailable = $this->languageTraductionsList($repLocale); $arrLanguageAvailable[] = $languageCode; $languageCode = ""; // Analyse pour donner la meilleure language possible foreach ($arrAccept as $value) { // Regarde si un repertoire existe avec la language proposee. // Recherche insensible à la casse, retourne le nom du fichier avec la // casse $val2 = strtolower($value); foreach ($arrLanguageAvailable as $val) { $val3 = strtolower($val); if ($val2 === $val3) { $languageCode = $val; break; } } // Regarde si un repertoire existe avec language en tant que base // (l'utilisateur demande fr et on a fr_FR.utf-8) foreach ($arrLanguageAvailable as $languageCodeAvailable) { if ($value === substr($languageCodeAvailable, 0, strlen($value))) { $languageCode = $languageCodeAvailable; break; } } // On a trouvé : on arrête de chercher if ($languageCode !== "") { break; } } // Si on n'a toujours pas trouve de language, c'est que la language par // defaut proposee en tete de fichier est inconnue : on met la language "C" // et le code s'affiche selon la programmation if ($languageCode === "") { $languageCode = "C"; } return ($languageCode); } /** * Return the language recorded in the Cookie. Check if this language is * @param string $cookieName The cookie name * @param string|null $repLocale The directory use to store the locale files * allowed * @return string The language allowed or FALSE */ public function languageCookie($cookieName, $repLocale = "./locale") { if (!isset($_COOKIE[$cookieName])) { return false; } $listeTranslations = $this->languageTraductionsList($repLocale); if ($listeTranslations === false) { return false; } if (in_array($_COOKIE[$cookieName], $listeTranslations, true)) { return $_COOKIE[$cookieName]; } return false; } /** * Set the cookie with a TTL of one month * @param string $cookieName The name of the cookie * @param string $languageCode Language to store * @param string $sitepath The site path */ public function languageCookieSet($cookieName, $languageCode, $sitepath) { @setcookie($cookieName, $languageCode, time() + 60 * 60 * 24 * 30, $sitepath); } /** * Return an array with all the languages available in the $repLocale dir * The languages are in the format 'en_US' without the codeset. * @param string|null $repLocale The directory use to store the locale files * Return FALSE if there is an error */ public function languageTraductionsList($repLocale = "./locale") { if (! is_dir($repLocale) || ! is_readable($repLocale)) { return false; } $list = glob("$repLocale/*"); foreach ($list as $key => $val) { $val = basename($val); $pos = strpos($val, "."); if ($pos === false) { $list[$key] = $val; } else { $list[$key] = substr($val, 0, $pos); } } sort($list); return $list; } /** * Return the full text of the category * Return false if it doesn't exists * @param string $category The category to analyze */ private function languageCategoryText($category) { $categories[LC_ALL] = "LC_ALL"; $categories[LC_COLLATE] = "LC_COLLATE"; $categories[LC_CTYPE] = "LC_CTYPE"; $categories[LC_MONETARY] = "LC_MONETARY"; $categories[LC_NUMERIC] = "LC_NUMERIC"; $categories[LC_TIME] = "LC_TIME"; $categories[LC_MESSAGES] = "LC_MESSAGES"; if (! isset($categories[$category])) { return false; } return $categories[$category]; } /** * This function manage the cache of $package.mo files as Apache cache them * It return the directory to use * @param string $languageCode Language with format "fr_FR" * @param string|null $package The package name of the soft ($package.mo * file). "messages" by default * @param string|null $category The folder name LC_MESSAGES by default * @param string|null $repLocale The folder where all the locales are stored */ public function languageCache( $languageCode, $package = "messages", $category = LC_MESSAGES, $repLocale = "./locale" ) { // Apache cache le fichier messages.mo jusqu'au prochain redémarrage. L'idée // est de créer un fichier temporaire basé sur le fichier normal. Du coup, // si on change le fichier temporaire, Apache recharge le cache et donne les // dernières traductions. // Cette fonction gère le cache et renvoie le nom du fichier temporaire // La ligne ci-dessous permet de récupérer la language sans le codeset si // il est fourni en_US.UTF8 -> en_US if (($pos = strpos($languageCode, ".")) !== false) { list($languageCode, $codeset) = explode(".", $languageCode); } $codeset = "UTF8"; // SANS TIRET ET EN MAJSUCULES!!! // -> Le répertoire de données doit être fr_FR.UTF8 $category = $this->languageCategoryText($category); $temporaries = glob("$repLocale/$languageCode.$codeset/$category/*-*.mo"); $moFile = "$repLocale/$languageCode.$codeset/$category/$package.mo"; if (! file_exists($moFile)) { return ""; } $linkBase = $this->cacheDir . "/" . filemtime($moFile) . "/"; $linkEnd = "$languageCode.$codeset/$category/$package.mo"; $link = $linkBase . $linkEnd; clearstatcache(false, $moFile); // Manage the cache directory if (! file_exists(dirname($link))) { // Try to create the cache dir. If there is an error, return the official // moFile. Apache will need to be restarted @mkdir(dirname($link), 0777, true); } if ( is_dir(dirname($link)) && is_writeable(dirname($link)) && is_readable(dirname($link)) ) { // Manage the cache file if ( ! file_exists($link) || ! is_readable($link) || filemtime($moFile) > filemtime($link) ) { // Do not remove immediately the old files : they can be used by Apache $files = glob($this->cacheDir . "/ */" . $linkEnd); foreach ($files as $file) { unlink($file); // Remove the empty dirs. If not empty, do not display an error @rmdir(dirname($file)); @rmdir(dirname(dirname($file))); @rmdir(dirname(dirname(dirname($file)))); } copy($moFile, $link); chmod($link, 0666); } if (filemtime($moFile) <= filemtime($link)) { return $link; } } return $moFile; } /** * Start the Gettext support with the cached file .mo provided as parameter * @param string $moFile The .mo file * @param string $languageCode The language code to use * @param integer $category the LC_ type to use * @param string|null $repLocale The locale directory. Use "./locale" if not * provided */ public function languageActivation( $moFile, $languageCode, $category = LC_MESSAGES, $repLocale = "./locale" ) { // Active le support Gettext pour le fichier de language .mo caché fourni en // paramètre. // fichierMo : fichier messages-1354015006.mo // On ne vérifie pas la language fournie, elle doit correspondre à un // fichier de language (qui peut être récupéré par languageSelection ou // languageTraductionsList // Language doit être fourni sans codeset (fr_FR et PAS fr_FR.UTF8) if ($languageCode !== "C" && $moFile !== "") { $package = substr(basename($moFile), 0, -3); // La ligne ci-dessous permet de récupérer la language sans le codeset si // il est fourni en_US.UTF8 -> en_US if (($pos = strpos($languageCode, ".")) !== false) { list($languageCode, $codeset) = explode(".", $languageCode); } $codeset = "UTF8"; // SANS TIRET ET EN MAJSUCULES!!! // -> Le répertoire de données doit être fr_FR.UTF8 putenv('LANG=' . $languageCode . '.' . $codeset); putenv('LANGUAGE=' . $languageCode . '.' . $codeset); $GLOBALS["domframework"]["lang"] = $languageCode; bind_textdomain_codeset($package, "utf-8"); bindtextdomain($package, $repLocale); textdomain($package); $rc = setlocale(LC_MESSAGES, $languageCode . '.' . $codeset); if ($rc === false) { // Language non disponible sur le système // La liste des languages est affichée par 'locale -a' return false; } $rc = setlocale(LC_TIME, $languageCode . '.' . $codeset); return true; } return false; } /** * The complete stack of language selection * @param string $package The package name (package.(po|mo) files) * @param string $languageCookie The name of the cookie saved in the browser * @param string $forcedLanguage The name of a forced language * @return string The language with format fr_FR to be used */ public function activeLanguage( $package, $languageCookie, $forcedLanguage = null ) { // Prefered language in the browser $langNav = $this->languageSelection("./locale", "fr_FR"); // Language defined in the cookie $langCookie = $this->languageCookie($languageCookie); if ($forcedLanguage !== null) { $languageCode = $forcedLanguage; } elseif ($langCookie !== false) { $languageCode = $langCookie; // Update the already set cookie $this->languageCookieSet($languageCookie, $languageCode, "/"); } else { $languageCode = $langNav; } // Cache the domframework's .mo file too $dfFile = $this->languageCache( $languageCode, "domframework", LC_MESSAGES, __DIR__ . "/../locale" ); $dfDir = dirname(dirname(dirname($dfFile))); $this->languageActivation($dfFile, $languageCode, LC_MESSAGES, $dfDir); $languageCodeFichier = $this->languageCache($languageCode, $package); $languageCodeDir = dirname(dirname(dirname($languageCodeFichier))); $this->languageActivation( $languageCodeFichier, $languageCode, LC_MESSAGES, $languageCodeDir ); return $languageCode; } /** * Return the language name from the language * Ex. : $languageCode=fr_FR, return France * @param string $languageCode Language with format "fr_FR" * @return string Then language name */ public function languageName($languageCode) { switch ($languageCode) { case "fr_FR": return dgettext("domframework", "French"); case "en_US": return dgettext("domframework", "English (US)"); case "en_GB": return dgettext("domframework", "English (GB)"); default: throw new \Exception("No language available for '$languageCode'", 500); } } /** * Return the language subtag for the language * http://www.iana.org/assignments/language-subtag-registry/ * language-subtag-registry * @param string $languageCode The language code to convert */ public function languageSubTag($languageCode) { return str_replace("_", "-", $languageCode); } }