From 5d13c67caec0269db916a68f3bb0384c3c24b72a Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 17 Mar 2016 13:29:35 +0900 Subject: [PATCH 1/5] Merge Lang::getPluginLang() back into Lang::loadDirectory() --- common/framework/lang.php | 98 ++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 58 deletions(-) diff --git a/common/framework/lang.php b/common/framework/lang.php index e2cfb49be..a08e0ce09 100644 --- a/common/framework/lang.php +++ b/common/framework/lang.php @@ -108,70 +108,52 @@ class Lang return true; } - // Load the language file. - $lang = $this->getPluginLang($dir); + // Initialize variables. + $filename = null; + $lang = new \stdClass; + $result = true; - // Load the default language file. + // Find a suitable language file in the given directory. + if (file_exists($dir . '/' . $this->_language . '.php')) + { + $filename = $dir . '/' . $this->_language . '.php'; + } + elseif (($hyphen = strpos($this->_language, '-')) !== false && file_exists($dir . '/' . substr($this->_language, 0, $hyphen) . '.php')) + { + $filename = $dir . '/' . substr($this->_language, 0, $hyphen) . '.php'; + } + elseif (file_exists("$dir/lang.xml")) + { + $filename = Parsers\LangParser::compileXMLtoPHP("$dir/lang.xml", $this->_language === 'ja' ? 'jp' : $this->_language); + } + elseif (file_exists($dir . '/' . ($this->_language === 'ja' ? 'jp' : $this->_language) . '.lang.php')) + { + $filename = $dir . '/' . ($this->_language === 'ja' ? 'jp' : $this->_language) . '.lang.php'; + } + + // Load the language file. + if ($filename) + { + include $filename; + array_unshift($this->_search_priority, $plugin_name); + $result = true; + } + else + { + $result = false; + } + + // Mark this directory and plugin as loaded. + $this->_loaded_directories[$dir] = true; + $this->_loaded_plugins[$plugin_name] = $lang; + + // Load the same directory in the default language, too. if ($this->_language !== 'en') { self::getInstance('en')->loadDirectory($dir, $plugin_name); } - if (!empty($lang)) - { - $this->_loaded_directories[$dir] = true; - $this->_loaded_plugins[$plugin_name] = $lang; - array_unshift($this->_search_priority, $plugin_name); - return true; - } - else - { - $this->_loaded_directories[$dir] = true; - $this->_loaded_plugins[$plugin_name] = new \stdClass; - return false; - } - } - - /** - * Get the language file from plugin. - * - * @param string $dir - * @param string $language - * @return object - */ - public function getPluginLang($dir, $language = null) - { - if (!$language) - { - $language = $this->_language; - } - - if (file_exists($dir . '/' . $language . '.php')) - { - $filename = $dir . '/' . $language . '.php'; - } - elseif (($hyphen = strpos($language, '-')) !== false && file_exists($dir . '/' . substr($language, 0, $hyphen) . '.php')) - { - $filename = $dir . '/' . substr($language, 0, $hyphen) . '.php'; - } - elseif (file_exists("$dir/lang.xml")) - { - $filename = Parsers\LangParser::compileXMLtoPHP("$dir/lang.xml", $language === 'ja' ? 'jp' : $language); - } - elseif (file_exists($dir . '/' . ($language === 'ja' ? 'jp' : $language) . '.lang.php')) - { - $filename = $dir . '/' . ($language === 'ja' ? 'jp' : $language) . '.lang.php'; - } - - if (!$filename) - { - return new \stdClass; - } - - $lang = new \stdClass; - include $filename; - - return $lang; + return $result; } /** From 09eb7935fa0fbc146600bce0ece67fb1880aea7b Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 17 Mar 2016 13:58:57 +0900 Subject: [PATCH 2/5] Refactor default language search routine for better performance --- common/framework/lang.php | 111 +++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/common/framework/lang.php b/common/framework/lang.php index a08e0ce09..ec47de9ad 100644 --- a/common/framework/lang.php +++ b/common/framework/lang.php @@ -191,6 +191,24 @@ class Lang $this->__set($key, $value); } + /** + * Fallback method for getting the default translation. + * + * @param string $key + * @return string + */ + public function getFromDefaultLang($key) + { + if ($this->_language === 'en') + { + return $key; + } + else + { + return self::getInstance('en')->__get($key); + } + } + /** * Magic method for translations without arguments. * @@ -199,45 +217,58 @@ class Lang */ public function __get($key) { - // Get default language - if ($this->_language !== 'en') + // Load a dot-separated key (prefixed by plugin name). + if (preg_match('/^[a-z0-9_.-]+$/i', $key) && ($keys = explode('.', $key)) && count($keys) >= 2) { - $lang_en = self::getInstance('en')->{$key}; - } - - // Separate the plugin name from the key. - if (preg_match('/^[a-z0-9_.-]+$/i', $key) && ($keys = explode('.', $key, 2)) && count($keys) === 2) - { - list($plugin_name, $lang_key) = $keys; + // Attempt to load the plugin. + $plugin_name = array_shift($keys); if (!isset($this->_loaded_plugins[$plugin_name])) { $this->loadPlugin($plugin_name); } - - if (isset($this->_loaded_plugins[$plugin_name]->{$lang_key})) + if (!isset($this->_loaded_plugins[$plugin_name])) { - $lang = $this->_loaded_plugins[$plugin_name]->{$lang_key}; - if (is_array($lang) && is_array($lang_en) && count($lang_en, COUNT_RECURSIVE) > count($lang, COUNT_RECURSIVE)) + return $this->getFromDefaultLang($key); + } + + // Find the given key. + $lang = $this->_loaded_plugins[$plugin_name]; + foreach ($keys as $subkey) + { + if (isset($lang->{$subkey})) { - return $lang_en; + $lang = is_scalar($lang->{$subkey}) ? $lang->{$subkey} : new \ArrayObject($lang->{$subkey}, 3); } - + else + { + return $this->getFromDefaultLang($key); + } + } + return $lang; + } + + // Search custom translations first. + if (isset($this->_loaded_plugins['_custom_']->{$key})) + { + $lang = $this->_loaded_plugins['_custom_']->{$key}; + if (is_array($lang)) + { + return new \ArrayObject($lang, 3); + } + else + { return $lang; } } - else + + // Search other plugins. + foreach ($this->_search_priority as $plugin_name) { - // Search custom translations first. - if (isset($this->_loaded_plugins['_custom_']->{$key})) + if (isset($this->_loaded_plugins[$plugin_name]->{$key})) { - $lang = $this->_loaded_plugins['_custom_']->{$key}; + $lang = $this->_loaded_plugins[$plugin_name]->{$key}; if (is_array($lang)) { - if (is_array($lang_en) && count($lang_en, COUNT_RECURSIVE) > count($lang, COUNT_RECURSIVE)) - { - return new \ArrayObject($lang_en, 3); - } - return new \ArrayObject($lang, 3); } else @@ -245,38 +276,10 @@ class Lang return $lang; } } - - // Search other plugins. - foreach ($this->_search_priority as $plugin_name) - { - if (isset($this->_loaded_plugins[$plugin_name]->{$key})) - { - $lang = $this->_loaded_plugins[$plugin_name]->{$key}; - if (is_array($lang)) - { - if (is_array($lang_en) && count($lang_en, COUNT_RECURSIVE) > count($lang, COUNT_RECURSIVE)) - { - return new \ArrayObject($lang_en, 3); - } - - return new \ArrayObject($lang, 3); - } - else - { - return $lang; - } - } - } } - // Search other language. - if (isset($lang_en)) - { - return $lang_en; - } - - // If no translation is found, return the key. - return $key; + // If no translation is found, return the default language. + return $this->getFromDefaultLang($key); } /** From 014e7f13a495f54fa0505dd68315d5c044b5ae0f Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 17 Mar 2016 14:11:33 +0900 Subject: [PATCH 3/5] Move string interpolation from __call() to get() --- common/framework/lang.php | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/common/framework/lang.php b/common/framework/lang.php index ec47de9ad..8366e715f 100644 --- a/common/framework/lang.php +++ b/common/framework/lang.php @@ -176,7 +176,19 @@ class Lang { $args = func_get_args(); array_shift($args); - return $this->__call($key, $args); + if (count($args) === 1 && is_array($args[0])) + { + $args = $args[0]; + } + + // Get the translation. + $translation = $this->__get($key); + + // If there are no arguments, return the translation. + if (!count($args)) return $translation; + + // If there are arguments, interpolate them into the translation and return the result. + return vsprintf($translation, $args); } /** @@ -338,16 +350,6 @@ class Lang */ public function __call($key, $args = array()) { - // Remove a colon from the beginning of the string. - if ($key !== '' && $key[0] === ':') $key = substr($key, 1); - - // Find the translation. - $translation = $this->__get($key); - - // If there are no arguments, return the translation. - if (!count($args)) return $translation; - - // If there are arguments, interpolate them into the translation and return the result. - return vsprintf($translation, $args); + return $this->get($key, $args); } } From ef30f0fa408dcb12e0a4dbd774a756f3753505fc Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 17 Mar 2016 14:19:35 +0900 Subject: [PATCH 4/5] Update and expand unit tests for Lang class --- tests/unit/framework/LangTest.php | 42 +++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/unit/framework/LangTest.php b/tests/unit/framework/LangTest.php index 2ca57cb61..094cb9843 100644 --- a/tests/unit/framework/LangTest.php +++ b/tests/unit/framework/LangTest.php @@ -4,30 +4,50 @@ class LangTest extends \Codeception\TestCase\Test { public function testLang() { + // Test separation of languages. $ko = Rhymix\Framework\Lang::getInstance('ko'); $en = Rhymix\Framework\Lang::getInstance('en'); $this->assertTrue($ko instanceof Rhymix\Framework\Lang); $this->assertTrue($en instanceof Rhymix\Framework\Lang); $this->assertFalse($ko === $en); + // Test backward compatible language code for Japanese. $ja = Rhymix\Framework\Lang::getInstance('ja'); $jp = Rhymix\Framework\Lang::getInstance('jp'); $this->assertTrue($ja === $jp); - $this->assertEquals('도움말', $ko->get('common.help')); - $this->assertEquals('Help', $en->get('common.help')); - $this->assertEquals('도움말', $ko->help); - $this->assertEquals('Help', $en->help); - - $this->assertEquals('common.nonexistent', $ko->get('common.nonexistent')); - $this->assertEquals('common.nonexistent', $ko->get('common.nonexistent', 'foo', 'bar')); - - $this->assertEquals('admin.help', $ko->get('admin.help')); - $this->assertEquals('admin.help', $en->get('admin.help')); - + // Test loading new plugins. + $this->assertNotEquals('ヘルプ', $ja->help); $ja->loadPlugin('common'); $this->assertEquals('ヘルプ', $ja->help); + // Test simple translations with namespacing. + $this->assertEquals('도움말', $ko->get('common.help')); + $this->assertEquals('Help', $en->get('common.help')); + + // Test simple translations without namespacing. + $this->assertEquals('도움말', $ko->help); + $this->assertEquals('Help', $en->help); + + // Test complex translations with multidimensional arrays. + $this->assertEquals('%d분 전', $ko->get('common.time_gap.min')); + $this->assertEquals('10분 전', $ko->get('common.time_gap.min', 10)); + $this->assertTrue($ko->get('common.time_gap') instanceof \ArrayObject); + $this->assertEquals('%d분 전', $ko->get('common.time_gap')->min); + + // Test nonexistent keys. + $this->assertEquals('common.nonexistent', $ko->get('common.nonexistent')); + $this->assertEquals('common.nonexistent', $ko->get('common.nonexistent', 'foo', 'bar')); + $this->assertEquals('admin.help', $ko->get('admin.help')); + $this->assertEquals('admin.help', $en->get('admin.help')); + + // Test fallback to English. + $en->only_in_english = 'Hello world'; + $this->assertEquals('Hello world', $ko->only_in_english); + $this->assertEquals('Hello world', $en->only_in_english); + $this->assertEquals('Hello world', $ja->only_in_english); + + // Test string interpolation. $ko->foobartestlang = '%s님 안녕하세요?'; $this->assertEquals('Travis님 안녕하세요?', $ko->foobartestlang('Travis')); $en->foobartestlang = 'Hello, %s!'; From 15e33606098e619fadd7e79007816b786c43e421 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 17 Mar 2016 14:52:05 +0900 Subject: [PATCH 5/5] Also support dot notation when setting new language keys --- common/framework/lang.php | 103 ++++++++++++++++++++++-------- tests/unit/framework/LangTest.php | 13 ++++ 2 files changed, 90 insertions(+), 26 deletions(-) diff --git a/common/framework/lang.php b/common/framework/lang.php index 8366e715f..023ece8d7 100644 --- a/common/framework/lang.php +++ b/common/framework/lang.php @@ -247,30 +247,27 @@ class Lang $lang = $this->_loaded_plugins[$plugin_name]; foreach ($keys as $subkey) { - if (isset($lang->{$subkey})) + if (is_object($lang) && isset($lang->{$subkey})) { - $lang = is_scalar($lang->{$subkey}) ? $lang->{$subkey} : new \ArrayObject($lang->{$subkey}, 3); + $lang = $lang->{$subkey}; + } + elseif (is_array($lang) && isset($lang[$subkey])) + { + $lang = $lang[$subkey]; } else { return $this->getFromDefaultLang($key); } } - return $lang; + return is_array($lang) ? new \ArrayObject($lang, 3) : $lang; } // Search custom translations first. if (isset($this->_loaded_plugins['_custom_']->{$key})) { $lang = $this->_loaded_plugins['_custom_']->{$key}; - if (is_array($lang)) - { - return new \ArrayObject($lang, 3); - } - else - { - return $lang; - } + return is_array($lang) ? new \ArrayObject($lang, 3) : $lang; } // Search other plugins. @@ -279,14 +276,7 @@ class Lang if (isset($this->_loaded_plugins[$plugin_name]->{$key})) { $lang = $this->_loaded_plugins[$plugin_name]->{$key}; - if (is_array($lang)) - { - return new \ArrayObject($lang, 3); - } - else - { - return $lang; - } + return is_array($lang) ? new \ArrayObject($lang, 3) : $lang; } } @@ -303,6 +293,73 @@ class Lang */ public function __set($key, $value) { + // Set a dot-separated key (prefixed by plugin name). + if (preg_match('/^[a-z0-9_.-]+$/i', $key) && ($keys = explode('.', $key)) && count($keys) >= 2) + { + // Attempt to load the plugin. + $plugin_name = array_shift($keys); + if (!isset($this->_loaded_plugins[$plugin_name])) + { + $this->loadPlugin($plugin_name); + } + if (!isset($this->_loaded_plugins[$plugin_name])) + { + return false; + } + + // Set the given key. + $count = count($keys); + $lang = $this->_loaded_plugins[$plugin_name]; + foreach ($keys as $i => $subkey) + { + if (is_object($lang) && isset($lang->{$subkey})) + { + if ($i === $count - 1) + { + $lang->{$subkey} = $value; + break; + } + elseif (is_array($lang->{$subkey})) + { + $lang = &$lang->{$subkey}; + } + else + { + return false; + } + } + elseif (is_array($lang) && isset($lang[$subkey])) + { + if ($i === $count - 1) + { + $lang[$subkey] = $value; + break; + } + elseif (is_array($lang[$subkey])) + { + $lang = &$lang[$subkey]; + } + else + { + return false; + } + } + else + { + if (is_object($lang)) + { + $lang->{$subkey} = $value; + } + else + { + $lang[$subkey] = $value; + } + break; + } + } + } + + // Set a regular key. $this->_loaded_plugins['_custom_']->{$key} = $value; } @@ -332,13 +389,7 @@ class Lang */ public function __unset($key) { - foreach ($this->_loaded_plugins as $plugin_name => $translations) - { - if (isset($translations->{$key})) - { - unset($translations->{$key}); - } - } + $this->set($key, null); } /** diff --git a/tests/unit/framework/LangTest.php b/tests/unit/framework/LangTest.php index 094cb9843..4533b063c 100644 --- a/tests/unit/framework/LangTest.php +++ b/tests/unit/framework/LangTest.php @@ -41,6 +41,19 @@ class LangTest extends \Codeception\TestCase\Test $this->assertEquals('admin.help', $ko->get('admin.help')); $this->assertEquals('admin.help', $en->get('admin.help')); + // Test setting new keys with and without namespacing. + $ko->set('foo', 'FOO!'); + $this->assertEquals('FOO!', $ko->get('foo')); + $ko->set('common.foobar', 'FOOBAR!'); + $this->assertEquals('FOOBAR!', $ko->get('common.foobar')); + $this->assertEquals('FOOBAR!', $ko->get('foobar')); + + // Test setting new keys with multidimensional arrays. + $ko->set('common.time_gap.foobar', 'FOOBAR!'); + $this->assertEquals('FOOBAR!', $ko->get('common.time_gap.foobar')); + $ko->set('common.foobar.baz', 'BAZ!'); + $this->assertNotEquals('BAZ!', $ko->get('common.foobar.baz')); + // Test fallback to English. $en->only_in_english = 'Hello world'; $this->assertEquals('Hello world', $ko->only_in_english);