diff --git a/autoload.php b/autoload.php new file mode 100644 index 00000000..6bb5f111 --- /dev/null +++ b/autoload.php @@ -0,0 +1,3 @@ + '', 'confUnreadable' => '', ]); - self::$path = self::$vfs->url(); + self::$path = self::$vfs->url()."/"; // set up a file without read access - chmod(self::$path."/confUnreadable", 0000); + chmod(self::$path."confUnreadable", 0000); } static function tearDownAfterClass() { @@ -48,9 +48,9 @@ class TestConf extends \PHPUnit\Framework\TestCase { */ function testImportFile() { $conf = new Conf(); - $conf->importFile(self::$path."/confGood"); + $conf->importFile(self::$path."confGood"); $this->assertEquals("xx", $conf->lang); - $conf = new Conf(self::$path."/confGood"); + $conf = new Conf(self::$path."confGood"); $this->assertEquals("xx", $conf->lang); } @@ -59,7 +59,7 @@ class TestConf extends \PHPUnit\Framework\TestCase { */ function testImportFileMissing() { $this->assertException("fileMissing", "Conf"); - $conf = new Conf(self::$path."/confMissing"); + $conf = new Conf(self::$path."confMissing"); } /** @@ -67,7 +67,7 @@ class TestConf extends \PHPUnit\Framework\TestCase { */ function testImportFileEmpty() { $this->assertException("fileCorrupt", "Conf"); - $conf = new Conf(self::$path."/confEmpty"); + $conf = new Conf(self::$path."confEmpty"); } /** @@ -75,7 +75,7 @@ class TestConf extends \PHPUnit\Framework\TestCase { */ function testImportFileUnreadable() { $this->assertException("fileUnreadable", "Conf"); - $conf = new Conf(self::$path."/confUnreadable"); + $conf = new Conf(self::$path."confUnreadable"); } /** @@ -83,7 +83,7 @@ class TestConf extends \PHPUnit\Framework\TestCase { */ function testImportFileNotAnArray() { $this->assertException("fileCorrupt", "Conf"); - $conf = new Conf(self::$path."/confNotArray"); + $conf = new Conf(self::$path."confNotArray"); } /** @@ -92,7 +92,7 @@ class TestConf extends \PHPUnit\Framework\TestCase { function testImportFileNotPHP() { $this->assertException("fileCorrupt", "Conf"); // this should not print the output of the non-PHP file - $conf = new Conf(self::$path."/confNotPHP"); + $conf = new Conf(self::$path."confNotPHP"); } /** @@ -101,6 +101,6 @@ class TestConf extends \PHPUnit\Framework\TestCase { function testImportFileCorrupt() { $this->assertException("fileCorrupt", "Conf"); // this should not print the output of the non-PHP file - $conf = new Conf(self::$path."/confCorrupt"); + $conf = new Conf(self::$path."confCorrupt"); } } diff --git a/tests/TestLang.php b/tests/TestLang.php index aa9e8e68..8188f79e 100644 --- a/tests/TestLang.php +++ b/tests/TestLang.php @@ -10,13 +10,16 @@ class TestLang extends \PHPUnit\Framework\TestCase { static $vfs; static $path; static $files; + static $defaultPath; static function setUpBeforeClass() { + // this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping Lang\Exception::$test = true; + // test files self::$files = [ 'en.php' => ' "and the Philosopher\'s Stone"];', - 'en-ca.php' => ' ' "and the Sorcerer\'s Stone"];', + 'en_ca.php' => ' "{0} and {1}"];', + 'en_us.php' => ' "and the Sorcerer\'s Stone"];', 'fr.php' => ' "à l\'école des sorciers"];', 'ja.php' => ' "賢者の石"];', 'de.php' => ' "und der Stein der Weisen"];', @@ -24,7 +27,7 @@ class TestLang extends \PHPUnit\Framework\TestCase { 'it.php' => ' ' 'DEAD BEEF', - 'fr-ca.php' => '', + 'fr_ca.php' => '', // unreadable file 'ru.php' => '', ]; @@ -32,16 +35,65 @@ class TestLang extends \PHPUnit\Framework\TestCase { self::$path = self::$vfs->url(); // set up a file without read access chmod(self::$path."/ru.php", 0000); + // make the Lang class use the vfs files + self::$defaultPath = Lang::$path; + Lang::$path = self::$path."/"; } static function tearDownAfterClass() { Lang\Exception::$test = false; + Lang::$path = self::$defaultPath; self::$path = null; self::$vfs = null; self::$files = null; + Lang::set(Lang::DEFAULT, true); } function testList() { - $this->assertEquals(sizeof(self::$files), sizeof(Lang::list("en", "vfs://langtest/"))); + $this->assertCount(sizeof(self::$files), Lang::list("en")); } + + /** + * @depends testList + */ + function testSet() { + $this->assertEquals("en", Lang::set("en")); + $this->assertEquals("en_ca", Lang::set("en_ca")); + $this->assertEquals("de", Lang::set("de_ch")); + $this->assertEquals("en", Lang::set("en_gb_hixie")); + $this->assertEquals("en_ca", Lang::set("en_ca_jking")); + $this->assertEquals("en", Lang::set("es")); + $this->assertEquals("", Lang::set("")); + } + + /** + * @depends testSet + */ + function testLoadInternalStrings() { + $this->assertEquals("", Lang::set("", true)); + $this->assertCount(sizeof(Lang::REQUIRED), Lang::dump()); + } + + /** + * @depends testLoadInternalStrings + */ + function testLoadDefaultStrings() { + $this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true)); + $str = Lang::dump(); + $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str); + $this->assertArrayHasKey('Test.presentText', $str); + } + + /** + * @depends testLoadDefaultStrings + */ + function testLoadMultipleFiles() { + Lang::set(Lang::DEFAULT, true); + $this->assertEquals("ja", Lang::set("ja", true)); + $str = Lang::dump(); + $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str); + $this->assertArrayHasKey('Test.presentText', $str); + $this->assertArrayHasKey('Test.absentText', $str); + } + } \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 371301cd..da51a19e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,10 +2,7 @@ declare(strict_types=1); namespace JKingWeb\NewsSync; -const BASE = __DIR__.DIRECTORY_SEPARATOR."..".DIRECTORY_SEPARATOR; -const NS_BASE = __NAMESPACE__."\\"; - -require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php"; +require_once __DIR__.DIRECTORY_SEPARATOR."..".DIRECTORY_SEPARATOR."bootstrap.php"; trait TestingHelpers { function assertException(string $msg, string $prefix = "", string $type = "Exception") { @@ -19,6 +16,4 @@ trait TestingHelpers { $this->expectException($class); $this->expectExceptionCode($code); } -} - -ignore_user_abort(true); \ No newline at end of file +} \ No newline at end of file diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 5292daaf..2e2fb7ff 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,19 +1,20 @@ - - - TestLang.php - TestConf.php - - + colors="true" + bootstrap="bootstrap.php" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + beStrictAboutTestsThatDoNotTestAnything="true" + beStrictAboutOutputDuringTests="true" + beStrictAboutTestSize="true"> + + TestLang.php + TestLangComplex.php + + + TestConf.php + \ No newline at end of file diff --git a/tests/testLangComplex.php b/tests/testLangComplex.php new file mode 100644 index 00000000..86673075 --- /dev/null +++ b/tests/testLangComplex.php @@ -0,0 +1,106 @@ + ' "and the Philosopher\'s Stone"];', + 'en_ca.php' => ' "{0} and {1}"];', + 'en_us.php' => ' "and the Sorcerer\'s Stone"];', + 'fr.php' => ' "à l\'école des sorciers"];', + 'ja.php' => ' "賢者の石"];', + 'de.php' => ' "und der Stein der Weisen"];', + // corrupt files + 'it.php' => ' ' 'DEAD BEEF', + 'fr_ca.php' => '', + // unreadable file + 'ru.php' => '', + ]; + self::$vfs = vfsStream::setup("langtest", 0777, self::$files); + self::$path = self::$vfs->url(); + // set up a file without read access + chmod(self::$path."/ru.php", 0000); + // make the Lang class use the vfs files + self::$defaultPath = Lang::$path; + Lang::$path = self::$path."/"; + } + + static function tearDownAfterClass() { + Lang\Exception::$test = false; + Lang::$path = self::$defaultPath; + self::$path = null; + self::$vfs = null; + self::$files = null; + Lang::set(Lang::DEFAULT, true); + } + + function setUp() { + Lang::set(Lang::DEFAULT, true); + } + + function testLoadLazy() { + Lang::set("ja"); + $this->assertArrayNotHasKey('Test.absentText', Lang::dump()); + } + + function testLoadCascade() { + Lang::set("ja", true); + $this->assertEquals("de", Lang::set("de", true)); + $str = Lang::dump(); + $this->assertArrayNotHasKey('Test.absentText', $str); + $this->assertEquals('und der Stein der Weisen', $str['Test.presentText']); + } + + /** + * @depends testLoadCascade + */ + function testLoadSubtag() { + $this->assertEquals("en_ca", Lang::set("en_ca", true)); + } + + /** + * @depends testLoadSubtag + */ + function testMessage() { + Lang::set("de", true); + $this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText')); + } + + /** + * @depends testMessage + */ + function testMessageNumMSingle() { + Lang::set("en_ca", true); + $this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing', Lang::DEFAULT)); + } + + /** + * @depends testMessage + */ + function testMessageNumMulti() { + Lang::set("en_ca", true); + $this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone'])); + } + + /** + * @depends testMessage + */ + function testMessageNamed() { + $this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en'])); + } +} \ No newline at end of file diff --git a/vendor/JKingWeb/NewsSync/Lang.php b/vendor/JKingWeb/NewsSync/Lang.php index 7f1ae97e..ed47ae45 100644 --- a/vendor/JKingWeb/NewsSync/Lang.php +++ b/vendor/JKingWeb/NewsSync/Lang.php @@ -4,7 +4,6 @@ namespace JKingWeb\NewsSync; use \Webmozart\Glob\Glob; class Lang { - const PATH = BASE."locale".DIRECTORY_SEPARATOR; const DEFAULT = "en"; const REQUIRED = [ 'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php', @@ -16,6 +15,7 @@ class Lang { 'Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})', ]; + static public $path = BASE."locale".DIRECTORY_SEPARATOR; static protected $requirementsMet = false; static protected $synched = false; static protected $wanted = self::DEFAULT; @@ -25,13 +25,16 @@ class Lang { protected function __construct() {} - static public function set(string $locale = "", bool $immediate = false): string { + static public function set(string $locale, bool $immediate = false): string { if(!self::$requirementsMet) self::checkRequirements(); - if($locale=="") $locale = self::DEFAULT; if($locale==self::$wanted) return $locale; - $list = self::listFiles(); - if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT); - self::$wanted = self::match($locale, $list); + if($locale != "") { + $list = self::listFiles(); + if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT); + self::$wanted = self::match($locale, $list); + } else { + self::$wanted = ""; + } self::$synched = false; if($immediate) self::load(); return self::$wanted; @@ -41,12 +44,15 @@ class Lang { return (self::$locale=="") ? self::DEFAULT : self::$locale; } + static public function dump(): array { + return self::$strings; + } + static public function msg(string $msgID, $vars = null): string { // if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead if(!self::$synched) try {self::load();} catch(Lang\Exception $e) { if(self::$wanted==self::DEFAULT) { - self::set(); - self::load(); + self::set("", true); } else { throw $e; } @@ -64,9 +70,9 @@ class Lang { return $msg; } - static public function list(string $locale = "", string $path = self::PATH): array { + static public function list(string $locale = ""): array { $out = []; - $files = self::listFiles($path); + $files = self::listFiles(); foreach($files as $tag) { $out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale); } @@ -85,10 +91,12 @@ class Lang { return true; } - static protected function listFiles(string $path = self::PATH): array { - $out = Glob::glob($path."*.php"); + static protected function listFiles(): array { + $out = glob(self::$path."*.php"); + if(empty($out)) $out = Glob::glob(self::$path."*.php"); $out = array_map(function($file) { - $file = substr(str_replace(DIRECTORY_SEPARATOR, "/", $file),strrpos($file,"/")+1); + $file = str_replace(DIRECTORY_SEPARATOR, "/", $file); + $file = substr($file, strrpos($file, "/")+1); return strtolower(substr($file,0,strrpos($file,"."))); },$out); natsort($out); @@ -96,21 +104,19 @@ class Lang { } static protected function load(): bool { - self::$synched = true; if(!self::$requirementsMet) self::checkRequirements(); - // if we've yet to request a locale, just load the fallback strings and return + // if we've requested no locale (""), just load the fallback strings and return if(self::$wanted=="") { self::$strings = self::REQUIRED; self::$locale = self::$wanted; + self::$synched = true; return true; } // decompose the requested locale from specific to general, building a list of files to load $tags = \Locale::parseLocale(self::$wanted); $files = []; - $loaded = []; - $strings = []; while(sizeof($tags) > 0) { - $files[] = \Locale::composeLocale($tags); + $files[] = strtolower(\Locale::composeLocale($tags)); $tag = array_pop($tags); } // include the default locale as the base if the most general locale requested is not the default @@ -124,15 +130,21 @@ class Lang { $files[] = $file; } // if we need to load all files, start with the fallback strings - if($files==$loaded) $strings[] = self::REQUIRED; + $strings = []; + if($files==$loaded) { + $strings[] = self::REQUIRED; + } else { + // otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca" + $strings[] = self::$strings; + } // read files in reverse order $files = array_reverse($files); foreach($files as $file) { - if(!file_exists(self::PATH."$file.php")) throw new Lang\Exception("fileMissing", $file); - if(!is_readable(self::PATH."$file.php")) throw new Lang\Exception("fileUnreadable", $file); + if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file); + if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file); try { ob_start(); - $arr = (include self::PATH."$file.php"); + $arr = (include self::$path."$file.php"); } catch(\Throwable $e) { $arr = null; } finally { diff --git a/vendor/JKingWeb/NewsSync/Lang/Exception.php b/vendor/JKingWeb/NewsSync/Lang/Exception.php index e0ea4c51..88291d4a 100644 --- a/vendor/JKingWeb/NewsSync/Lang/Exception.php +++ b/vendor/JKingWeb/NewsSync/Lang/Exception.php @@ -18,7 +18,7 @@ class Exception extends \JKingWeb\NewsSync\Exception { $code = self::CODES[$codeID]; $msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID"; } - \Exception::construct($msg, $code, $e); + \Exception::__construct($msg, $code, $e); } } } \ No newline at end of file