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
This commit is contained in:
Mark Stephens 2010-05-13 22:44:37 +00:00 committed by Sam Minnee
parent b9d3b1b04d
commit 8e1f2b645c
2 changed files with 131 additions and 8 deletions

View File

@ -1411,7 +1411,7 @@ class i18n extends Object {
// get current locale (either default or user preference) // get current locale (either default or user preference)
$locale = i18n::get_locale(); $locale = i18n::get_locale();
// parse $entity into its parts // parse $entity into its parts
$entityParts = explode('.',$entity); $entityParts = explode('.',$entity);
$realEntity = array_pop($entityParts); $realEntity = array_pop($entityParts);
@ -1746,25 +1746,38 @@ class i18n extends Object {
* Includes all available language files for a certain defined locale * 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 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(); $base = Director::baseFolder();
$topLevel = scandir($base); $topLevel = scandir($base);
foreach($topLevel as $module) { foreach($topLevel as $module) {
//$topLevel is the website root, some server is configurated not to allow excess website root's parent level // $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 websit root level for its lang folder, so we skip these 2 levels checking. // 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($module[0] == '.') continue;
if ( if (
is_dir("$base/$module") is_dir("$base/$module")
&& file_exists("$base/$module/_config.php") && file_exists("$base/$module/_config.php")
&& file_exists($file = "$base/$module/lang/$locale.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, * Given a class name (a "locale namespace"), will search for its module and, if available,
* will load the resources for the currently defined locale. * will load the resources for the currently defined locale.
@ -1810,6 +1823,92 @@ class i18n extends Object {
} }
echo "Language {$this->urlParams['ID']} successfully removed"; 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;
}
}
}
} }
?> ?>

View File

@ -209,6 +209,30 @@ class i18nTest extends SapphireTest {
$this->assertEquals('de_DE', i18n::get_locale_from_lang('de_DE')); $this->assertEquals('de_DE', i18n::get_locale_from_lang('de_DE'));
$this->assertEquals('xy_XY', i18n::get_locale_from_lang('xy')); $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 { class i18nTest_DataObject extends DataObject implements TestOnly {