<?php /** * The translation-relevant bits from our original minimalistic XHTML PHP based template system. * * @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> * @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no> * @package SimpleSAMLphp */ namespace SimpleSAML\Locale; class Translate { private $configuration = null; private $langtext = array(); /** * Associative array of dictionaries. */ private $dictionaries = array(); /** * The default dictionary. */ private $defaultDictionary = null; /** * Constructor * * @param \SimpleSAML_Configuration $configuration Configuration object * @param string|null $defaultDictionary The default dictionary where tags will come from. */ public function __construct(\SimpleSAML_Configuration $configuration, $defaultDictionary = null) { $this->configuration = $configuration; $this->language = new Language($configuration); if ($defaultDictionary !== null && substr($defaultDictionary, -4) === '.php') { // TODO: drop this entire if clause for 2.0 // for backwards compatibility - print warning $backtrace = debug_backtrace(); $where = $backtrace[0]['file'].':'.$backtrace[0]['line']; \SimpleSAML_Logger::warning( 'Deprecated use of new SimpleSAML\Locale\Translate(...) at '.$where. '. The last parameter is now a dictionary name, which should not end in ".php".' ); $this->defaultDictionary = substr($defaultDictionary, 0, -4); } else { $this->defaultDictionary = $defaultDictionary; } } /** * This method retrieves a dictionary with the name given. * * @param string $name The name of the dictionary, as the filename in the dictionary directory, without the * '.php' ending. * * @return array An associative array with the dictionary. */ private function getDictionary($name) { assert('is_string($name)'); if (!array_key_exists($name, $this->dictionaries)) { $sepPos = strpos($name, ':'); if ($sepPos !== false) { $module = substr($name, 0, $sepPos); $fileName = substr($name, $sepPos + 1); $dictDir = \SimpleSAML_Module::getModuleDir($module).'/dictionaries/'; } else { $dictDir = $this->configuration->getPathValue('dictionarydir', 'dictionaries/'); $fileName = $name; } $this->dictionaries[$name] = $this->readDictionaryFile($dictDir.$fileName); } return $this->dictionaries[$name]; } /** * This method retrieves a tag as an array with language => string mappings. * * @param string $tag The tag name. The tag name can also be on the form '{<dictionary>:<tag>}', to retrieve a tag * from the specific dictionary. * * @return array An associative array with language => string mappings, or null if the tag wasn't found. */ public function getTag($tag) { assert('is_string($tag)'); // first check translations loaded by the includeInlineTranslation and includeLanguageFile methods if (array_key_exists($tag, $this->langtext)) { return $this->langtext[$tag]; } // check whether we should use the default dictionary or a dictionary specified in the tag if (substr($tag, 0, 1) === '{' && preg_match('/^{((?:\w+:)?\w+?):(.*)}$/D', $tag, $matches)) { $dictionary = $matches[1]; $tag = $matches[2]; } else { $dictionary = $this->defaultDictionary; if ($dictionary === null) { // we don't have any dictionary to load the tag from return null; } } $dictionary = $this->getDictionary($dictionary); if (!array_key_exists($tag, $dictionary)) { return null; } return $dictionary[$tag]; } /** * Retrieve the preferred translation of a given text. * * @param array $translations The translations, as an associative array with language => text mappings. * * @return string The preferred translation. * * @throws \Exception If there's no suitable translation. */ public function getPreferredTranslation($translations) { assert('is_array($translations)'); // look up translation of tag in the selected language $selected_language = $this->language->getLanguage(); if (array_key_exists($selected_language, $translations)) { return $translations[$selected_language]; } // look up translation of tag in the default language $default_language = $this->language->getDefaultLanguage(); if (array_key_exists($default_language, $translations)) { return $translations[$default_language]; } // check for english translation if (array_key_exists('en', $translations)) { return $translations['en']; } // pick the first translation available if (count($translations) > 0) { $languages = array_keys($translations); return $translations[$languages[0]]; } // we don't have anything to return throw new \Exception('Nothing to return from translation.'); } /** * Translate the name of an attribute. * * @param string $name The attribute name. * * @return string The translated attribute name, or the original attribute name if no translation was found. */ public function getAttributeTranslation($name) { // normalize attribute name $normName = strtolower($name); $normName = str_replace(":", "_", $normName); // check for an extra dictionary $extraDict = $this->configuration->getString('attributes.extradictionary', null); if ($extraDict !== null) { $dict = $this->getDictionary($extraDict); if (array_key_exists($normName, $dict)) { return $this->getPreferredTranslation($dict[$normName]); } } // search the default attribute dictionary $dict = $this->getDictionary('attributes'); if (array_key_exists('attribute_'.$normName, $dict)) { return $this->getPreferredTranslation($dict['attribute_'.$normName]); } // no translations found return $name; } /** * Translate a tag into the current language, with a fallback to english. * * This function is used to look up a translation tag in dictionaries, and return the translation into the current * language. If no translation into the current language can be found, english will be tried, and if that fails, * placeholder text will be returned. * * An array can be passed as the tag. In that case, the array will be assumed to be on the form (language => text), * and will be used as the source of translations. * * This function can also do replacements into the translated tag. It will search the translated tag for the keys * provided in $replacements, and replace any found occurrences with the value of the key. * * @param string|array $tag A tag name for the translation which should be looked up, or an array with * (language => text) mappings. * @param array $replacements An associative array of keys that should be replaced with values in the * translated string. * @param boolean $fallbackdefault Default translation to use as a fallback if no valid translation was found. * * @return string The translated tag, or a placeholder value if the tag wasn't found. */ public function t( $tag, $replacements = array(), $fallbackdefault = true, $oldreplacements = array(), // TODO: remove this for 2.0 $striptags = false // TODO: remove this for 2.0 ) { if (!is_array($replacements)) { // TODO: remove this entire if for 2.0 // old style call to t(...). Print warning to log $backtrace = debug_backtrace(); $where = $backtrace[0]['file'].':'.$backtrace[0]['line']; \SimpleSAML_Logger::warning( 'Deprecated use of SimpleSAML_Template::t(...) at '.$where. '. Please update the code to use the new style of parameters.' ); // for backwards compatibility if (!$replacements && $this->getTag($tag) === null) { \SimpleSAML_Logger::warning( 'Code which uses $fallbackdefault === FALSE should be updated to use the getTag() method instead.' ); return null; } $replacements = $oldreplacements; } if (is_array($tag)) { $tagData = $tag; } else { $tagData = $this->getTag($tag); if ($tagData === null) { // tag not found \SimpleSAML_Logger::info('Template: Looking up ['.$tag.']: not translated at all.'); return $this->getStringNotTranslated($tag, $fallbackdefault); } } $translated = $this->getPreferredTranslation($tagData); foreach ($replacements as $k => $v) { // try to translate if no replacement is given if ($v == null) { $v = $this->t($k); } $translated = str_replace($k, $v, $translated); } return $translated; } /** * Return the string that should be used when no translation was found. * * @param string $tag A name tag of the string that should be returned. * @param boolean $fallbacktag If set to true and string was not found in any languages, return the tag itself. If * false return null. * * @return string The string that should be used, or the tag name if $fallbacktag is set to false. */ private function getStringNotTranslated($tag, $fallbacktag) { if ($fallbacktag) { return 'not translated ('.$tag.')'; } else { return $tag; } } /** * Include a translation inline instead of putting translations in dictionaries. This function is recommended to be * used ONLU from variable data, or when the translation is already provided by an external source, as a database * or in metadata. * * @param string $tag The tag that has a translation * @param array|string $translation The translation array * * @throws \Exception If $translation is neither a string nor an array. */ public function includeInlineTranslation($tag, $translation) { if (is_string($translation)) { $translation = array('en' => $translation); } elseif (!is_array($translation)) { throw new \Exception("Inline translation should be string or array. Is ".gettype($translation)." now!"); } \SimpleSAML_Logger::debug('Template: Adding inline language translation for tag ['.$tag.']'); $this->langtext[$tag] = $translation; } /** * Include a language file from the dictionaries directory. * * @param string $file File name of dictionary to include * @param \SimpleSAML_Configuration|null $otherConfig Optionally provide a different configuration object than the * one provided in the constructor to be used to find the directory of the dictionary. This allows to combine * dictionaries inside the SimpleSAMLphp main code distribution together with external dictionaries. Defaults to * null. */ public function includeLanguageFile($file, $otherConfig = null) { $filebase = null; if (!empty($otherConfig)) { $filebase = $otherConfig->getPathValue('dictionarydir', 'dictionaries/'); } else { $filebase = $this->configuration->getPathValue('dictionarydir', 'dictionaries/'); } $lang = $this->readDictionaryFile($filebase.$file); \SimpleSAML_Logger::debug('Template: Merging language array. Loading ['.$file.']'); $this->langtext = array_merge($this->langtext, $lang); } /** * Read a dictionary file in JSON format. * * @param string $filename The absolute path to the dictionary file, minus the .definition.json ending. * * @return array An array holding all the translations in the file. */ private function readDictionaryJSON($filename) { $definitionFile = $filename.'.definition.json'; assert('file_exists($definitionFile)'); $fileContent = file_get_contents($definitionFile); $lang = json_decode($fileContent, true); if (empty($lang)) { \SimpleSAML_Logger::error('Invalid dictionary definition file ['.$definitionFile.']'); return array(); } $translationFile = $filename.'.translation.json'; if (file_exists($translationFile)) { $fileContent = file_get_contents($translationFile); $moreTrans = json_decode($fileContent, true); if (!empty($moreTrans)) { $lang = array_merge_recursive($lang, $moreTrans); } } return $lang; } /** * Read a dictionary file in PHP format. * * @param string $filename The absolute path to the dictionary file. * * @return array An array holding all the translations in the file. */ private function readDictionaryPHP($filename) { $phpFile = $filename.'.php'; assert('file_exists($phpFile)'); $lang = null; include($phpFile); if (isset($lang)) { return $lang; } return array(); } /** * Read a dictionary file. * * @param string $filename The absolute path to the dictionary file. * * @return array An array holding all the translations in the file. */ private function readDictionaryFile($filename) { assert('is_string($filename)'); \SimpleSAML_Logger::debug('Template: Reading ['.$filename.']'); $jsonFile = $filename.'.definition.json'; if (file_exists($jsonFile)) { return $this->readDictionaryJSON($filename); } $phpFile = $filename.'.php'; if (file_exists($phpFile)) { return $this->readDictionaryPHP($filename); } \SimpleSAML_Logger::error( $_SERVER['PHP_SELF'].' - Template: Could not find dictionary file at ['.$filename.']' ); return array(); } }