API CHANGE Using Zend_Translate with YAML translation files, replacing the $lang global and PHP files in the /lang folders.

This commit is contained in:
Ingo Schommer 2011-11-26 14:34:11 +01:00
parent 0a0be63ee2
commit bd23a07bba
26 changed files with 775 additions and 256 deletions

View File

@ -178,7 +178,7 @@ class SS_ConfigManifest {
// We use Symfony Yaml since it's the most complete. It still doesn't handle all of YAML, but it's better than
// nothing.
require_once 'thirdparty/symfony-yaml/lib/sfYamlParser.php';
if(!class_exists('sfYamlParser', false)) require_once 'thirdparty/symfony-yaml/lib/sfYamlParser.php';
$parser = new sfYamlParser();
// The base header

View File

@ -1,4 +1,7 @@
<?php
require_once 'Zend/Translate.php';
require_once 'i18nRailsYamlAdapter.php';
/**
* Base-class for storage and retrieval of translated entities.
*
@ -84,6 +87,11 @@ class i18n extends Object implements TemplateGlobalProvider {
*/
protected static $time_format;
/**
* @var array Array of priority keys to instances of Zend_Translate, mapped by name.
*/
protected static $translators;
/**
* Use javascript i18n through the ss.i18n class (enabled by default).
* If set to TRUE, includes javascript requirements for the base library
@ -1452,24 +1460,98 @@ class i18n extends Object implements TemplateGlobalProvider {
* @return string The translated string, according to the currently set locale {@link i18n::set_locale()}
*/
static function _t($entity, $string = "", $priority = 40, $context = "") {
global $lang;
foreach(self::get_translators() as $priority => $translators) {
foreach($translators as $name => $translator) {
$adapter = $translator->getAdapter();
// get current locale (either default or user preference)
$locale = i18n::get_locale();
$adapter->setLocale($locale);
// if language table isn't loaded for this locale, get it for each of the modules
if(!$adapter->isAvailable($locale)) i18n::include_by_locale($locale);
$translation = $adapter->translate($entity, $locale);
// Return translation only if we found a match thats not the entity itself (Zend fallback)
if($translation && $translation != $entity) return $translation;
}
}
// get current locale (either default or user preference)
$locale = i18n::get_locale();
// Fall back to default string argument
return $string;
}
// parse $entity into its parts
$entityParts = explode('.',$entity);
$realEntity = array_pop($entityParts);
$class = implode('.',$entityParts);
/**
* @return array Array of priority keys to instances of Zend_Translate, mapped by name.
*/
static function get_translators() {
if(!self::$translators) {
Zend_Translate::setCache(
SS_Cache::factory('i18n', 'Output', array('lifetime' => -1, 'automatic_serialization' => true))
);
$defaultPriority = 10;
self::$translators[$defaultPriority] = array(
'core' => new Zend_Translate(array(
'adapter' => 'i18nRailsYamlAdapter',
'locale' => self::$default_locale,
'disableNotices' => true,
))
);
self::$translators[$defaultPriority-1] = array(
'legacy' => new Zend_Translate(array(
'adapter' => 'i18nSSLegacyAdapter',
'locale' => self::$default_locale,
'disableNotices' => true,
))
);
i18n::include_by_locale('en_US');
}
// if language table isn't loaded for this locale, get it for each of the modules
if(!isset($lang[$locale])) i18n::include_by_locale($locale);
return self::$translators;
}
/**
* @param String
* @return Zend_Translate
*/
static function get_translator($name) {
foreach(self::get_translators() as $priority => $translators) {
if(isset($translators[$name])) return $translators[$name];
}
return false;
}
/**
* @param Zend_Translate Needs to implement {@link i18nTranslateAdapterInterface}
* @param String If left blank will override the default translator.
* @param Int
*/
static function register_translator($translator, $name, $priority = 10) {
if (!is_int($priority)) throw new InvalidArgumentException("register_translator expects an int priority");
// fallback to the passed $string if no translation is present
$transEntity = isset($lang[$locale][$class][$realEntity]) ? $lang[$locale][$class][$realEntity] : $string;
// Ensure it's not there. If it is, we're replacing it. It may exist in a different priority.
self::unregister_translator($name);
// entities can be stored in both array and literal values in the language tables
return (is_array($transEntity) ? $transEntity[0] : $transEntity);
// Add our new translator
if(!isset(self::$translators[$priority])) self::$translators[$priority] = array();
self::$translators[$priority][$name] = $translator;
// Resort array, ensuring highest priority comes first
krsort(self::$translators);
i18n::include_by_locale('en_US');
}
/**
* @param String
*/
static function unregister_translator($name) {
foreach (self::get_translators() as $priority => $translators) {
if (isset($translators[$name])) unset(self::$translators[$priority][$name]);
}
}
/**
@ -1517,31 +1599,41 @@ class i18n extends Object implements TemplateGlobalProvider {
* @return array
*/
static function get_existing_translations() {
$locales = array();
$localeWithTitles = array();
$baseDir = Director::baseFolder();
$modules = scandir($baseDir);
foreach($modules as $module) {
if($module[0] == '.') continue;
$moduleDir = $baseDir . DIRECTORY_SEPARATOR . $module;
$langDir = $moduleDir . DIRECTORY_SEPARATOR . "lang";
if(is_dir($moduleDir) && is_file($moduleDir . DIRECTORY_SEPARATOR . "_config.php") && is_dir($langDir)) {
$moduleLocales = scandir($langDir);
foreach($moduleLocales as $moduleLocale) {
if(preg_match('/(.*)\.php$/',$moduleLocale, $matches)) {
if(isset($matches[1]) && isset(self::$all_locales[$matches[1]])) {
$locales[$matches[1]] = self::$all_locales[$matches[1]];
}
}
}
foreach(self::get_translators() as $priority => $translators) {
foreach($translators as $name => $translator) {
$adapter = $translator->getAdapter();
// TODO Inspect themes
$modules = SS_ClassLoader::instance()->getManifest()->getModules();
foreach($modules as $module) {
if(!file_exists("{$module}/lang/")) continue;
$adapter->addTranslation(array(
'content' => "{$module}/lang/",
'scan' => Zend_Translate_Adapter::LOCALE_FILENAME,
// TODO Support custom translators with their own file extensions
'ignore' => array(
'.',
'_manifest_exclude',
'regex' => '/^.*\.(?!yml).*$/i'
)
));
}
$locales = $adapter->getList();
foreach($locales as $locale) {
// Normalize locale to include likely region tag.
// TODO Replace with CLDR list of actually available languages/regions
$locale = self::get_locale_from_lang($locale);
$localeWithTitles[$locale] = (@self::$all_locales[$locale]) ? self::$all_locales[$locale] : $locale;
}
}
}
// sort by title (not locale)
asort($locales);
asort($localeWithTitles);
return $locales;
return $localeWithTitles;
}
/**
@ -1729,8 +1821,6 @@ class i18n extends Object implements TemplateGlobalProvider {
* @param string $locale Locale to be set. See http://unicode.org/cldr/data/diff/supplemental/languages_and_territories.html for a list of possible locales.
*/
static function set_locale($locale) {
if(!self::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale));
if ($locale) self::$current_locale = $locale;
}
@ -1767,77 +1857,78 @@ class i18n extends Object implements TemplateGlobalProvider {
* @param String $locale
*/
static function set_default_locale($locale) {
if(!self::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale));
self::$default_locale = $locale;
}
/**
* Include a locale file determined by module name and locale
*
* @deprecated 3.0 Use Zend_Translate instead
*
* @param string $module Module that contains the locale file
* @param string $locale Locale to be loaded
*/
static function include_locale_file($module, $locale) {
if(!self::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale));
Deprecation::notice('3.0', 'Use Zend_Translate instead.');
if (file_exists($file = Director::getAbsFile("$module/lang/$locale.php"))) include_once($file);
}
/**
* Includes all available language files for a certain defined locale
* Includes all available language files for a certain defined locale.
* If the locale is a fully qualified locale (e.g. "en_US" rather than "en"),
* will load the base locale file as well (if available).
*
* @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, $load_plugins = true, $force_load = false) {
if(!self::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale));
static function include_by_locale($locale) {
$lang = i18n::get_lang_from_locale($locale);
global $lang;
// Automatically include fallback language (if applicable)
// TODO Also include custom Zend_Translate routing languages
$selectedLocales = array_unique(array($lang, $locale));
$base = Director::baseFolder();
$topLevel = scandir($base);
// Loop in reverse order, meaning the translator with the highest priority goes first
$translators = array_reverse(self::get_translators(), true);
foreach($translators as $priority => $translators) {
foreach($translators as $name => $translator) {
$adapter = $translator->getAdapter();
$modules = SS_ClassLoader::instance()->getManifest()->getModules();
foreach($topLevel as $module) {
// $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;
// Load translations from modules
foreach($modules as $module) {
foreach($selectedLocales as $selectedLocale) {
$filename = $adapter->getFilenameForLocale($selectedLocale);
$filepath = "{$module}/lang/" . $filename;
if($filename && !file_exists($filepath)) continue;
$adapter->addTranslation(
array('content' => $filepath, 'locale' => $selectedLocale)
);
}
}
if (
is_dir("$base/$module")
&& file_exists("$base/$module/_config.php")
&& file_exists($file = "$base/$module/lang/$locale.php")
) {
if ($force_load) include($file);
else include_once($file);
}
}
// Load translations from themes
$themesBase = $base . '/themes';
if(is_dir($themesBase) && SSViewer::current_theme()) {
foreach(scandir($themesBase) as $theme) {
if(
strpos($theme, SSViewer::current_theme()) === 0
&& file_exists($file = "$themesBase/$theme/lang/$locale.php")
) {
if ($force_load) include($file);
else include_once($file);
// Load translations from themes
// TODO Replace with theme listing once implemented in TemplateManifest
$themesBase = Director::baseFolder() . '/themes';
if(is_dir($themesBase)) {
foreach(scandir($themesBase) as $theme) {
if(
strpos($theme, SSViewer::current_theme()) === 0
&& file_exists("{$themesBase}/{$theme}/lang/")
) {
foreach($selectedLocales as $selectedLocale) {
$filename = $adapter->getFilenameForLocale($selectedLocale);
$filepath = "{$themesBase}/{$theme}/lang/" . $filename;
if($filename && !file_exists($filepath)) continue;
$adapter->addTranslation(
array('content' => $filepath, 'locale' => $selectedLocale)
);
}
}
}
}
}
}
// Load any translations from registered plugins
if ($load_plugins) self::plugins_load($locale);
// Make sure this is only done once. We don't want to attempt it hundreds of times for missing locals
if(!isset($lang[$locale])) $lang[$locale] = array();
}
/**
@ -1849,113 +1940,18 @@ class i18n extends Object implements TemplateGlobalProvider {
*/
static function include_by_class($class) {
$module = self::get_owner_module($class);
if(!$module) user_error("i18n::include_by_class: Class {$class} not found", E_USER_WARNING);
$locale = self::get_locale();
if (file_exists($file = Director::getAbsFile("$module/lang/". self::get_locale() . '.php'))) {
include($file);
} else if (self::get_locale() != self::$default_locale) {
$old = self::get_locale();
self::set_locale(self::$default_locale);
self::include_by_class($class);
self::set_locale($old);
} else if(file_exists(Director::getAbsFile("$module/lang"))) {
user_error("i18n::include_by_class: Locale file $file should exist", E_USER_WARNING);
}
// If the language file wasn't included for this class, include an empty array to prevent
// this method from being called again
global $lang;
if(!isset($lang[$locale][$class])) $lang[$locale][$class] = array();
}
//-----------------------------------------------------------------------------------------------//
/**
* 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 || !isset($extra[$locale])) return;
foreach ($extra[$locale] as $class => $entities) {
foreach ($entities as $entity => $translation) {
$lang[$locale][$class][$entity] = $translation;
$translators = array_reverse(self::get_translators(), true);
foreach($translators as $priority => $translators) {
foreach($translators as $name => $translator) {
$adapter = $translator->getAdapter();
$filename = $adapter->getFilenameForLocale(self::get_locale());
$filepath = "{$module}/lang/" . $filename;
if($filename && !file_exists($filepath)) continue;
$adapter->addTranslation(array(
'content' => $filepath,
'locale' => self::get_locale()
));
}
}
}
@ -1966,6 +1962,5 @@ class i18n extends Object implements TemplateGlobalProvider {
'get_locale',
);
}
}

View File

@ -0,0 +1,19 @@
<?php
require_once 'Zend/Translate.php';
require_once 'zend_translate_railsyaml/library/Translate/Adapter/RailsYaml.php';
/**
* @package sapphire
* @subpackage i18n
*/
class i18nRailsYamlAdapter extends Translate_Adapter_RailsYaml implements i18nTranslateAdapterInterface {
/**
* @param String
* @return String
*/
public function getFilenameForLocale($locale) {
return "$locale.yml";
}
}

View File

@ -0,0 +1,87 @@
<?php
require_once 'Zend/Locale.php';
require_once 'Zend/Translate/Adapter.php';
/**
* @package sapphire
* @subpackage i18n
*/
class i18nSSLegacyAdapter extends Zend_Translate_Adapter implements i18nTranslateAdapterInterface {
/**
* Generates the adapter
*
* @param array|Zend_Config $options Translation content
*/
public function __construct($options = array()) {
$this->_options['keyDelimiter'] = ".";
parent::__construct($options);
}
protected function _loadTranslationData($filename, $locale, array $options = array()) {
$options = array_merge($this->_options, $options);
if ($options['clear'] || !isset($this->_translate[$locale])) {
$this->_translate[$locale] = array();
}
$this->_filename = $filename;
// Ignore files with other extensions
if(pathinfo($this->_filename, PATHINFO_EXTENSION) != 'php') return;
if (!is_readable($this->_filename)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Error opening translation file \'' . $filename . '\'.');
}
global $lang;
if(!isset($lang['en_US'])) $lang['en_US'] = array();
// TODO Diff locale array to avoid re-parsing all previous translations whenever a new module is included.
require_once($this->_filename);
$flattened = array();
if($lang[$locale]) {
$iterator = new i18nSSLegacyAdapter_Iterator(new RecursiveArrayIterator($lang[$locale]));
foreach($iterator as $k => $v) {
$flattenedKey = implode($options['keyDelimiter'], array_filter($iterator->getKeyStack()));
$flattened[$flattenedKey] = (is_array($v)) ? $v[0] : $v;
}
}
return array($locale => $flattened);
}
public function toString() {
return "i18nSSLegacy";
}
function getFilenameForLocale($locale) {
return "{$locale}.php";
}
}
class i18nSSLegacyAdapter_Iterator extends RecursiveIteratorIterator {
protected $keyStack = array();
public function callGetChildren() {
$this->keyStack[] = parent::key();
return parent::callGetChildren();
}
public function endChildren() {
array_pop($this->keyStack);
parent::endChildren();
}
public function key() {
return json_encode($this->getKeyStack());
}
public function getKeyStack() {
return array_merge($this->keyStack, array(parent::key()));
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* @package sapphire
* @subpackage i18n
*/
/**
* Makes the {@link Zend_Translate_Adapter} base class aware of file naming conventions within SilverStripe.
* Needs to be implemented by all translators used through {@link i18n::register_translator()}.
*
* A bit of context: Zend is file extension agnostic by default, and simply uses the filenames to detect locales
* with the 'scan' option, passing all files to the used adapter. We support multiple formats in the same /lang/
* folder, so need to be more selective about including files to avoid e.g. a YAML adapter trying to parse a PHP file.
*
* @see http://framework.zend.com/manual/en/zend.translate.additional.html#zend.translate.additional.combination
*/
interface i18nTranslateAdapterInterface {
/**
* @param String
* @return String
*/
public function getFilenameForLocale($locale);
}

View File

@ -131,7 +131,7 @@ he-IL:
GREETING: "ברוך הבא"
INTERFACELANG: "שפת ממשק"
LOGGEDINAS: |
אתה מחובר כ %s.
אתה מחובר כ %s.
MOBILE: "נייד"
NAME: "שם"
NEWPASSWORD: "סיסמא חדשה"
@ -145,7 +145,7 @@ he-IL:
SURNAME: "שם משפחה"
VALIDATIONMEMBEREXISTS: "קיים כבר משתמש עם כתובת דואר זו."
WELCOMEBACK: |
ברוך הבא, %s
ברוך הבא, %s
YOUROLDPASSWORD: "הסיסמא הישנה שלך"
belongs_many_many_Groups: "קבוצות"
db_LockedOutUntil: "נעול עד "
@ -205,11 +205,6 @@ he-IL:
LOSTPASSWORDHEADER: "איבדת סיסמא"
NOTEPAGESECURED: "עמוד זה אינו מאובטח. הכנס את הפרטים שלך להלן ונשלח אליך מייד."
NOTERESETPASSWORD: "הכנס את כתובת הדואר האלקטרוני שלך ונשלח אליך קישור שבעזרתו תוכל לאפס את הסיסמא שלך"
PASSWORDSENTHEADER: |
קישור לאיפוס סיסמא נשלח ל '%s'
PASSWORDSENTTEXT: |
תודה רבה! קישור לאיפוס הסיסמא נשלח ל '%s'.
SecurityAdmin:
ADDMEMBER: "הוסף חבר"
EDITPERMISSIONS: "ערוך הרשאות וכתובות IP לכל קבוצה"
MENUTITLE: "אבטחה"

View File

@ -0,0 +1,4 @@
de:
i18nOtherModule:
ENTITY: Other Module Entity (de)
MAINTEMPLATE: Main Template Other Module (de)

View File

@ -0,0 +1,13 @@
<?php
i18n::include_locale_file('i18nothermodule', 'en_US');
global $lang;
if(array_key_exists('de_DE', $lang) && is_array($lang['de_DE'])) {
$lang['de_DE'] = array_merge($lang['en_US'], $lang['de_DE']);
} else {
$lang['de_DE'] = $lang['en_US'];
}
$lang['de_DE']['i18nOtherModule']['LEGACY'] = 'Legacy translation (de_DE)';

View File

@ -0,0 +1,4 @@
en:
i18nOtherModule:
ENTITY: Other Module Entity
MAINTEMPLATE: Main Template Other Module

View File

@ -0,0 +1,5 @@
<?php
global $lang;
$lang['en_US']['i18nOtherModule']['LEGACY'] = 'Legacy translation';

View File

@ -0,0 +1,3 @@
de:
i18nTestModule:
OTHERENTITY: Other Entity (de)

View File

@ -0,0 +1,3 @@
en:
i18nTestModule:
OTHERENTITY: Other Entity

View File

@ -0,0 +1,15 @@
de:
NONAMESPACE: Include Entity without Namespace (de)
SPRINTFNONAMESPACE: My replacement no namespace: %s (de)
SPRINTFINCLUDENONAMESPACE: My include replacement no namespace: %s (de)
LAYOUTTEMPLATENONAMESPACE: Layout Template no namespace (de)
i18nTestModule:
# Comment for entity
ENTITY: Entity with "Double Quotes" (de)
ADDITION: Addition (de)
MAINTEMPLATE: Main Template (de)
WITHNAMESPACE: Include Entity with Namespace (de)
LAYOUTTEMPLATE: Layout Template (de)
SPRINTFNAMESPACE: My replacement: %s (de)
i18nTestModuleInclude.ss:
SPRINTFINCLUDENAMESPACE: My include replacement: %s (de)

View File

@ -0,0 +1,3 @@
de_AT:
i18nTestModule:
ENTITY: Entity with "Double Quotes" (de_AT)

View File

@ -0,0 +1,15 @@
en:
NONAMESPACE: Include Entity without Namespace
SPRINTFNONAMESPACE: My replacement no namespace: %s
SPRINTFINCLUDENONAMESPACE: My include replacement no namespace: %s
LAYOUTTEMPLATENONAMESPACE: Layout Template no namespace
i18nTestModule:
# Comment for entity
ENTITY: Entity with "Double Quotes"
ADDITION: Addition
MAINTEMPLATE: Main Template
WITHNAMESPACE: Include Entity with Namespace
LAYOUTTEMPLATE: Layout Template
SPRINTFNAMESPACE: My replacement: %s
i18nTestModuleInclude.ss:
SPRINTFINCLUDENAMESPACE: My include replacement: %s

View File

@ -0,0 +1,3 @@
fr:
i18nTestModule:
ENTITY: Entity with "Double Quotes" (fr)

View File

@ -0,0 +1,11 @@
de:
i18nTestTheme1:
LAYOUTTEMPLATE: Theme1 Layout Template (de)
SPRINTFNAMESPACE: Theme1 My replacement: %s (de)
i18nTestTheme1Include:
WITHNAMESPACE: Theme1 Include Entity with Namespace (de)
SPRINTFINCLUDENAMESPACE: Theme1 My include replacement: %s (de)
NONAMESPACE: Theme1 Include Entity without Namespace (de)
SPRINTFINCLUDENONAMESPACE: Theme1 My include replacement no namespace: %s (de)
LAYOUTTEMPLATENONAMESPACE: Theme1 Layout Template no namespace (de)
SPRINTFNONAMESPACE: Theme1 My replacement no namespace: %s (de)

View File

@ -0,0 +1,13 @@
<?php
i18n::include_locale_file('i18nothermodule', 'en_US');
global $lang;
if(array_key_exists('de_DE', $lang) && is_array($lang['de_DE'])) {
$lang['de_DE'] = array_merge($lang['en_US'], $lang['de_DE']);
} else {
$lang['de_DE'] = $lang['en_US'];
}
$lang['de_DE']['i18nOtherModule']['LEGACYTHEME'] = 'Legacy translation (de_DE)';

View File

@ -0,0 +1,11 @@
en:
i18nTestTheme1:
LAYOUTTEMPLATE: Theme1 Layout Template
SPRINTFNAMESPACE: Theme1 My replacement: %s
i18nTestTheme1Include:
WITHNAMESPACE: Theme1 Include Entity with Namespace
SPRINTFINCLUDENAMESPACE: Theme1 My include replacement: %s
NONAMESPACE: Theme1 Include Entity without Namespace
SPRINTFINCLUDENONAMESPACE: Theme1 My include replacement no namespace: %s
LAYOUTTEMPLATENONAMESPACE: Theme1 Layout Template no namespace
SPRINTFNONAMESPACE: Theme1 My replacement no namespace: %s

View File

@ -0,0 +1,5 @@
<?php
global $lang;
$lang['en_US']['i18nOtherModule']['LEGACYTHEME'] = 'Legacy translation';

View File

@ -0,0 +1,3 @@
de:
i18nTestTheme2:
MAINTEMPLATE: Theme2 Main Template (de)

View File

@ -0,0 +1,3 @@
en:
i18nTestTheme2:
MAINTEMPLATE: Theme2 Main Template

View File

@ -0,0 +1,90 @@
<?php
/**
* @package sapphire
* @subpackage i18n
*/
class i18nSSLegacyAdapterTest extends SapphireTest {
function setUp() {
parent::setUp();
$this->alternateBasePath = $this->getCurrentAbsolutePath() . "/_fakewebroot";
$this->alternateBaseSavePath = TEMP_FOLDER . '/i18nTextCollectorTest_webroot';
FileSystem::makeFolder($this->alternateBaseSavePath);
Director::setBaseFolder($this->alternateBasePath);
// Push a template loader running from the fake webroot onto the stack.
$templateManifest = new SS_TemplateManifest($this->alternateBasePath, false, true);
$templateManifest->regenerate(false);
SS_TemplateLoader::instance()->pushManifest($templateManifest);
$this->_oldTheme = SSViewer::current_theme();
SSViewer::set_theme('testtheme1');
$classManifest = new SS_ClassManifest($this->alternateBasePath, true, true, false);
SS_ClassLoader::instance()->pushManifest($classManifest);
$this->originalLocale = i18n::get_locale();
// Override default adapter to avoid cached translations between tests.
// Emulates behaviour in i18n::get_translators()
$this->origAdapter = i18n::get_translator('core');
$adapter = new Zend_Translate(array(
'adapter' => 'i18nRailsYamlAdapter',
'locale' => i18n::default_locale(),
'disableNotices' => true,
));
i18n::register_translator($adapter, 'core');
$adapter->removeCache();
i18n::include_by_locale('en');
}
function tearDown() {
SS_TemplateLoader::instance()->popManifest();
SS_ClassLoader::instance()->popManifest();
i18n::set_locale($this->originalLocale);
Director::setBaseFolder(null);
SSViewer::set_theme($this->_oldTheme);
i18n::register_translator($this->origAdapter, 'core');
parent::tearDown();
}
function testTranslate() {
i18n::set_locale('en_US');
$this->assertEquals(
'Legacy translation',
// defined in i18nothermodule/lang/en_US.php
i18n::_t('i18nOtherModule.LEGACY'),
'Finds original strings in PHP module files'
);
$this->assertEquals(
'Legacy translation',
// defined in themes/testtheme1/lang/en_US.php
i18n::_t('i18nOtherModule.LEGACYTHEME'),
'Finds original strings in theme files'
);
i18n::set_locale('de_DE');
$this->assertEquals(
'Legacy translation (de_DE)',
// defined in i18nothermodule/lang/de_DE.php
i18n::_t('i18nOtherModule.LEGACY'),
'Finds translations in PHP module files'
);
$this->assertEquals(
'Legacy translation (de_DE)',
// defined in themes/testtheme1/lang/de_DE.php
i18n::_t('i18nOtherModule.LEGACYTHEME'),
'Finds original strings in theme files'
);
// TODO Implement likely subtags solution
// i18n::set_locale('de');
// $this->assertEquals(
// 'Legacy translation (de_DE)',
// // defined in i18nothermodule/lang/de_DE.php
// i18n::_t('i18nOtherModule.LEGACY'),
// 'Finds translations in PHP module files if only language locale is set'
// );
}
}

View File

@ -1,4 +1,6 @@
<?php
require_once 'Zend/Translate.php';
/**
* @package framework
* @subpackage tests
@ -29,18 +31,36 @@ class i18nTest extends SapphireTest {
$this->alternateBasePath = $this->getCurrentAbsolutePath() . "/_fakewebroot";
$this->alternateBaseSavePath = TEMP_FOLDER . '/i18nTextCollectorTest_webroot';
FileSystem::makeFolder($this->alternateBaseSavePath);
Director::setBaseFolder($this->alternateBasePath);
// Push a template loader running from the fake webroot onto the stack.
$manifest = new SS_TemplateManifest($this->alternateBasePath, false, true);
$manifest->regenerate(false);
SS_TemplateLoader::instance()->pushManifest($manifest);
$templateManifest = new SS_TemplateManifest($this->alternateBasePath, false, true);
$templateManifest->regenerate(false);
SS_TemplateLoader::instance()->pushManifest($templateManifest);
$this->_oldTheme = SSViewer::current_theme();
SSViewer::set_theme('testtheme1');
$this->originalLocale = i18n::get_locale();
// Override default adapter to avoid cached translations between tests.
// Emulates behaviour in i18n::get_translators()
$this->origAdapter = i18n::get_translator('core');
$adapter = new Zend_Translate(array(
'adapter' => 'i18nRailsYamlAdapter',
'locale' => i18n::default_locale(),
'disableNotices' => true,
));
i18n::register_translator($adapter, 'core');
$adapter->removeCache();
i18n::include_by_locale('en');
}
function tearDown() {
SS_TemplateLoader::instance()->popManifest();
i18n::set_locale($this->originalLocale);
Director::setBaseFolder(null);
SSViewer::set_theme($this->_oldTheme);
i18n::register_translator($this->origAdapter, 'core');
parent::tearDown();
}
@ -78,24 +98,30 @@ class i18nTest extends SapphireTest {
function testGetExistingTranslations() {
$translations = i18n::get_existing_translations();
$this->assertTrue(isset($translations['en_US']), 'Checking for en_US translation');
$this->assertEquals($translations['en_US'], 'English (United States)');
$this->assertTrue(isset($translations['de_DE']), 'Checking for de_DE translation');
}
function testDataObjectFieldLabels() {
global $lang;
$oldLocale = i18n::get_locale();
i18n::set_locale('de_DE');
$obj = new i18nTest_DataObject();
$lang['en_US']['i18nTest_DataObject']['MyProperty'] = 'MyProperty';
$lang['de_DE']['i18nTest_DataObject']['MyProperty'] = 'Mein Attribut';
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTest_DataObject.MyProperty' => 'MyProperty'
), 'en_US');
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTest_DataObject.MyProperty' => 'Mein Attribut'
), 'de_DE');
$this->assertEquals(
$obj->fieldLabel('MyProperty'),
'Mein Attribut'
);
$lang['en_US']['i18nTest_DataObject']['MyUntranslatedProperty'] = 'MyUntranslatedProperty';
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTest_DataObject.MyUntranslatedProperty' => 'Mein Attribut'
), 'en_US');
$this->assertEquals(
$obj->fieldLabel('MyUntranslatedProperty'),
'My Untranslated Property'
@ -105,12 +131,16 @@ class i18nTest extends SapphireTest {
}
function testProvideI18nEntities() {
global $lang;
$oldLocale = i18n::get_locale();
$lang['en_US']['i18nTest_Object']['my_translatable_property'] = 'Untranslated';
$lang['de_DE']['i18nTest_Object']['my_translatable_property'] = 'Übersetzt';
i18n::set_locale('en_US');
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTest_Object.MyProperty' => 'Untranslated'
), 'en_US');
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTest_Object.my_translatable_property' => 'Übersetzt'
), 'de_DE');
$this->assertEquals(
i18nTest_Object::$my_translatable_property,
'Untranslated'
@ -136,19 +166,21 @@ class i18nTest extends SapphireTest {
}
function testTemplateTranslation() {
global $lang;
$oldLocale = i18n::get_locale();
i18n::set_locale('en_US');
$lang['en_US']['i18nTestModule']['MAINTEMPLATE'] = 'Main Template';
$lang['en_US']['i18nTestModule.ss']['SPRINTFNONAMESPACE'] = 'My replacement no namespace: %s';
$lang['en_US']['i18nTestModule']['LAYOUTTEMPLATE'] = 'Layout Template';
$lang['en_US']['i18nTestModule.ss']['LAYOUTTEMPLATENONAMESPACE'] = 'Layout Template no namespace';
$lang['en_US']['i18nTestModule']['SPRINTFNAMESPACE'] = 'My replacement: %s';
$lang['en_US']['i18nTestModule']['WITHNAMESPACE'] = 'Include Entity with Namespace';
$lang['en_US']['i18nTestModuleInclude.ss']['NONAMESPACE'] = 'Include Entity without Namespace';
$lang['en_US']['i18nTestModuleInclude.ss']['SPRINTFINCLUDENAMESPACE'] = 'My include replacement: %s';
$lang['en_US']['i18nTestModuleInclude.ss']['SPRINTFINCLUDENONAMESPACE'] = 'My include replacement no namespace: %s';
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTestModule.MAINTEMPLATE' => 'Main Template',
'i18nTestModule.ss.SPRINTFNONAMESPACE' => 'My replacement no namespace: %s',
'i18nTestModule.LAYOUTTEMPLATE' => 'Layout Template',
'i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE' => 'Layout Template no namespace',
'i18nTestModule.SPRINTFNAMESPACE' => 'My replacement: %s',
'i18nTestModule.WITHNAMESPACE' => 'Include Entity with Namespace',
'i18nTestModuleInclude.ss.NONAMESPACE' => 'Include Entity without Namespace',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE' => 'My include replacement: %s',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE' => 'My include replacement no namespace: %s'
), 'en_US');
$viewer = new SSViewer('i18nTestModule');
$parsedHtml = $viewer->process(new ArrayData(array('TestProperty' => 'TestPropertyValue')));
$this->assertContains(
@ -161,15 +193,18 @@ class i18nTest extends SapphireTest {
);
i18n::set_locale('de_DE');
$lang['de_DE']['i18nTestModule']['MAINTEMPLATE'] = 'TRANS Main Template';
$lang['de_DE']['i18nTestModule.ss']['SPRINTFNONAMESPACE'] = 'TRANS My replacement no namespace: %s';
$lang['de_DE']['i18nTestModule']['LAYOUTTEMPLATE'] = 'TRANS Layout Template';
$lang['de_DE']['i18nTestModule.ss']['LAYOUTTEMPLATENONAMESPACE'] = 'TRANS Layout Template no namespace';
$lang['de_DE']['i18nTestModule']['SPRINTFNAMESPACE'] = 'TRANS My replacement: %s';
$lang['de_DE']['i18nTestModule']['WITHNAMESPACE'] = 'TRANS Include Entity with Namespace';
$lang['de_DE']['i18nTestModuleInclude.ss']['NONAMESPACE'] = 'TRANS Include Entity without Namespace';
$lang['de_DE']['i18nTestModuleInclude.ss']['SPRINTFINCLUDENAMESPACE'] = 'TRANS My include replacement: %s';
$lang['de_DE']['i18nTestModuleInclude.ss']['SPRINTFINCLUDENONAMESPACE'] = 'TRANS My include replacement no namespace: %s';
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTestModule.MAINTEMPLATE' => 'TRANS Main Template',
'i18nTestModule.ss.SPRINTFNONAMESPACE' => 'TRANS My replacement no namespace: %s',
'i18nTestModule.LAYOUTTEMPLATE' => 'TRANS Layout Template',
'i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE' => 'TRANS Layout Template no namespace',
'i18nTestModule.SPRINTFNAMESPACE' => 'TRANS My replacement: %s',
'i18nTestModule.WITHNAMESPACE' => 'TRANS Include Entity with Namespace',
'i18nTestModuleInclude.ss.NONAMESPACE' => 'TRANS Include Entity without Namespace',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE' => 'TRANS My include replacement: %s',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE' => 'TRANS My include replacement no namespace: %s'
), 'de_DE');
$viewer = new SSViewer('i18nTestModule');
$parsedHtml = $viewer->process(new ArrayData(array('TestProperty' => 'TestPropertyValue')));
$this->assertContains(
@ -213,29 +248,6 @@ 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;
// save lang state, if we don't do this we may break other tests
$oldLang = $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");
$lang = $oldLang;
}
function testValidateLocale() {
$this->assertTrue(i18n::validate_locale('en_US'), 'Known locale in underscore format is valid');
@ -244,12 +256,160 @@ class i18nTest extends SapphireTest {
$this->assertFalse(i18n::validate_locale('xx_XX'), 'Unknown locale in correct format is not valid');
$this->assertFalse(i18n::validate_locale(''), 'Empty string is not valid');
}
static function translationTestPlugin($locale) {
$result = array();
$result["en_US"]["i18nTestProvider"]["foo"] = "baz_en";
return $result;
function testTranslate() {
$oldLocale = i18n::get_locale();
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTestModule.ENTITY' => 'Entity with "Double Quotes"',
), 'en_US');
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTestModule.ENTITY' => 'Entity with "Double Quotes" (de)',
'i18nTestModule.ADDITION' => 'Addition (de)',
), 'de');
i18n::get_translator('core')->getAdapter()->addTranslation(array(
'i18nTestModule.ENTITY' => 'Entity with "Double Quotes" (de_AT)',
), 'de_AT');
$this->assertEquals(i18n::_t('i18nTestModule.ENTITY'), 'Entity with "Double Quotes"',
'Returns translation in default language'
);
i18n::set_locale('de');
$this->assertEquals(i18n::_t('i18nTestModule.ENTITY'), 'Entity with "Double Quotes" (de)',
'Returns translation according to current locale'
);
i18n::set_locale('de_AT');
$this->assertEquals(i18n::_t('i18nTestModule.ENTITY'), 'Entity with "Double Quotes" (de_AT)',
'Returns specific regional translation if available'
);
$this->assertEquals(i18n::_t('i18nTestModule.ADDITION'), 'Addition (de)',
'Returns fallback non-regional translation if regional is not available'
);
i18n::set_locale('fr');
$this->assertEquals(i18n::_t('i18nTestModule.ENTITY'), '',
'Returns empty translation without default string if locale is not found'
);
$this->assertEquals(i18n::_t('i18nTestModule.ENTITY', 'default'), 'default',
'Returns default string if locale is not found'
);
i18n::set_locale($oldLocale);
}
function testIncludeByLocale() {
// Looping through modules, so we can test the translation autoloading
// Load non-exclusive to retain core class autoloading
$classManifest = new SS_ClassManifest($this->alternateBasePath, true, true, false);
SS_ClassLoader::instance()->pushManifest($classManifest);
$adapter = i18n::get_translator('core')->getAdapter();
$this->assertTrue($adapter->isAvailable('en'));
$this->assertFalse($adapter->isAvailable('de'));
$this->assertFalse($adapter->isTranslated('i18nTestModule.ENTITY', 'de'),
'Existing unloaded entity not available before call'
);
$this->assertFalse($adapter->isTranslated('i18nTestModule.ENTITY', 'af'),
'Non-existing unloaded entity not available before call'
);
i18n::include_by_locale('de');
$this->assertTrue($adapter->isAvailable('en'));
$this->assertTrue($adapter->isAvailable('de'));
$this->assertTrue($adapter->isTranslated('i18nTestModule.ENTITY', null, 'de'), 'Includes module files');
$this->assertTrue($adapter->isTranslated('i18nTestTheme1.LAYOUTTEMPLATE', null, 'de'), 'Includes theme files');
$this->assertTrue($adapter->isTranslated('i18nTestModule.OTHERENTITY', null, 'de'), 'Includes submodule files');
SS_ClassLoader::instance()->popManifest();
}
function testRegisterTranslator() {
$translator = new Zend_Translate(array(
'adapter' => 'i18nTest_CustomTranslatorAdapter',
'disableNotices' => true,
));
i18n::register_translator($translator, 'custom', 10);
$translators = i18n::get_translators();
$this->assertArrayHasKey('custom', $translators[10]);
$this->assertType('Zend_Translate', $translators[10]['custom']);
$this->assertType('i18nTest_CustomTranslatorAdapter', $translators[10]['custom']->getAdapter());
i18n::unregister_translator('custom');
$translators = i18n::get_translators();
$this->assertArrayNotHasKey('custom', $translators[10]);
}
function testMultipleTranslators() {
// Looping through modules, so we can test the translation autoloading
// Load non-exclusive to retain core class autoloading
$classManifest = new SS_ClassManifest($this->alternateBasePath, true, true, false);
SS_ClassLoader::instance()->pushManifest($classManifest);
i18n::set_locale('en_US');
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'Entity with "Double Quotes"'
);
$this->assertEquals(
i18n::_t('AdapterEntity1', 'AdapterEntity1'),
'AdapterEntity1',
'Falls back to default string if not found'
);
// Add a new translator
$translator = new Zend_Translate(array(
'adapter' => 'i18nTest_CustomTranslatorAdapter',
'disableNotices' => true,
));
i18n::register_translator($translator, 'custom', 11);
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'i18nTestModule.ENTITY CustomAdapter (en_US)',
'Existing entities overruled by adapter with higher priority'
);
$this->assertEquals(
i18n::_t('AdapterEntity1', 'AdapterEntity1'),
'AdapterEntity1 CustomAdapter (en_US)',
'New entities only defined in new adapter are detected'
);
// Add a second new translator to test priorities
$translator = new Zend_Translate(array(
'adapter' => 'i18nTest_OtherCustomTranslatorAdapter',
'disableNotices' => true,
));
i18n::register_translator($translator, 'othercustom_lower_prio', 5);
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'i18nTestModule.ENTITY CustomAdapter (en_US)',
'Adapter with lower priority loses'
);
// Add a third new translator to test priorities
$translator = new Zend_Translate(array(
'adapter' => 'i18nTest_OtherCustomTranslatorAdapter',
'disableNotices' => true,
));
i18n::register_translator($translator, 'othercustom_higher_prio', 15);
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'i18nTestModule.ENTITY OtherCustomAdapter (en_US)',
'Adapter with higher priority wins'
);
i18n::unregister_translator('custom');
i18n::unregister_translator('othercustom_lower_prio');
i18n::unregister_translator('othercustom_higher_prio');
SS_ClassLoader::instance()->popManifest();
}
}
class i18nTest_DataObject extends DataObject implements TestOnly {
@ -301,3 +461,39 @@ class i18nTest_Object extends Object implements TestOnly, i18nEntityProvider {
}
}
class i18nTest_CustomTranslatorAdapter extends Zend_Translate_Adapter implements TestOnly,i18nTranslateAdapterInterface {
protected function _loadTranslationData($filename, $locale, array $options = array()) {
return array(
$locale => array(
'AdapterEntity1' => 'AdapterEntity1 CustomAdapter (' . $locale . ')',
'i18nTestModule.ENTITY' => 'i18nTestModule.ENTITY CustomAdapter (' . $locale . ')',
)
);
}
function toString() {
return 'i18nTest_CustomTranslatorAdapter';
}
function getFilenameForLocale($locale) {
return false; // not file based
}
}
class i18nTest_OtherCustomTranslatorAdapter extends Zend_Translate_Adapter implements TestOnly,i18nTranslateAdapterInterface {
protected function _loadTranslationData($filename, $locale, array $options = array()) {
return array(
$locale => array(
'i18nTestModule.ENTITY' => 'i18nTestModule.ENTITY OtherCustomAdapter (' . $locale . ')',
)
);
}
function toString() {
return 'i18nTest_OtherCustomTranslatorAdapter';
}
function getFilenameForLocale($locale) {
return false; // not file based
}
}

View File

@ -6,7 +6,7 @@ require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
require_once 'thirdparty/sfYaml/lib/sfYaml.php';
// require_once 'thirdparty/sfYaml/lib/sfYaml.php';
class Translate_Adapter_RailsYaml extends Zend_Translate_Adapter {