From 8e1f2b645cdb67dcaf55f59f36535454110ecdf2 Mon Sep 17 00:00:00 2001 From: Mark Stephens Date: Thu, 13 May 2010 22:44:37 +0000 Subject: [PATCH] ENHANCEMENT: added plugins to i18n to support modules that provide custom translations. git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/branches/2.4@104827 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- core/i18n.php | 115 +++++++++++++++++++++++++++++++++++++--- tests/i18n/i18nTest.php | 24 +++++++++ 2 files changed, 131 insertions(+), 8 deletions(-) diff --git a/core/i18n.php b/core/i18n.php index 6e94dd9ce..69fbd32cc 100755 --- a/core/i18n.php +++ b/core/i18n.php @@ -1411,7 +1411,7 @@ class i18n extends Object { // get current locale (either default or user preference) $locale = i18n::get_locale(); - + // parse $entity into its parts $entityParts = explode('.',$entity); $realEntity = array_pop($entityParts); @@ -1746,25 +1746,38 @@ class i18n extends Object { * Includes all available language files for a certain defined locale * * @param string $locale All resources from any module in locale $locale will be loaded + * @param boolean $load_plugins If true (default), load extra translations from registered plugins + * @param boolean $force_load If true (not default), we force the inclusion. Generally this should be off + * for performance, but enabling this is useful for interfaces like + * CustomTranslationAdmin which need to load more than the usual locales, + * and may need to reload them. */ - static function include_by_locale($locale) { + static function include_by_locale($locale, $load_plugins = true, $force_load = false) { + global $lang; + $base = Director::baseFolder(); $topLevel = scandir($base); + foreach($topLevel as $module) { - //$topLevel is the website root, some server is configurated not to allow excess website root's parent level - //and we don't need to check website root's parent level and websit root level for its lang folder, so we skip these 2 levels checking. + // $topLevel is the website root, some servers are configured not to allow excess website root's parent level + // and we don't need to check website root's parent level and website root level for its lang folder, so + // we skip these 2 levels checking. if($module[0] == '.') continue; if ( is_dir("$base/$module") && file_exists("$base/$module/_config.php") && file_exists($file = "$base/$module/lang/$locale.php") - ) { - include_once($file); + ) { + if ($force_load) include($file); + else include_once($file); } } + + // Finally, load any translations from registered plugins + if ($load_plugins) self::plugins_load($locale); } - + /** * Given a class name (a "locale namespace"), will search for its module and, if available, * will load the resources for the currently defined locale. @@ -1810,6 +1823,92 @@ class i18n extends Object { } echo "Language {$this->urlParams['ID']} successfully removed"; } - + + /** + * This variable holds translation plugins that are invoked on a call to _t. It is a two dimensional array, + * priority the first dimension and name the second, mapping to the callback. + * Translations from lower priority plugins are used first, and callback is a callback for call_user_func_array. + * + * Callback functions are passed one parameter: + * - locale string + * The callback function should return an array that can be merged with $lang[$locale], overriding values read + * from the language file. + * + * @var array + */ + private static $plugins = array(); + + /** + * Register a named translation plug-in function. + * Plug-ins are assumed to be registered before any call to _t. If registered after a call to _t + * for a given local, it will not be called. + * @static + * @throws Exception + * @param $name String A unique name for the translation plug-in. If the plug-in is already registered, + * it is replaced, including if its a different priority. + * @param $callback A callback function as given to call_user_func_array. + * @param int $priority An integer priority, default 10. + * @return void + */ + static function register_plugin($name, $callback, $priority = 10) { + // Validate + if (!is_int($priority)) throw new Exception("register_plugin expects an int priority"); + + // Ensure it's not there. If it is, we're replacing it. It may exist in a different priority. + self::unregister_plugin($name); + + // Add it. + self::$plugins[$priority][$name] = $callback; + } + + /** + * Unregister a plugin by name. + * @static + * @param $name String Name of previously registered plugin + * @return Boolean Returns true if remove, false if not. + */ + static function unregister_plugin($name) { + foreach (self::$plugins as $priority => $plugins) { + if (isset($plugins[$name])) unset(self::$plugins[$priority][$name]); + } + } + + /** + * Load any translations from registered plugins. Merges them directly into $lang. + * @static + * @param $local + * @param $value + * @return void + */ + static function plugins_load($locale) { + // sort the plugins by lowest priority (highest value) first, as each one replaces translations of the provider + // before it. + krsort(self::$plugins); + foreach (self::$plugins as $priority => $plugins) { + foreach ($plugins as $name => $callback) { + self::merge_locale_data($locale, call_user_func_array($callback, array($locale))); + } + } + } + + /** + * Merge an extra of language translations into $lang[$locale]. We'd use array_merge_recursive, except + * it doesn't work for translations that specify priorities and comments, because they are indexed by number. + * @static + * @param $locale String The locale we are merging into + * @param $extra Array An array of [locale][class][entity]=> translation, keyed on entity, that are to be + * merged for this locale. + * @return void + */ + static function merge_locale_data($locale, $extra) { + global $lang; + if (!$extra || count($extra) == 0) return; + foreach ($extra[$locale] as $class => $entities) { + foreach ($entities as $entity => $translation) { + $lang[$locale][$class][$entity] = $translation; + } + } + } } + ?> diff --git a/tests/i18n/i18nTest.php b/tests/i18n/i18nTest.php index 950ba021e..54801c14b 100644 --- a/tests/i18n/i18nTest.php +++ b/tests/i18n/i18nTest.php @@ -209,6 +209,30 @@ class i18nTest extends SapphireTest { $this->assertEquals('de_DE', i18n::get_locale_from_lang('de_DE')); $this->assertEquals('xy_XY', i18n::get_locale_from_lang('xy')); } + + function testRegisteredPlugin() { + global $lang; + + $lang = array(); // clear translations + i18n::register_plugin("testPlugin", array("i18nTest", "translationTestPlugin")); + + // We have to simulate what include_by_locale() does, including loading translation provider data. + $lang['en_US']["i18nTestProvider"]["foo"] = "bar_en"; + $lang['de_DE']["i18nTestProvider"]["foo"] = "bar_de"; + i18n::plugins_load('en_US'); + + i18n::set_locale('en_US'); + $this->assertEquals(_t("i18nTestProvider.foo"), "baz_en"); + i18n::set_locale('de_DE'); + $this->assertEquals(_t("i18nTestProvider.foo"), "bar_de"); + i18n::unregister_plugin("testTranslator"); + } + + static function translationTestPlugin($locale) { + $result = array(); + $result["en_US"]["i18nTestProvider"]["foo"] = "baz_en"; + return $result; + } } class i18nTest_DataObject extends DataObject implements TestOnly {