diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..350603f --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,95 @@ +# This is a basic workflow to help you get started with Actions + +name: Tests + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + pull_request: + branches: [ master ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + + - run: composer install --no-progress --prefer-dist + - run: composer phpstan -- --no-progress + continue-on-error: true + + phpcs: + name: PHP CS + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + + - run: composer install --no-progress --prefer-dist + - run: composer phpcs + continue-on-error: true + +# psalm: +# name: Psalm +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v3 +# - uses: shivammathur/setup-php@v2 +# with: +# php-version: 8.0 +# coverage: none +# +# - run: composer install --no-progress --prefer-dist +# - run: composer psalm -- --no-progress +# continue-on-error: true + + tests: + runs-on: ubuntu-latest + strategy: + matrix: + php: [ '8.2', '8.3', '8.4' ] + + fail-fast: false + + name: PHP ${{ matrix.php }} tests + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - run: composer install --no-progress --prefer-dist + - run: vendor/bin/tester tests -s -C + + code_coverage: + name: Code Coverage + runs-on: ubuntu-latest + needs: [tests] + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + + - run: composer install --no-progress --prefer-dist + - run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src +# - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar +# - env: +# COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# run: php php-coveralls.phar --verbose --config tests/.coveralls.yml diff --git a/.gitignore b/.gitignore index 50e0c90..6bd167c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /vendor /temp /composer.lock +.php-cs-fixer.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..7392061 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,15 @@ +in(__DIR__ . '/src') +; + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PER-CS' => true, + '@PHP82Migration' => true, + 'array_syntax' => ['syntax' => 'short'], + ]) + ->setIndent("\t") + ->setFinder($finder) + ; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0c8f5fb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,74 +0,0 @@ -language: php -php: - - 7.1 - - 7.2 - - 7.3 - - 7.4 - - 8.0snapshot - -env: - - PHP_BIN=php - - PHP_BIN=php-cgi - -before_install: - # turn off XDebug - - phpenv config-rm xdebug.ini || return 0 - -install: - - travis_retry composer install --no-progress --prefer-dist - -script: - - travis_retry vendor/bin/tester -p $PHP_BIN tests -s - -after_failure: - # Print *.actual content - - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done - -jobs: - include: - - name: Lowest Dependencies - env: PHP_BIN=php - install: - - travis_retry composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable - - - - name: Lint - install: - - travis_retry composer create-project jakub-onderka/php-parallel-lint temp/php-parallel-lint --no-interaction --no-progress - script: - - php temp/php-parallel-lint/parallel-lint src - - - - name: Code Checker - script: - - vendor/bin/phpcs --standard=psr12 src - - - - stage: Static Analysis (informative) - install: - - travis_retry composer install --no-progress --prefer-dist - script: - - vendor/bin/phpstan.phar analyse --level 8 src - - - - stage: Code Coverage - script: - - vendor/bin/tester -p phpdbg tests -s --coverage ./coverage.xml --coverage-src ./src - after_script: - - wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar - - php coveralls.phar --verbose --config tests/.coveralls.yml - - - allow_failures: - - stage: Static Analysis (informative) - - stage: Code Coverage - - -sudo: false - -cache: - directories: - - $HOME/.composer/cache - -notifications: - email: false diff --git a/README.md b/README.md index e3c3441..e46cc46 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ Bckp\Translator ==================== [![Downloads this Month](https://img.shields.io/packagist/dm/bckp/translator-core.svg)](https://packagist.org/packages/bckp/translator-core) -[![Build Status](https://travis-ci.org/bckp/translator-core.svg?branch=master)](https://travis-ci.org/bckp/translator-core) -[![Coverage Status](https://coveralls.io/repos/github/bckp/translator-core/badge.svg?branch=master)](https://coveralls.io/github/bckp/translator-core?branch=master) +[![Build Status](https://github.com/bckp/translator-core/actions/workflows/tests.yaml)](https://github.com/bckp/translator-core/actions/workflows/tests.yaml/badge.svg) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/bckp/translator-core/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/bckp/translator-core/?branch=master) [![Latest Stable Version](https://poser.pugx.org/bckp/translator-core/v/stable)](https://packagist.org/packages/bckp/translator-core) [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/application/blob/master/license.md) @@ -25,7 +24,7 @@ $compiledCatalogue = $catalogue->compile(); $translator = new Translator($compiledCatalogue); $translator->translate('errors.error.notFound'); // Will output "Soubor nenalezen" -$translator->translate(['messages.plural', 4]); // Will output "4 lidé" +$translator->translate('messages.plural', 4); // Will output "4 lidé" $translator->translate('messages.withArgs', 'Honza', 'poledne'); // Will output "Ahoj, já jsem Honza, přeji krásné poledne" $translator->translate('messages.withArgsRev', 'Honza', 'poledne'); // Will output "Krásné poledne, já jsem Honza" ``` diff --git a/composer.json b/composer.json index 0014ca7..c6fb6a4 100644 --- a/composer.json +++ b/composer.json @@ -1,29 +1,42 @@ { - "name": "bckp/translator-core", - "description": "Simple and fast PHP translator", - "keywords": ["translator"], - "homepage": "https://github.com/bckp/translator-core", - "license": "BSD-3-Clause", - "authors": [ - { - "name": "Radovan Kepák", - "homepage": "https://kepak.eu" - } - ], - "require": { - "php": "^7.1|^8.0", - "nette/php-generator": ">3.3 <4", - "nette/neon": ">=2.4" - }, - "suggest": { - "bckp/translator-nette": "For NETTE support" - }, - "autoload": { - "classmap": ["src/"] - }, - "require-dev": { - "squizlabs/php_codesniffer": "*", - "phpstan/phpstan": "^0.12", - "nette/tester": "^2.3" - } + "name": "bckp/translator-core", + "description": "Simple and fast PHP translator", + "keywords": [ + "translator" + ], + "homepage": "https://github.com/bckp/translator-core", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Radovan Kepák", + "homepage": "https://kepak.dev" + } + ], + "require": { + "php": "^8.2", + "nette/php-generator": ">=4 <5", + "nette/neon": ">3.3 <4" + }, + "suggest": { + "bckp/translator-nette": "For NETTE support" + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "scripts": { + "phpstan": "phpstan analyse", + "tests": "tester tests -s", + "tests-watch": "tester tests -s -w src", + "phpcs": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer check", + "phpcs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix" + }, + "require-dev": { + "roave/security-advisories": "dev-latest", + "phpstan/phpstan": "^2", + "nette/utils": "^4.0", + "nette/tester": "^2.5", + "friendsofphp/php-cs-fixer": "^3.75" + } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..4ae2171 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 8 + + paths: + - src diff --git a/ruleset.xml b/ruleset.xml deleted file mode 100644 index dee3d38..0000000 --- a/ruleset.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - bckp/translator-core coding standard. - - - - - diff --git a/src/Builder/Catalogue.php b/src/Builder/Catalogue.php deleted file mode 100644 index 10f099a..0000000 --- a/src/Builder/Catalogue.php +++ /dev/null @@ -1,396 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Bckp\Translator\Builder; - -use Bckp\Translator\BuilderException; -use Bckp\Translator\FileInvalidException; -use Bckp\Translator\ICatalogue; -use Bckp\Translator\PathInvalidException; -use Bckp\Translator\PluralProvider; -use Nette\Neon\Neon; -use Nette\PhpGenerator\Method; -use Nette\PhpGenerator\PhpFile; -use Throwable; - -use function class_exists; -use function file_exists; -use function file_get_contents; -use function file_put_contents; -use function filemtime; -use function is_readable; -use function is_writable; -use function strtolower; -use function uniqid; -use function unlink; - -use const PHP_VERSION_ID; - -/** - * Class Catalogue - * - * @package Bckp\Translator\Builder - */ -class Catalogue -{ - /** @var array */ - public $onCompile = []; - - /** @var array */ - public $onCheck = []; - - /** @var ICatalogue|null */ - private $catalogue; - - /** @var array */ - private $collection = []; - - /** @var array */ - private $dynamic = []; - - /** @var bool */ - private $debug; - - /** @var bool */ - private $loaded = false; - - /** @var string */ - private $locale; - - /** @var string */ - private $path; - - /** @var PluralProvider */ - private $plural; - - /** - * Catalogue constructor. - * - * @param PluralProvider $plural - * @param string $path - * @param string $locale - */ - public function __construct(PluralProvider $plural, string $path, string $locale) - { - if (!is_writable($path)) { - throw new PathInvalidException("Path '{$path}' is not writable."); - } - - $this->path = $path; - $this->plural = $plural; - $this->locale = strtolower($locale); - } - - /** - * @param string $file - * @return static - */ - public function addFile(string $file): self - { - $this->collection[] = $file; - return $this; - } - - /** - * @param string $resource - * @param callable $callback - * @return static - */ - public function addDynamic(string $resource, callable $callback): self - { - $this->dynamic[strtolower($resource)] = $callback; - return $this; - } - - /** - * @param int $attempt - * @return ICatalogue - * @throws Throwable - */ - public function rebuild(int $attempt = 0): ICatalogue - { - $filename = $this->path . '/' . $this->getName() . '.php'; - $this->unlink($filename); - return $this->compile($attempt); - } - - /** - * @return string - */ - protected function getName(): string - { - return $this->locale . 'Catalogue'; - } - - /** - * @param string $filename - */ - private function unlink(string $filename): void - { - /** @scrutinizer ignore-unhandled */ - @unlink($filename); // @ intentionally as file may not exists - $this->loaded = false; - } - - /** - * Compile catalogue (or load from cache if exists) - * - * @param int $rebuild - * @return ICatalogue - * @throws Throwable - */ - public function compile(int $rebuild = 0): ICatalogue - { - // Exception on to many rebuild try - if ($rebuild > 3) { - throw new BuilderException('Failed to build language file'); - } - - // Check for file exist, create if no - $filename = $this->path . '/' . $this->getName() . '.php'; - if (!file_exists($filename)) { - file_put_contents($filename, $this->compileCode()); - } - - // Link file and rebuild if error - try { - $this->checkForChanges($filename); - $this->link($filename); - - if (!$this->catalogue instanceof ICatalogue) { - throw new BuilderException('Catalogue is not implementing ICatalogue'); - } - - return $this->catalogue; - } catch (Throwable $e) { - $this->unlink($filename); - return $this->compile(++$rebuild); - } - } - - /** - * @param string $filename - */ - protected function checkForChanges(string $filename): void - { - if (!$this->debug) { - return; - } - - $cacheTime = (int)filemtime($filename); - foreach ($this->collection as $file) { - $file = new \SplFileInfo($file); - $fileTime = $file->getMTime(); - if ($fileTime > $cacheTime || ($this->catalogue && $fileTime > $this->catalogue->buildTime())) { - throw new BuilderException('Rebuild required'); - } - } - - $this->onCheck($cacheTime); - } - - /** - * Compile cache code - * - * @return string - */ - protected function compileCode(): string - { - // Load messages, then generate code - $messages = $this->getMessages(); - $this->onCompile($messages); - - do { - $className = $this->getName() . uniqid(); - } while (class_exists($className)); - - // File - $file = new PhpFile(); - $file->setStrictTypes(true); - $file->setComment('This file was auto-generated'); - - // Create class - $file->addUse('Bckp\Translator\PluralProvider'); - $class = $file->addClass($className); - $class->setExtends(\Bckp\Translator\Catalogue::class); - $class->setImplements([ICatalogue::class]); - $class->addComment("This file was auto-generated"); - - // Setup plural method - $method = $class->addMethod('plural'); - $plural = Method::from((array)$this->plural->getPlural($this->locale)); - $method->setParameters($plural->getParameters()); - $parameters = $method->getParameters(); - $method->setBody('return PluralProvider::?($?);', [$plural->getName(), key($parameters)]); - $method->setReturnNullable($plural->isReturnNullable()); - $method->setReturnType($plural->getReturnType()); - - // Messages & build time - $class->addProperty('locale', $this->getLocale())->setVisibility('protected'); - $class->addProperty('build', time())->setVisibility('protected'); - $class->addProperty('messages', $messages)->setStatic(true)->setVisibility('protected'); - - // Generate code - $code = (string)$file; - $code .= "\nreturn new {$class->getName()};\n"; - - // Return string - return $code; - } - - /** - * Get all messages - * - * @return string[] - * @throws FileInvalidException - */ - protected function getMessages(): array - { - $messages = []; - - // Add files - foreach ($this->collection as $file) { - $info = new \SplFileInfo($file); - $resource = strtolower($info->getBasename('.' . $this->locale . '.neon')); - foreach ($this->loadFile($file) as $key => $item) { - $messages[$resource . '.' . $key] = $item; - } - } - - // Add dynamic translations - foreach ($this->dynamic as $resource => $callback) { - $resource = $namespace = strtolower($resource); - $locale = $this->locale; - $array = []; - - $callback($array, $resource, $locale); - - // @phpstan-ignore-next-line - foreach ($array as $key => $item) { - $messages[$namespace . '.' . $key] = $item; - } - } - - return $messages; - } - - /** - * @param string $file - * @return string[] - * @throws PathInvalidException - * @throws FileInvalidException - */ - protected function loadFile(string $file): array - { - if (!file_exists($file) || !is_readable($file)) { - throw new PathInvalidException("File '{$file}' not found or is not readable."); - } - - $content = file_get_contents($file); - if (!$content) { - return []; - } - - try { - $content = Neon::decode($content); - if (!is_array($content)) { - throw new \Exception('No array'); - } - } catch (Throwable $e) { - throw new FileInvalidException( - "File '{$file}' do not contain array of translations", - $e->getCode(), - $e - ); - } - return $content; - } - - /** - * Occurs when new catalogue is compiled, after all strings are loaded - * @param array|string> $messages - */ - private function onCompile(array &$messages): void - { - foreach ($this->onCompile as $callback) { - $locale = $this->locale; - $callback($messages, $locale); - } - } - - /** - * @return string - */ - public function getLocale(): string - { - return $this->locale; - } - - /** - * Occurs on debug mode in check for changes - * @param int $fileTime - */ - private function onCheck(int $fileTime): void - { - foreach ($this->onCheck as $callback) { - $locale = $this->locale; - $callback($fileTime, $locale); - } - } - - /** - * Link catalogue if not already linked - * - * @param string $filename - * @throws BuilderException - */ - private function link(string $filename): void - { - if ($this->loaded) { - return; - } - - /** @noinspection PhpIncludeInspection */ - $this->catalogue = include $filename; - $this->loaded = true; - } - - /** - * @param callable $callback function(array &$messages, string $locale): void - */ - public function addCompileCallback(callable $callback): void - { - $this->onCompile[] = $callback; - } - - /** - * @param callable $callback function(string $locale): void - */ - public function addCheckCallback(callable $callback): void - { - $this->onCheck[] = $callback; - } - - /** - * Enable debug mode - * - * @param bool $debug - * @return static - */ - public function setDebugMode(bool $debug): self - { - $this->debug = $debug; - return $this; - } -} diff --git a/src/Catalogue.php b/src/Catalogue.php index eabe7b3..3f829e1 100644 --- a/src/Catalogue.php +++ b/src/Catalogue.php @@ -19,62 +19,25 @@ * * @package Bckp\Translator */ -abstract class Catalogue implements ICatalogue +abstract class Catalogue { - /** @var array */ - protected static $messages; - - /** @var int */ - protected $build; - - /** @var string */ - protected $locale; - - /** - * Get build time - * - * @return int - */ - public function buildTime(): int - { - return $this->build; - } - - /** - * Get the message translation - * - * @param string $message - * @return string|array return array if plural is detected - */ - public function get(string $message) - { - return static::$messages[$message] ?? ''; - } - - /** - * Check if catalogue has message translation - * - * @param string $message - * @return bool - */ - public function has(string $message): bool - { - return isset(static::$messages[$message]); - } - - /** - * @return string - */ - public function locale(): string - { - return $this->locale; - } - - /** - * Plural form getter - * - * @param int $n - * @return string - */ - abstract public function plural(int $n): string; + /** @var array> */ + protected static array $messages; + + /** + * @return string|array + */ + public function get(string $message): array|string + { + return static::$messages[$message] ?? ''; + } + + public function has(string $message): bool + { + return array_key_exists($message, static::$messages); + } + + abstract public function plural(int $n): Plural; + abstract public function locale(): string; + abstract public function build(): int; } diff --git a/src/CatalogueBuilder.php b/src/CatalogueBuilder.php new file mode 100644 index 0000000..4ab25fc --- /dev/null +++ b/src/CatalogueBuilder.php @@ -0,0 +1,340 @@ + + */ + +declare(strict_types=1); + +namespace Bckp\Translator; + +use Bckp\Translator\Exceptions\BuilderException; +use Bckp\Translator\Exceptions\FileInvalidException; +use Bckp\Translator\Exceptions\PathInvalidException; +use Nette\Neon\Neon; +use Nette\PhpGenerator\ClassType; +use Nette\PhpGenerator\Method; +use Nette\PhpGenerator\PhpFile; +use RuntimeException; +use SplFileInfo; +use Throwable; + +use function file_exists; +use function file_get_contents; +use function file_put_contents; +use function filemtime; +use function is_readable; +use function is_writable; +use function strtolower; +use function unlink; + +final class CatalogueBuilder +{ + /** @var callable[] */ + public array $onCompile = []; + + /** @var callable[] */ + public array $onCheck = []; + + /** @var callable[] */ + private array $dynamic = []; + + private ?Catalogue $catalogue = null; + + /** @var array */ + private array $collection = []; + + private bool $debug = false; + private bool $loaded = false; + private string $locale; + + + public function __construct( + private readonly PluralProvider $plural, + private readonly string $path, + string $locale + ) { + if (!is_writable($path)) { + throw new PathInvalidException("Path '$path' is not writable."); + } + + $this->locale = strtolower($locale); + } + + public function addFile(string $file): self + { + $this->collection[] = $file; + return $this; + } + + public function addDynamic(string $resource, callable $callback): self + { + $this->dynamic[strtolower($resource)] = $callback; + return $this; + } + + /** + * @throws Throwable + */ + public function rebuild(int $attempt = 0): Catalogue + { + $filename = $this->path . '/' . $this->getName() . '.php'; + $this->unlink($filename); + return $this->compile($attempt); + } + + /** + * @return string + */ + protected function getName(): string + { + return $this->locale . 'Catalogue'; + } + + /** + * @param string $filename + */ + private function unlink(string $filename): void + { + /** @scrutinizer ignore-unhandled */ + @unlink($filename); // @ intentionally as file may not exist + $this->loaded = false; + } + + /** + * @throws BuilderException + */ + public function compile(int $rebuild = 0): Catalogue + { + // Exception on to many rebuild try + if ($rebuild > 3) { + throw new BuilderException('Failed to build language file'); + } + + // Check for file exist, create if no + $filename = $this->path . '/' . $this->getName() . '.php'; + if (!file_exists($filename)) { + file_put_contents($filename, $this->compileCode()); + } + + // Link file and rebuild if error + try { + $this->checkForChanges($filename); + $this->link($filename); + + if (!$this->catalogue instanceof Catalogue) { + throw new BuilderException('Catalogue is not implementing Catalogue'); + } + + return $this->catalogue; + } catch (Throwable $e) { + $this->unlink($filename); + return $this->compile(++$rebuild); + } + } + + /** + * @param string $filename + */ + protected function checkForChanges(string $filename): void + { + if (!$this->debug) { + return; + } + + $cacheTime = (int) filemtime($filename); + foreach ($this->collection as $file) { + $file = new SplFileInfo($file); + $fileTime = $file->getMTime(); + if ($fileTime > $cacheTime || ($this->catalogue && $fileTime > $this->catalogue->build())) { + throw new BuilderException('Rebuild required'); + } + } + + $this->onCheck($cacheTime); + } + + /** + * Compile cache code + * + * @return string + */ + protected function compileCode(): string + { + // Load messages, then generate code + $messages = $this->getMessages(); + $this->onCompile($messages); + + /* + do { + $className = $this->getName() . substr(md5((string) mt_rand()), 4, 8); + } while (class_exists($className)); + */ + + // File + $file = new PhpFile(); + $file->setStrictTypes(); + $file->setComment('This file was auto-generated'); + + $class = new ClassType(); + $class->setExtends(Catalogue::class); + + // Setup plural method + $method = $class->addMethod('plural'); + $plural = Method::from((array) $this->plural->getPlural($this->locale)); + $method->setParameters($plural->getParameters()); + $parameters = $method->getParameters(); + $method->setBody('return Bckp\Translator\PluralProvider::?($?);', [$plural->getName(), key($parameters)]); + $method->setReturnNullable($plural->isReturnNullable()); + $method->setReturnType($plural->getReturnType()); + + // Messages & build time + $class->addMethod('locale')->setBody("return '{$this->getLocale()}';")->setReturnType('string'); + $class->addMethod('build')->setBody('return ' . time() . ';')->setReturnType('int'); + $class->addProperty('messages', $messages)->setType('array')->setStatic()->setVisibility('protected'); + + // Generate code + $code = (string) $file; + $code .= "\nreturn new class {$class};\n"; + + // Return string + return $code; + } + + /** + * Get all messages + * + * @return string[] + * @throws FileInvalidException + */ + protected function getMessages(): array + { + $messages = []; + + // Add files + foreach ($this->collection as $file) { + $info = new SplFileInfo($file); + $resource = strtolower($info->getBasename('.' . $this->locale . '.neon')); + foreach ($this->loadFile($file) as $key => $item) { + $messages[$resource . '.' . $key] = $item; + } + } + + // Add dynamic translations + foreach ($this->dynamic as $resource => $callback) { + $resource = $namespace = strtolower($resource); + $locale = $this->locale; + $array = []; + + $callback($array, $resource, $locale); + + // @phpstan-ignore-next-line + foreach ($array as $key => $item) { + $messages[$namespace . '.' . $key] = $item; + } + } + + return $messages; + } + + /** + * @param string $file + * @return string[] + * @throws PathInvalidException + * @throws FileInvalidException + */ + protected function loadFile(string $file): array + { + if (!file_exists($file) || !is_readable($file)) { + throw new PathInvalidException("File '$file' not found or is not readable."); + } + + $content = file_get_contents($file); + if (!$content) { + return []; + } + + try { + $content = Neon::decode($content); + if (!is_array($content)) { + throw new RuntimeException('No array'); + } + } catch (Throwable $e) { + throw new FileInvalidException( + "File '$file' do not contain array of translations", + $e->getCode(), + $e + ); + } + return $content; + } + + /** + * Occurs when new catalogue is compiled, after all strings are loaded + * @param array|string> $messages + */ + private function onCompile(array &$messages): void + { + foreach ($this->onCompile as $callback) { + $locale = $this->locale; + $callback($messages, $locale); + } + } + + public function getLocale(): string + { + return $this->locale; + } + + private function onCheck(int $fileTime): void + { + foreach ($this->onCheck as $callback) { + $locale = $this->locale; + $callback($fileTime, $locale); + } + } + + /** + * Link catalogue if not already linked + * + * @param string $filename + * @throws BuilderException + */ + private function link(string $filename): void + { + if ($this->loaded) { + return; + } + + $this->catalogue = require $filename; + $this->loaded = true; + } + + /** + * @param callable $callback function(array &$messages, string $locale): void + */ + public function addCompileCallback(callable $callback): void + { + $this->onCompile[] = $callback; + } + + /** + * @param callable $callback function(string $locale): void + */ + public function addCheckCallback(callable $callback): void + { + $this->onCheck[] = $callback; + } + + public function setDebugMode(bool $debug): self + { + $this->debug = $debug; + return $this; + } +} diff --git a/src/Diagnostics/Diagnostics.php b/src/Diagnostics/Diagnostics.php index b58a653..aae364c 100644 --- a/src/Diagnostics/Diagnostics.php +++ b/src/Diagnostics/Diagnostics.php @@ -14,69 +14,54 @@ namespace Bckp\Translator\Diagnostics; -use Bckp\Translator\IDiagnostics; +use Bckp\Translator\Interfaces; use function array_unique; -/** - * Class Diagnostics - * - * @package Bckp\Translator\Diagnostics - */ -class Diagnostics implements IDiagnostics +class Diagnostics implements Interfaces\Diagnostics { - /** @var string */ - private $locale = ''; + /** @var string */ + private string $locale = ''; - /** @var array */ - private $messages = []; + /** @var array */ + private array $messages = []; - /** @var array */ - private $untranslated = []; + /** @var array */ + private array $untranslated = []; - /** - * @return string - */ - public function getLocale(): string - { - return $this->locale; - } + public function getLocale(): string + { + return $this->locale; + } - /** - * @return array - */ - public function getUntranslated(): array - { - return array_unique($this->untranslated); - } + /** + * @return string[] + */ + public function getUntranslated(): array + { + return array_unique($this->untranslated); + } - /** - * @return array - */ - public function getWarnings(): array - { - return array_unique($this->messages); - } + /** + * @return string[] + */ + public function getWarnings(): array + { + return array_unique($this->messages); + } - /** @param string $locale */ - public function setLocale(string $locale): void - { - $this->locale = $locale; - } + public function setLocale(string $locale): void + { + $this->locale = $locale; + } - /** - * @param string $message - */ - public function untranslated(string $message): void - { - $this->untranslated[] = $message; - } + public function untranslated(string $message): void + { + $this->untranslated[] = $message; + } - /** - * @param string $message - */ - public function warning(string $message): void - { - $this->messages[] = $message; - } + public function warning(string $message): void + { + $this->messages[] = $message; + } } diff --git a/src/Exceptions/BuilderException.php b/src/Exceptions/BuilderException.php index 8e2f47b..69ce201 100644 --- a/src/Exceptions/BuilderException.php +++ b/src/Exceptions/BuilderException.php @@ -12,8 +12,6 @@ declare(strict_types=1); -namespace Bckp\Translator; +namespace Bckp\Translator\Exceptions; -class BuilderException extends TranslatorException -{ -} +class BuilderException extends TranslatorException {} diff --git a/src/Exceptions/FileInvalidException.php b/src/Exceptions/FileInvalidException.php index 030340d..56d4279 100644 --- a/src/Exceptions/FileInvalidException.php +++ b/src/Exceptions/FileInvalidException.php @@ -12,10 +12,8 @@ declare(strict_types=1); -namespace Bckp\Translator; +namespace Bckp\Translator\Exceptions; use Nette\InvalidStateException; -class FileInvalidException extends InvalidStateException -{ -} +class FileInvalidException extends InvalidStateException {} diff --git a/src/Exceptions/PathInvalidException.php b/src/Exceptions/PathInvalidException.php index 05e7838..79e0ded 100644 --- a/src/Exceptions/PathInvalidException.php +++ b/src/Exceptions/PathInvalidException.php @@ -12,12 +12,10 @@ declare(strict_types=1); -namespace Bckp\Translator; +namespace Bckp\Translator\Exceptions; use Nette\FileNotFoundException; use Nette\InvalidStateException; use RuntimeException; -class PathInvalidException extends FileNotFoundException -{ -} +class PathInvalidException extends FileNotFoundException {} diff --git a/src/Exceptions/TranslatorException.php b/src/Exceptions/TranslatorException.php index 1ec2b21..c88f8f5 100644 --- a/src/Exceptions/TranslatorException.php +++ b/src/Exceptions/TranslatorException.php @@ -12,10 +12,8 @@ declare(strict_types=1); -namespace Bckp\Translator; +namespace Bckp\Translator\Exceptions; use RuntimeException; -class TranslatorException extends RuntimeException -{ -} +class TranslatorException extends RuntimeException {} diff --git a/src/Interfaces/Diagnostics.php b/src/Interfaces/Diagnostics.php new file mode 100644 index 0000000..1f24369 --- /dev/null +++ b/src/Interfaces/Diagnostics.php @@ -0,0 +1,22 @@ + + */ + +namespace Bckp\Translator\Interfaces; + +interface Diagnostics +{ + public function setLocale(string $locale): void; + public function untranslated(string $message): void; + public function warning(string $message): void; +} diff --git a/src/Interfaces/ICatalogue.php b/src/Interfaces/ICatalogue.php deleted file mode 100644 index 9f7395c..0000000 --- a/src/Interfaces/ICatalogue.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Bckp\Translator; - -/** - * Interface ICatalogue - * - * @package Bckp\Translator - */ -interface ICatalogue -{ - /** - * Get build time in seconds - * @return int - */ - public function buildTime(): int; - - /** - * Get the message translation - * @param string $message - * @return string|string[] return array if plural is detected - */ - public function get(string $message); - - /** - * Check if catalogue has message translation - * @param string $message - * @return bool - */ - public function has(string $message): bool; - - /** - * Get locale - * @return string - */ - public function locale(): string; - - /** - * Plural form getter - * @param int $n - * @return string - */ - public function plural(int $n): string; -} diff --git a/src/Interfaces/IDiagnostics.php b/src/Interfaces/IDiagnostics.php deleted file mode 100644 index 34e1548..0000000 --- a/src/Interfaces/IDiagnostics.php +++ /dev/null @@ -1,38 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Bckp\Translator; - -/** - * Interface IDiagnostics - * - * @package Bckp\Translator - */ -interface IDiagnostics -{ - /** - * @param string $locale - */ - public function setLocale(string $locale): void; - - /** - * @param string $message - */ - public function untranslated(string $message): void; - - /** - * @param string $message - */ - public function warning(string $message): void; -} diff --git a/src/Interfaces/IPlural.php b/src/Interfaces/IPlural.php deleted file mode 100644 index 3224784..0000000 --- a/src/Interfaces/IPlural.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Bckp\Translator; - -/** - * Interface IPlural - * - * @package Bckp\Translator - */ -interface IPlural -{ - /** - * Plural variants - */ - public const - ZERO = 'zero', - ONE = 'one', - TWO = 'two', - FEW = 'few', - MANY = 'many', - OTHER = 'other'; - - /** - * Get plural method - * @param string $locale - * @return callable - */ - public function getPlural(string $locale): callable; -} diff --git a/src/Interfaces/ITranslator.php b/src/Interfaces/ITranslator.php deleted file mode 100644 index c5865dd..0000000 --- a/src/Interfaces/ITranslator.php +++ /dev/null @@ -1,35 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Bckp\Translator; - -/** - * Interface ITranslator - * - * @package Bckp\Translator - */ -interface ITranslator -{ - /** - * @param callable $callback function(string $string): string - */ - public function setNormalizeCallback(callable $callback): void; - - /** - * @param array|string|object $message - * @param mixed ...$params - * @return string - */ - public function translate($message, ...$params): string; -} diff --git a/src/Interfaces/Translator.php b/src/Interfaces/Translator.php new file mode 100644 index 0000000..79c5c84 --- /dev/null +++ b/src/Interfaces/Translator.php @@ -0,0 +1,32 @@ + + */ + +declare(strict_types=1); + +namespace Bckp\Translator\Interfaces; + +use Stringable; + +/** + * Interface ITranslator + * + * @package Bckp\Translator + */ +interface Translator +{ + /** + * @param callable $callback function(string $string): string + */ + public function setNormalizeCallback(callable $callback): void; + + public function translate(string|Stringable $message, mixed ...$params): string; +} diff --git a/src/Plural.php b/src/Plural.php new file mode 100644 index 0000000..fa6bafb --- /dev/null +++ b/src/Plural.php @@ -0,0 +1,15 @@ + 'csPlural', - 'en' => 'enPlural', - 'id' => 'zeroPlural', - 'ja' => 'zeroPlural', - 'ka' => 'zeroPlural', - 'ko' => 'zeroPlural', - 'lo' => 'zeroPlural', - 'ms' => 'zeroPlural', - 'my' => 'zeroPlural', - 'th' => 'zeroPlural', - 'vi' => 'zeroPlural', - 'zh' => 'zeroPlural', - ]; - - /** - * Czech plural selector (zero-one-few-other) - * - * @param int|null $n - * @return string - */ - public static function csPlural(?int $n): string - { - return $n === 0 - ? IPlural::ZERO - : ($n === 1 - ? IPlural::ONE - : ($n >= 2 && $n < 5 - ? IPlural::FEW - : IPlural::OTHER - ) - ); - } - - /** - * Default plural detector (zero-one-other) - * - * @param int|null $n - * @return string - */ - public static function enPlural(?int $n): string - { - return $n === 0 - ? IPlural::ZERO - : ($n === 1 - ? IPlural::ONE - : IPlural::OTHER - ); - } + /** + * Czech plural selector (zero-one-few-other) + */ + public static function csPlural(?int $n): Plural + { + return match (true) { + $n === 0 => Plural::Zero, + $n === 1 => Plural::One, + $n >= 2 && $n <= 4 => Plural::Few, + default => Plural::Other, + }; + } - /** - * No plural detector (zero-other) - * - * @param int|null $n - * @return string - */ - public static function zeroPlural(?int $n): string - { - return $n === 0 - ? IPlural::ZERO - : IPlural::OTHER; - } + /** + * Default plural detector (zero-one-other) + */ + public static function enPlural(?int $n): Plural + { + return match (true) { + $n === 0 => Plural::Zero, + $n === 1 => Plural::One, + default => Plural::Other, + }; + } - /** - * Get plural method - * - * @param string $locale - * @return callable(int|null $n): string - */ - public function getPlural(string $locale): callable - { - $locale = strtolower($locale); - $callable = [$this, $this->plurals[$locale] ?? null]; + /** + * No plural detector (zero-other) + */ + public static function zeroPlural(?int $n): Plural + { + return $n === 0 + ? Plural::Zero + : Plural::Other; + } - if ($callable[1] && is_callable($callable)) { - return $callable; - } - return [$this, self::DEFAULT]; - } + public function getPlural(string $locale): callable + { + return match (strtolower($locale)) { + 'cs' => [$this, 'csPlural'], + 'id', 'ja', 'ka', 'ko', 'lo', 'ms', 'my', 'th', 'vi', 'zh' => [$this, 'zeroPlural'], + default => [$this, 'enPlural'], + }; + } } diff --git a/src/Translator.php b/src/Translator.php index 7309f7e..bc110d2 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -14,14 +14,12 @@ namespace Bckp\Translator; +use Bckp\Translator\Interfaces\Diagnostics; +use Stringable; + use function array_key_exists; -use function end; -use function gettype; +use function array_key_last; use function is_array; -use function is_object; -use function is_string; -use function key; -use function method_exists; use function vsprintf; /** @@ -29,179 +27,98 @@ * * @package Bckp\Translator */ -class Translator implements ITranslator +class Translator implements Interfaces\Translator { - /** @var ICatalogue */ - private $catalogue; - - /** @var IDiagnostics|null */ - private $diagnostics; - - /** @var callable function(string $string): string */ - private $normalizeCallback; - - /** - * Translator constructor. - * - * @param ICatalogue $catalogue - * @param IDiagnostics|null $diagnostics - */ - public function __construct(ICatalogue $catalogue, IDiagnostics $diagnostics = null) - { - $this->catalogue = $catalogue; - $this->normalizeCallback = [$this, 'normalize']; - if ($this->diagnostics = $diagnostics) { - $this->diagnostics->setLocale($catalogue->locale()); - } - } - - /** - * Normalize string to preserve frameworks placeholders - * - * @param string $string - * @return string - */ - public function normalize(string $string): string - { - return str_replace( - ['%label', '%value', '%name'], - ['%%label', '%%value', '%%name'], - $string - ); - } - - /** - * @param callable $callback - * @return void - */ - public function setNormalizeCallback(callable $callback): void - { - $this->normalizeCallback = $callback; - } - - /** - * @param mixed $message - * @param mixed ...$parameters - * @return string - */ - public function translate($message, ...$parameters): string - { - if (empty($message)) { - return ''; - } - - $form = null; - - $this->expandParameters($parameters, $message); - $message = $this->getMessage($message, $form); - if ($message === null) { - return $this->warn('Expected string|array|object::__toString, but %s given.', gettype($message)); - } - $result = $message; - - // process plural if any - if ($translation = $this->catalogue->get($message)) { - $result = $this->getVariant($message, $translation, $form); - - if ($parameters) { - $result = ($this->normalizeCallback)($result); - $result = @vsprintf($result, $parameters); - // Intentionally @ as argument count can mismatch - } - } else { - $this->untranslated((string)$message); - } - - return $result; - } - - /** - * @param string $message - * @param string|array $translation - * @param string|null $form - * @return string - */ - private function getVariant(string $message, $translation, string $form = null): string - { - if (!is_array($translation)) { - return $translation; - } - - if ($form === null || !array_key_exists($form, $translation)) { - $this->warn( - 'Plural form not defined. (message: %s, form: %s)', - (string)$message, - (string)$form - ); - end($translation); - $form = key($translation); - } - return $translation[$form]; - } - - /** - * @param mixed $message - * @param string|null $plural - * @return string|null - */ - protected function getMessage($message, ?string &$plural): ?string - { - if (is_string($message)) { - return $message; - } - - if (is_array($message) && is_string($message[0])) { - $plural = $this->catalogue->plural((int)$message[1] ?? 1); - return $message[0]; - } - - if (is_object($message) && method_exists($message, '__toString')) { - return (string)$message; - } - - return null; - } - - /** - * @param string $message - */ - protected function untranslated(string $message): void - { - if ($this->diagnostics !== null) { - $this->diagnostics->untranslated($message); - } - } - - /** - * @param string $message - * @param mixed ...$parameters - * @return string - */ - protected function warn(string $message, ...$parameters): string - { - if (!empty($parameters)) { - $message = @vsprintf($message, $parameters); - } // Intentionally @ as parameter count can mismatch - - if ($this->diagnostics !== null) { - $this->diagnostics->warning($message); - } - - return $message; - } - - /** - * @param array $parameters - * @param string|array|object $message - */ - private function expandParameters(array &$parameters, $message): void - { - if ( - empty($parameters) - && is_array($message) - && is_numeric($message[1] ?? null) - ) { - $parameters[] = $message[1]; - } - } + /** @var callable function(string $string): string */ + private $normalizeCallback; + + public function __construct( + private readonly Catalogue $catalogue, + private readonly ?Diagnostics $diagnostics = null + ) { + $this->normalizeCallback = [$this, 'normalize']; + $this->diagnostics?->setLocale($catalogue->locale()); + } + + public function normalize(string $string): string + { + return str_replace( + ['%label', '%value', '%name'], + ['%%label', '%%value', '%%name'], + $string + ); + } + + public function setNormalizeCallback(callable $callback): void + { + $this->normalizeCallback = $callback; + } + + public function translate(string|Stringable $message, mixed ...$parameters): string + { + $message = (string) $message; + + if (empty($message)) { + return ''; + } + + $translation = $this->catalogue->get($message); + if (!$translation) { + $this->untranslated($message); + return $message; + } + + // Plural option is returned, we need to choose the right one + if (is_array($translation)) { + $plural = is_numeric($parameters[0] ?? null) ? $this->catalogue->plural((int) $parameters[0]) : Plural::Other; + $translation = $this->getVariant($message, $translation, $plural); + } + + if (!empty($parameters)) { + $translation = ($this->normalizeCallback)($translation); + $translation = @vsprintf($translation, $parameters); + } + + return $translation; + } + + /** + * @param array $translations + */ + public function getVariant(string $message, array $translations, Plural $plural): string + { + if (!array_key_exists($plural->value, $translations)) { + $this->warn( + 'Plural form not defined. (message: %s, form: %s)', + $message, + $plural->value, + ); + } + + return $translations[$plural->value] ?? $translations[array_key_last($translations)]; + } + + /** + * @param string $message + */ + protected function untranslated(string $message): void + { + $this->diagnostics?->untranslated($message); + } + + /** + * @param string $message + * @param mixed ...$parameters + * @return string + */ + protected function warn(string $message, ...$parameters): string + { + if (!empty($parameters)) { + $message = @vsprintf($message, $parameters); + } // Intentionally @ as parameter count can mismatch + + $this->diagnostics?->warning($message); + + return $message; + } } diff --git a/src/TranslatorProvider.php b/src/TranslatorProvider.php index b340bda..3e8b7cd 100644 --- a/src/TranslatorProvider.php +++ b/src/TranslatorProvider.php @@ -14,78 +14,60 @@ namespace Bckp\Translator; -use Bckp\Translator\Builder\Catalogue as BuilderCatalogue; +use Bckp\Translator\Exceptions\BuilderException; +use Bckp\Translator\Exceptions\TranslatorException; use function strtolower; -/** - * Class TranslatorProvider - * - * @package Bckp\Translator - */ class TranslatorProvider { - /** @var BuilderCatalogue[] */ - protected $catalogues = []; - - /** @var IDiagnostics|null */ - protected $diagnostics = null; - - /** @var string[] */ - protected $languages = []; + /** + * @var array + */ + protected array $catalogues = []; - /** @var ITranslator[] */ - protected $translators = []; + /** + * @var array + */ + protected array $translators = []; - /** - * TranslatorProvider constructor. - * - * @param string[] $languages - * @param IDiagnostics|null $diagnostics - */ - public function __construct(array $languages, IDiagnostics $diagnostics = null) - { - $this->diagnostics = $diagnostics; - $this->languages = $languages; - } + /** + * @param array $languages + */ + public function __construct( + protected array $languages, + protected ?Interfaces\Diagnostics $diagnostics = null + ) {} - /** - * @param string $locale - * @param BuilderCatalogue $builder - * @return void - */ - public function addCatalogue(string $locale, BuilderCatalogue $builder): void - { - $locale = strtolower($locale); - $this->catalogues[$locale] = $builder; - } + public function addCatalogue( + string $locale, + CatalogueBuilder $builder + ): void { + $this->catalogues[strtolower($locale)] = $builder; + } - /** - * @param string $locale - * @return ITranslator - * @throws \Throwable - */ - public function getTranslator(string $locale): ITranslator - { - $locale = strtolower($locale); - if (!isset($this->translators[$locale])) { - $this->translators[$locale] = $this->createTranslator($locale); - } + public function getTranslator(string $locale): Interfaces\Translator + { + $locale = strtolower($locale); + if (!isset($this->translators[$locale])) { + $this->translators[$locale] = $this->createTranslator($locale); + } - return $this->translators[$locale]; - } + return $this->translators[$locale]; + } - /** - * @param string $locale - * @return ITranslator - * @throws \Throwable - */ - protected function createTranslator(string $locale): ITranslator - { - if (!isset($this->catalogues[$locale])) { - throw new TranslatorException("Language {$locale} requested, but corresponding catalogue missing."); - } + /** + * @throws BuilderException + */ + protected function createTranslator(string $locale): Interfaces\Translator + { + if (!isset($this->catalogues[$locale])) { + throw new TranslatorException("Language {$locale} requested, but corresponding catalogue missing."); + } - return new Translator($this->catalogues[$locale]->compile(), $this->diagnostics); - } + return new Translator( + $this->catalogues[$locale]->compile(), + $this->diagnostics + ); + } } diff --git a/tests/Catalogue/Catalogue.dynamic.phpt b/tests/Catalogue/Catalogue.dynamic.phpt index 2123498..faf69a4 100644 --- a/tests/Catalogue/Catalogue.dynamic.phpt +++ b/tests/Catalogue/Catalogue.dynamic.phpt @@ -11,12 +11,11 @@ $plural = (new PluralProvider()); const LOCALE = 'dynamic'; const RESOURCE = 'test'; -$catalogue = new Catalogue($plural, TEMP_DIR, LOCALE); +$catalogue = new CatalogueBuilder($plural, TEMP_DIR, LOCALE); $catalogue->addDynamic(RESOURCE, function (array &$messages, string &$resource, string &$locale) { # Verify callback is called properly Assert::equal($locale, LOCALE); Assert::equal($resource, RESOURCE); - Assert::true(is_array($messages)); # Add string $messages['string'] = 'test'; diff --git a/tests/Catalogue/Catalogue.onCheck.phpt b/tests/Catalogue/Catalogue.onCheck.phpt index 835232f..d9285b7 100644 --- a/tests/Catalogue/Catalogue.onCheck.phpt +++ b/tests/Catalogue/Catalogue.onCheck.phpt @@ -2,7 +2,7 @@ namespace Bckp\Translator; -use Bckp\Translator\Builder\Catalogue; +use Bckp\Translator\Exceptions\BuilderException; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; @@ -11,7 +11,7 @@ $plural = (new PluralProvider()); const LOCALE = 'dynamic'; const RESOURCE = 'test'; -$catalogue = new Catalogue($plural, TEMP_DIR, LOCALE); +$catalogue = new CatalogueBuilder($plural, TEMP_DIR, LOCALE); $catalogue->addDynamic(RESOURCE, function (array &$messages) { $messages['string'] = 'test'; }); @@ -19,21 +19,21 @@ $catalogue->addCheckCallback(function () { throw new BuilderException('Rebuild required'); }); -Assert::noError(function () use ($catalogue) { +Assert::noError(static function () use ($catalogue) { $catalogue->compile(); }); -Assert::exception(function () use ($catalogue) { +Assert::exception(static function () use ($catalogue) { $catalogue->setDebugMode(true); $catalogue->compile(); }, BuilderException::class); -Assert::exception(function () use ($catalogue) { +Assert::exception(static function () use ($catalogue) { $catalogue->setDebugMode(true); $catalogue->compile(2); }, BuilderException::class); -Assert::exception(function () use ($catalogue) { +Assert::exception(static function () use ($catalogue) { $catalogue->setDebugMode(true); $catalogue->compile(3); }, BuilderException::class); diff --git a/tests/Catalogue/Catalogue.onCompile.phpt b/tests/Catalogue/Catalogue.onCompile.phpt index aea49ff..07350da 100644 --- a/tests/Catalogue/Catalogue.onCompile.phpt +++ b/tests/Catalogue/Catalogue.onCompile.phpt @@ -11,12 +11,11 @@ $plural = (new PluralProvider()); const LOCALE = 'dynamic'; const RESOURCE = 'test'; -$catalogue = new Catalogue($plural, TEMP_DIR, LOCALE); +$catalogue = new CatalogueBuilder($plural, TEMP_DIR, LOCALE); $catalogue->addDynamic(RESOURCE, function (array &$messages, string &$resource, string &$locale) { # Verify callback is called properly Assert::equal($locale, LOCALE); Assert::equal($resource, RESOURCE); - Assert::true(is_array($messages)); # Add string $messages['string'] = 'test'; diff --git a/tests/Catalogue/Catalogue.phpt b/tests/Catalogue/CatalogueBuilder.phpt similarity index 72% rename from tests/Catalogue/Catalogue.phpt rename to tests/Catalogue/CatalogueBuilder.phpt index a02fa5f..a202f55 100644 --- a/tests/Catalogue/Catalogue.phpt +++ b/tests/Catalogue/CatalogueBuilder.phpt @@ -2,41 +2,43 @@ namespace Bckp\Translator; -use Bckp\Translator\Builder\Catalogue; +use Bckp\Translator\Exceptions\BuilderException; +use Bckp\Translator\Exceptions\FileInvalidException; +use Bckp\Translator\Exceptions\PathInvalidException; use Tester\Assert; use Tester\Environment; require __DIR__ . '/../bootstrap.php'; $plural = (new PluralProvider()); -Assert::exception(function () use ($plural) { - new Catalogue($plural, '/no-exists', 'x1'); +Assert::exception(static function () use ($plural) { + new CatalogueBuilder($plural, '/no-exists', 'x1'); }, PathInvalidException::class); @unlink(TEMP_DIR . '/x1Catalogue.php'); -Assert::exception(function () use ($plural) { - $catalogue = new Catalogue($plural, TEMP_DIR, 'x2'); +Assert::exception(static function () use ($plural) { + $catalogue = new CatalogueBuilder($plural, TEMP_DIR, 'x2'); $catalogue->addFile('not-exists'); $catalogue->compile(); }, PathInvalidException::class); @unlink(TEMP_DIR . '/x2Catalogue.php'); -Assert::exception(function () use ($plural) { - $catalogue = new Catalogue($plural, TEMP_DIR, 'x3'); +Assert::exception(static function () use ($plural) { + $catalogue = new CatalogueBuilder($plural, TEMP_DIR, 'x3'); $catalogue->compile(4); }, BuilderException::class); @unlink(TEMP_DIR . '/x3Catalogue.php'); -Assert::exception(function () use ($plural) { - $catalogue = new Catalogue($plural, TEMP_DIR, 'x4'); +Assert::exception(static function () use ($plural) { + $catalogue = new CatalogueBuilder($plural, TEMP_DIR, 'x4'); $catalogue->addFile('./translations/broken.xx.neon'); $catalogue->compile(2); }, FileInvalidException::class); @unlink(TEMP_DIR . '/x4Catalogue.php'); -Assert::exception(function () use ($plural) { - $catalogue = new Catalogue($plural, TEMP_DIR, 'x5'); +Assert::exception(static function () use ($plural) { + $catalogue = new CatalogueBuilder($plural, TEMP_DIR, 'x5'); $catalogue->addFile('./translations/string.xx.neon'); $catalogue->compile(2); }, FileInvalidException::class); @unlink(TEMP_DIR . '/x5Catalogue.php'); -Assert::exception(function () use ($plural) { +Assert::exception(static function () use ($plural) { @unlink(TEMP_DIR . '/x6Catalogue.php'); file_put_contents(TEMP_DIR . '/x6Catalogue.php', 'compile(3); }, BuilderException::class); @unlink(TEMP_DIR . '/x7Catalogue.php'); @@ -62,31 +64,31 @@ return new Class{ } }; '); -$catalogue = new Catalogue($plural, TEMP_DIR, 'x7'); +$catalogue = new CatalogueBuilder($plural, TEMP_DIR, 'x7'); $catalogue->addFile('./translations/test.cs.neon'); $compiled = $catalogue->compile(2); -Assert::type(ICatalogue::class, $compiled); +Assert::type(Catalogue::class, $compiled); Assert::type('string', $compiled->get('test.welcome')); @unlink(TEMP_DIR . '/x7Catalogue.php'); // Rebuild 2 -$catalogue = new Catalogue($plural, TEMP_DIR, 'x8'); +$catalogue = new CatalogueBuilder($plural, TEMP_DIR, 'x8'); $catalogue->addFile('./translations/test.cs.neon'); file_put_contents(TEMP_DIR . '/x8Catalogue.php', 'compile(2); Assert::same('x8', $catalogue->getLocale()); -Assert::type(ICatalogue::class, $compiled); +Assert::type(Catalogue::class, $compiled); @unlink(TEMP_DIR . '/x8Catalogue.php'); -$catalogue = new Catalogue($plural, TEMP_DIR, 'CS'); +$catalogue = new CatalogueBuilder($plural, TEMP_DIR, 'CS'); $catalogue->addFile('./translations/test.cs.neon'); $catalogue->addFile('./translations/blank.cs.neon'); Assert::same('cs', $catalogue->getLocale()); Assert::same('cs', $catalogue->compile()->locale()); $compiled = $catalogue->compile(); -Assert::type(ICatalogue::class, $compiled); -Assert::true(filemtime('./translations/test.cs.neon') < $compiled->buildTime()); +Assert::type(Catalogue::class, $compiled); +Assert::true(filemtime('./translations/test.cs.neon') < $compiled->build()); Assert::true($compiled->has('test.welcome')); Assert::false($compiled->has('not-exists')); @@ -102,13 +104,13 @@ Assert::equal([ 'other' => '%d lidí', ], $compiled->get('test.plural')); -$catalogue = new Catalogue($plural, TEMP_DIR, 'EN'); +$catalogue = new CatalogueBuilder($plural, TEMP_DIR, 'EN'); $catalogue->addFile('./translations/test.en.neon'); Assert::same('en', $catalogue->getLocale()); $compiled = $catalogue->compile(); -Assert::type(ICatalogue::class, $compiled); -Assert::true(filemtime('./translations/test.en.neon') < $compiled->buildTime()); +Assert::type(Catalogue::class, $compiled); +Assert::true(filemtime('./translations/test.en.neon') < $compiled->build()); Assert::same('en', $compiled->locale()); // en catalogue @@ -128,13 +130,13 @@ file_put_contents(TEMP_DIR . '/x9Catalogue.php', file_get_contents('assets/x9cat touch(TEMP_DIR . '/x9Catalogue.php', $time); if (filemtime(TEMP_DIR . '/x9Catalogue.php') === $time) { touch('./translations/blank.cs.neon'); - $catalogue = new Catalogue($plural, TEMP_DIR, 'x9'); + $catalogue = new CatalogueBuilder($plural, TEMP_DIR, 'x9'); $catalogue->setDebugMode(true); $catalogue->addFile('./translations/test.cs.neon'); $catalogue->addFile('./translations/blank.cs.neon'); $compiled = $catalogue->compile(); - Assert::true($compiled->buildTime() > $time); + Assert::true($compiled->build() > $time); } else { Environment::skip('Skipped test touch and debug'); } diff --git a/tests/Catalogue/Catalogue.rebuild.phpt b/tests/Catalogue/CatalogueBuilder.rebuild.phpt similarity index 67% rename from tests/Catalogue/Catalogue.rebuild.phpt rename to tests/Catalogue/CatalogueBuilder.rebuild.phpt index 7e1248a..1b3389b 100644 --- a/tests/Catalogue/Catalogue.rebuild.phpt +++ b/tests/Catalogue/CatalogueBuilder.rebuild.phpt @@ -2,7 +2,6 @@ namespace Bckp\Translator; -use Bckp\Translator\Builder\Catalogue; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; @@ -11,14 +10,14 @@ $plural = (new PluralProvider()); const LOCALE = 'dynamic'; const RESOURCE = 'test'; -$catalogue = new Catalogue($plural, TEMP_DIR, LOCALE); +$catalogue = new CatalogueBuilder($plural, TEMP_DIR, LOCALE); $catalogue->addDynamic(RESOURCE, function (array &$messages) { $messages['string'] = 'test'; }); $compiled = $catalogue->compile(); -$buildTime = $compiled->buildTime(); +$buildTime = $compiled->build(); sleep(2); $compiled = $catalogue->rebuild(); -Assert::notEqual($buildTime, $compiled->buildTime()); +Assert::notEqual($buildTime, $compiled->build()); diff --git a/tests/Diagnostics/Panel.phpt b/tests/Diagnostics/Panel.phpt index 35a30f3..c619d66 100644 --- a/tests/Diagnostics/Panel.phpt +++ b/tests/Diagnostics/Panel.phpt @@ -3,7 +3,6 @@ namespace Bckp\Translator; use Bckp\Translator\Diagnostics\Diagnostics; -use Bckp\Translator\Diagnostics\Panel; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Plural/Plural.phpt b/tests/Plural/Plural.phpt index 58281d8..2494dd6 100644 --- a/tests/Plural/Plural.phpt +++ b/tests/Plural/Plural.phpt @@ -16,33 +16,33 @@ Assert::type('callable', $provider->getPlural('non-sense')); $plural = $provider->getPlural('cs'); Assert::same('csPlural', $plural[1]); Assert::same(PluralProvider::csPlural(0), $plural(0)); -Assert::same(IPlural::ZERO, $plural(0)); +Assert::same(Plural::Zero, $plural(0)); Assert::same(PluralProvider::csPlural(1), $plural(1)); -Assert::same(IPlural::ONE, $plural(1)); +Assert::same(Plural::One, $plural(1)); Assert::same(PluralProvider::csPlural(2), $plural(2)); -Assert::same(IPlural::FEW, $plural(2)); +Assert::same(Plural::Few, $plural(2)); Assert::same(PluralProvider::csPlural(3), $plural(3)); -Assert::same(IPlural::FEW, $plural(3)); +Assert::same(Plural::Few, $plural(3)); Assert::same(PluralProvider::csPlural(4), $plural(4)); -Assert::same(IPlural::FEW, $plural(4)); +Assert::same(Plural::Few, $plural(4)); Assert::same(PluralProvider::csPlural(5), $plural(5)); -Assert::same(IPlural::OTHER, $plural(5)); +Assert::same(Plural::Other, $plural(5)); Assert::same(PluralProvider::csPlural(-5), $plural(-5)); -Assert::same(IPlural::OTHER, $plural(-5)); +Assert::same(Plural::Other, $plural(-5)); # English $plural = $provider->getPlural('en'); Assert::same('enPlural', $plural[1]); Assert::same(PluralProvider::enPlural(0), $plural(0)); -Assert::same(IPlural::ZERO, $plural(0)); +Assert::same(Plural::Zero, $plural(0)); Assert::same(PluralProvider::enPlural(1), $plural(1)); -Assert::same(IPlural::ONE, $plural(1)); +Assert::same(Plural::One, $plural(1)); Assert::same(PluralProvider::enPlural(2), $plural(2)); -Assert::same(IPlural::OTHER, $plural(2)); +Assert::same(Plural::Other, $plural(2)); Assert::same(PluralProvider::enPlural(5), $plural(5)); -Assert::same(IPlural::OTHER, $plural(5)); +Assert::same(Plural::Other, $plural(5)); Assert::same(PluralProvider::enPlural(-5), $plural(-5)); -Assert::same(IPlural::OTHER, $plural(-5)); +Assert::same(Plural::Other, $plural(-5)); # Zero plural foreach(['id','ja','ka','ko','lo','ms','my','th','vi','zh'] as $lang) { @@ -50,27 +50,27 @@ foreach(['id','ja','ka','ko','lo','ms','my','th','vi','zh'] as $lang) { Assert::type('callable', $plural); Assert::same('zeroPlural', $plural[1]); Assert::same(PluralProvider::zeroPlural(0), $plural(0)); - Assert::same(IPlural::ZERO, $plural(0)); + Assert::same(Plural::Zero, $plural(0)); Assert::same(PluralProvider::zeroPlural(5), $plural(5)); - Assert::same(IPlural::OTHER, $plural(5)); + Assert::same(Plural::Other, $plural(5)); Assert::same(PluralProvider::zeroPlural(-5), $plural(-5)); - Assert::same(IPlural::OTHER, $plural(-5)); + Assert::same(Plural::Other, $plural(-5)); } # csPlural -Assert::same(IPlural::ZERO, PluralProvider::csPlural(0)); -Assert::same(IPlural::ONE, PluralProvider::csPlural(1)); -Assert::same(IPlural::FEW, PluralProvider::csPlural(2)); -Assert::same(IPlural::OTHER, PluralProvider::csPlural(5)); +Assert::same(Plural::Zero, PluralProvider::csPlural(0)); +Assert::same(Plural::One, PluralProvider::csPlural(1)); +Assert::same(Plural::Few, PluralProvider::csPlural(2)); +Assert::same(Plural::Other, PluralProvider::csPlural(5)); # enPlural -Assert::same(IPlural::ZERO, PluralProvider::enPlural(0)); -Assert::same(IPlural::ONE, PluralProvider::enPlural(1)); -Assert::same(IPlural::OTHER, PluralProvider::enPlural(2)); -Assert::same(IPlural::OTHER, PluralProvider::enPlural(5)); +Assert::same(Plural::Zero, PluralProvider::enPlural(0)); +Assert::same(Plural::One, PluralProvider::enPlural(1)); +Assert::same(Plural::Other, PluralProvider::enPlural(2)); +Assert::same(Plural::Other, PluralProvider::enPlural(5)); # zeroPlural -Assert::same(IPlural::ZERO, PluralProvider::zeroPlural(0)); -Assert::same(IPlural::OTHER, PluralProvider::zeroPlural(1)); -Assert::same(IPlural::OTHER, PluralProvider::zeroPlural(5)); -Assert::same(IPlural::OTHER, PluralProvider::zeroPlural(-1)); +Assert::same(Plural::Zero, PluralProvider::zeroPlural(0)); +Assert::same(Plural::Other, PluralProvider::zeroPlural(1)); +Assert::same(Plural::Other, PluralProvider::zeroPlural(5)); +Assert::same(Plural::Other, PluralProvider::zeroPlural(-1)); diff --git a/tests/Translator/Translator.phpt b/tests/Translator/Translator.phpt index 8637ee7..25496f5 100644 --- a/tests/Translator/Translator.phpt +++ b/tests/Translator/Translator.phpt @@ -2,9 +2,10 @@ namespace Bckp\Translator; -use Bckp\Translator\Builder\Catalogue; use Bckp\Translator\Diagnostics\Diagnostics; +use Bckp\Translator\Exceptions\TranslatorException; use Nette\Utils\Html; +use Stringable; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; @@ -13,12 +14,12 @@ require __DIR__ . '/../bootstrap.php'; $plural = new PluralProvider(); $panel = new Diagnostics(); $provider = new TranslatorProvider(['cs', 'en'], $panel); -$provider->addCatalogue('cs', (new Catalogue($plural, TEMP_DIR, 'cs'))->addFile('./translations/test.cs.neon')); -$provider->addCatalogue('en', (new Catalogue($plural, TEMP_DIR, 'en'))->addFile('./translations/test.en.neon')); -$provider->addCatalogue('hu', (new Catalogue($plural, TEMP_DIR, 'hu'))); +$provider->addCatalogue('cs', (new CatalogueBuilder($plural, TEMP_DIR, 'cs'))->addFile('./translations/test.cs.neon')); +$provider->addCatalogue('en', (new CatalogueBuilder($plural, TEMP_DIR, 'en'))->addFile('./translations/test.en.neon')); +$provider->addCatalogue('hu', (new CatalogueBuilder($plural, TEMP_DIR, 'hu'))); // Test object -$string = new class { +$string = new class implements Stringable { public function __toString(): string { return 'test.welcome'; @@ -34,17 +35,21 @@ $nonString = new class { // cs translator $translator = $provider->getTranslator('cs'); Assert::equal('Vítejte', $translator->translate('test.welcome')); -Assert::equal('Vítejte', $translator->translate(['test.welcome', 3])); +Assert::equal('Vítejte', $translator->translate('test.welcome', 3)); Assert::equal('Vítejte', $translator->translate('test.welcome', 3)); Assert::equal('Vítejte', $translator->translate($string)); Assert::equal('', $translator->translate('')); Assert::equal('not.existing', $translator->translate('not.existing')); Assert::equal('html', $translator->translate(Html::el()->setText('html'))); Assert::equal('test.blank', $translator->translate('test.blank'), 'Translation is empty'); -Assert::equal('Expected string|array|object::__toString, but NULL given.', $translator->translate($nonString)); Assert::equal('zapnuto', $translator->translate('test.options')); -Assert::equal('zapnuto', $translator->translate(['test.options', 7])); -Assert::equal('5 lidí', $translator->translate(['test.plural', 5])); +Assert::equal('zapnuto', $translator->translate('test.options', 7)); +Assert::equal('5 lidí', $translator->translate('test.plural', 5, 5)); + +Assert::equal('1 2 3 4 5', $translator->translate('test.numbers', 1,2,3,4,5)); +Assert::equal('5 4 3 2 1', $translator->translate('test.numbersReverse', 1,2,3,4,5)); + +Assert::equal('1 + 1 = 2', $translator->translate('test.justSecond', 5, 1)); // en translator $translator = $provider->getTranslator('en'); @@ -63,7 +68,7 @@ Assert::truthy($panel->getUntranslated()); Assert::truthy($panel->getWarnings()); // Exception on non-exists catalogue -Assert::exception(function () use ($provider) { +Assert::exception(static function () use ($provider) { return $provider->getTranslator('jp'); }, TranslatorException::class); @@ -75,7 +80,7 @@ $translator->setNormalizeCallback(function (string $string) use (&$callbackUsed) $callbackUsed = true; return str_replace('%value', '%%value', $string); }); -Assert::equal('Hodnota prvku %value ma byt test.', $translator->translate('test.normalize', 'test')); +Assert::equal('Hodnota prvku %value ma byt test.', $translator->translate('test.normalize', parameters: 'test')); Assert::true($callbackUsed, 'Callback should be used.'); $callbackUsed = false; diff --git a/tests/Translator/translations/test.cs.neon b/tests/Translator/translations/test.cs.neon index a7fbcbf..388d3b2 100644 --- a/tests/Translator/translations/test.cs.neon +++ b/tests/Translator/translations/test.cs.neon @@ -10,3 +10,8 @@ options: one: 'zapnuto' normalize: 'Hodnota prvku %value ma byt %s.' normalize2: 'Hodnota prvku %value.' +numbers: '%d %d %d %d %d' +numbersReverse: '%5$d %4$d %3$d %2$d %1$d' +justSecond: + one: 'tahle ne' + other: '1 + %2$d = 2' diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 87faf8e..04ff334 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -4,19 +4,20 @@ namespace Bckp\Translator; +use Nette\Utils\Random; use Tester\Environment; -use function lcg_value; - use const TEMP_DIR; require __DIR__ . '/../vendor/autoload.php'; -define('TEMP_DIR', __DIR__ . '/../temp/' . (string)lcg_value()); +define('TEMP_DIR', __DIR__ . '/../temp/' . Random::generate(10)); if (file_exists(TEMP_DIR)) { @unlink(TEMP_DIR); } -mkdir(TEMP_DIR, 0775, true); +if (!mkdir($concurrentDirectory = TEMP_DIR, 0775, true) && !is_dir($concurrentDirectory)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory)); +} Environment::setup(); @unlink(TEMP_DIR);