From 75567d60c61313ad9df4a30565df7b942c1f9d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 01:04:01 +0200 Subject: [PATCH 01/12] feat(php): Reworked, added types, PHP8.2+ --- .github/workflows/tests.yaml | 81 +++++++++ composer.json | 60 ++++--- src/Catalogue.php | 53 +----- .../Catalogue.php => CatalogueBuilder.php} | 157 ++++++------------ src/Diagnostics/Diagnostics.php | 29 +--- src/Exceptions/BuilderException.php | 2 +- src/Exceptions/FileInvalidException.php | 2 +- src/Exceptions/PathInvalidException.php | 2 +- src/Exceptions/TranslatorException.php | 2 +- .../{IDiagnostics.php => Diagnostics.php} | 26 +-- src/Interfaces/ICatalogue.php | 56 ------- src/Interfaces/IPlural.php | 41 ----- .../{ITranslator.php => Translator.php} | 13 +- src/Plural.php | 13 ++ src/PluralProvider.php | 66 +++----- src/Translator.php | 153 ++++------------- src/TranslatorProvider.php | 71 +++----- tests/Catalogue/Catalogue.dynamic.phpt | 3 +- tests/Catalogue/Catalogue.onCheck.phpt | 12 +- tests/Catalogue/Catalogue.onCompile.phpt | 3 +- .../{Catalogue.phpt => CatalogueBuilder.phpt} | 52 +++--- ...ild.phpt => CatalogueBuilder.rebuild.phpt} | 7 +- tests/Diagnostics/Panel.phpt | 1 - tests/Plural/Plural.phpt | 54 +++--- tests/Translator/Translator.phpt | 27 +-- tests/Translator/translations/test.cs.neon | 5 + tests/bootstrap.php | 9 +- 27 files changed, 376 insertions(+), 624 deletions(-) create mode 100644 .github/workflows/tests.yaml rename src/{Builder/Catalogue.php => CatalogueBuilder.php} (67%) rename src/Interfaces/{IDiagnostics.php => Diagnostics.php} (51%) delete mode 100644 src/Interfaces/ICatalogue.php delete mode 100644 src/Interfaces/IPlural.php rename src/Interfaces/{ITranslator.php => Translator.php} (67%) create mode 100644 src/Plural.php rename tests/Catalogue/{Catalogue.phpt => CatalogueBuilder.phpt} (72%) rename tests/Catalogue/{Catalogue.rebuild.phpt => CatalogueBuilder.rebuild.phpt} (67%) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..b407b1d --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,81 @@ +# 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 + +# 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/composer.json b/composer.json index 0014ca7..8b54153 100644 --- a/composer.json +++ b/composer.json @@ -1,29 +1,35 @@ { - "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/" + ] + }, + "require-dev": { + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "*", + "phpstan/phpstan": "^2", + "nette/utils": "^4.0", + "nette/tester": "^2.5" + } } diff --git a/src/Catalogue.php b/src/Catalogue.php index eabe7b3..a7dd692 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; - } + protected static array $messages; /** - * Get the message translation - * - * @param string $message - * @return string|array return array if plural is detected + * @return string|string[] */ - public function get(string $message) + public function get(string $message): array|string { 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; + return array_key_exists($message, static::$messages); } - /** - * Plural form getter - * - * @param int $n - * @return string - */ - abstract public function plural(int $n): string; + abstract public function plural(int $n): Plural; + abstract public function locale(): string; + abstract public function build(): int; } diff --git a/src/Builder/Catalogue.php b/src/CatalogueBuilder.php similarity index 67% rename from src/Builder/Catalogue.php rename to src/CatalogueBuilder.php index 10f099a..fdc9035 100644 --- a/src/Builder/Catalogue.php +++ b/src/CatalogueBuilder.php @@ -12,16 +12,17 @@ declare(strict_types=1); -namespace Bckp\Translator\Builder; +namespace Bckp\Translator; -use Bckp\Translator\BuilderException; -use Bckp\Translator\FileInvalidException; -use Bckp\Translator\ICatalogue; -use Bckp\Translator\PathInvalidException; -use Bckp\Translator\PluralProvider; +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 class_exists; @@ -32,81 +33,47 @@ 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 +final class CatalogueBuilder { - /** @var array */ - public $onCompile = []; - - /** @var array */ - public $onCheck = []; - - /** @var ICatalogue|null */ - private $catalogue; - - /** @var array */ - private $collection = []; + /** @var callable[] */ + public array $onCompile = []; - /** @var array */ - private $dynamic = []; + /** @var callable[] */ + public array $onCheck = []; - /** @var bool */ - private $debug; + /** @var callable[] */ + private array $dynamic = []; - /** @var bool */ - private $loaded = false; + private ?Catalogue $catalogue = null; - /** @var string */ - private $locale; + /** @var array */ + private array $collection = []; - /** @var string */ - private $path; + private bool $debug = false; + private bool $loaded = false; + private readonly string $locale; - /** @var PluralProvider */ - private $plural; - /** - * Catalogue constructor. - * - * @param PluralProvider $plural - * @param string $path - * @param string $locale - */ - public function __construct(PluralProvider $plural, string $path, 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."); + 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; @@ -114,11 +81,9 @@ public function addDynamic(string $resource, callable $callback): self } /** - * @param int $attempt - * @return ICatalogue * @throws Throwable */ - public function rebuild(int $attempt = 0): ICatalogue + public function rebuild(int $attempt = 0): Catalogue { $filename = $this->path . '/' . $this->getName() . '.php'; $this->unlink($filename); @@ -139,18 +104,14 @@ protected function getName(): string private function unlink(string $filename): void { /** @scrutinizer ignore-unhandled */ - @unlink($filename); // @ intentionally as file may not exists + @unlink($filename); // @ intentionally as file may not exist $this->loaded = false; } /** - * Compile catalogue (or load from cache if exists) - * - * @param int $rebuild - * @return ICatalogue - * @throws Throwable + * @throws BuilderException */ - public function compile(int $rebuild = 0): ICatalogue + public function compile(int $rebuild = 0): Catalogue { // Exception on to many rebuild try if ($rebuild > 3) { @@ -168,8 +129,8 @@ public function compile(int $rebuild = 0): ICatalogue $this->checkForChanges($filename); $this->link($filename); - if (!$this->catalogue instanceof ICatalogue) { - throw new BuilderException('Catalogue is not implementing ICatalogue'); + if (!$this->catalogue instanceof Catalogue) { + throw new BuilderException('Catalogue is not implementing Catalogue'); } return $this->catalogue; @@ -190,9 +151,9 @@ protected function checkForChanges(string $filename): void $cacheTime = (int)filemtime($filename); foreach ($this->collection as $file) { - $file = new \SplFileInfo($file); + $file = new SplFileInfo($file); $fileTime = $file->getMTime(); - if ($fileTime > $cacheTime || ($this->catalogue && $fileTime > $this->catalogue->buildTime())) { + if ($fileTime > $cacheTime || ($this->catalogue && $fileTime > $this->catalogue->build())) { throw new BuilderException('Rebuild required'); } } @@ -211,39 +172,39 @@ protected function compileCode(): string $messages = $this->getMessages(); $this->onCompile($messages); +/* do { - $className = $this->getName() . uniqid(); + $className = $this->getName() . substr(md5((string) mt_rand()), 4, 8); } while (class_exists($className)); +*/ // File $file = new PhpFile(); - $file->setStrictTypes(true); + $file->setStrictTypes(); $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"); + $class = new ClassType(); + //$class = $file->addClass($className); + $class->setExtends(Catalogue::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->setBody('return Bckp\Translator\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'); + $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->getName()};\n"; + $code .= "\nreturn new class {$class};\n"; // Return string return $code; @@ -261,7 +222,7 @@ protected function getMessages(): array // Add files foreach ($this->collection as $file) { - $info = new \SplFileInfo($file); + $info = new SplFileInfo($file); $resource = strtolower($info->getBasename('.' . $this->locale . '.neon')); foreach ($this->loadFile($file) as $key => $item) { $messages[$resource . '.' . $key] = $item; @@ -294,7 +255,7 @@ protected function getMessages(): array protected function loadFile(string $file): array { if (!file_exists($file) || !is_readable($file)) { - throw new PathInvalidException("File '{$file}' not found or is not readable."); + throw new PathInvalidException("File '$file' not found or is not readable."); } $content = file_get_contents($file); @@ -305,11 +266,11 @@ protected function loadFile(string $file): array try { $content = Neon::decode($content); if (!is_array($content)) { - throw new \Exception('No array'); + throw new RuntimeException('No array'); } } catch (Throwable $e) { throw new FileInvalidException( - "File '{$file}' do not contain array of translations", + "File '$file' do not contain array of translations", $e->getCode(), $e ); @@ -329,18 +290,11 @@ private function onCompile(array &$messages): void } } - /** - * @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) { @@ -361,8 +315,7 @@ private function link(string $filename): void return; } - /** @noinspection PhpIncludeInspection */ - $this->catalogue = include $filename; + $this->catalogue = require $filename; $this->loaded = true; } @@ -382,12 +335,6 @@ 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; diff --git a/src/Diagnostics/Diagnostics.php b/src/Diagnostics/Diagnostics.php index b58a653..184452a 100644 --- a/src/Diagnostics/Diagnostics.php +++ b/src/Diagnostics/Diagnostics.php @@ -14,36 +14,28 @@ 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 = ''; + private string $locale = ''; /** @var array */ - private $messages = []; + private array $messages = []; /** @var array */ - private $untranslated = []; + private array $untranslated = []; - /** - * @return string - */ public function getLocale(): string { return $this->locale; } /** - * @return array + * @return string[] */ public function getUntranslated(): array { @@ -51,30 +43,23 @@ public function getUntranslated(): array } /** - * @return array + * @return string[] */ public function getWarnings(): array { return array_unique($this->messages); } - /** @param string $locale */ public function setLocale(string $locale): void { $this->locale = $locale; } - /** - * @param string $message - */ public function untranslated(string $message): void { $this->untranslated[] = $message; } - /** - * @param string $message - */ public function warning(string $message): void { $this->messages[] = $message; diff --git a/src/Exceptions/BuilderException.php b/src/Exceptions/BuilderException.php index 8e2f47b..b7ae6ca 100644 --- a/src/Exceptions/BuilderException.php +++ b/src/Exceptions/BuilderException.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Bckp\Translator; +namespace Bckp\Translator\Exceptions; class BuilderException extends TranslatorException { diff --git a/src/Exceptions/FileInvalidException.php b/src/Exceptions/FileInvalidException.php index 030340d..70333f2 100644 --- a/src/Exceptions/FileInvalidException.php +++ b/src/Exceptions/FileInvalidException.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Bckp\Translator; +namespace Bckp\Translator\Exceptions; use Nette\InvalidStateException; diff --git a/src/Exceptions/PathInvalidException.php b/src/Exceptions/PathInvalidException.php index 05e7838..9353d0b 100644 --- a/src/Exceptions/PathInvalidException.php +++ b/src/Exceptions/PathInvalidException.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Bckp\Translator; +namespace Bckp\Translator\Exceptions; use Nette\FileNotFoundException; use Nette\InvalidStateException; diff --git a/src/Exceptions/TranslatorException.php b/src/Exceptions/TranslatorException.php index 1ec2b21..066ff34 100644 --- a/src/Exceptions/TranslatorException.php +++ b/src/Exceptions/TranslatorException.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Bckp\Translator; +namespace Bckp\Translator\Exceptions; use RuntimeException; diff --git a/src/Interfaces/IDiagnostics.php b/src/Interfaces/Diagnostics.php similarity index 51% rename from src/Interfaces/IDiagnostics.php rename to src/Interfaces/Diagnostics.php index 34e1548..4cb6006 100644 --- a/src/Interfaces/IDiagnostics.php +++ b/src/Interfaces/Diagnostics.php @@ -1,4 +1,4 @@ - + * @author Radovan Kepak */ -declare(strict_types=1); +namespace Bckp\Translator\Interfaces; -namespace Bckp\Translator; - -/** - * Interface IDiagnostics - * - * @package Bckp\Translator - */ -interface IDiagnostics +interface Diagnostics { - /** - * @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/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/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/Translator.php similarity index 67% rename from src/Interfaces/ITranslator.php rename to src/Interfaces/Translator.php index c5865dd..9fe8016 100644 --- a/src/Interfaces/ITranslator.php +++ b/src/Interfaces/Translator.php @@ -12,24 +12,21 @@ declare(strict_types=1); -namespace Bckp\Translator; +namespace Bckp\Translator\Interfaces; + +use Stringable; /** * Interface ITranslator * * @package Bckp\Translator */ -interface ITranslator +interface Translator { /** * @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; + 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..512345a --- /dev/null +++ b/src/Plural.php @@ -0,0 +1,13 @@ + 'csPlural', 'en' => 'enPlural', 'id' => 'zeroPlural', @@ -49,50 +36,37 @@ final class PluralProvider implements IPlural /** * Czech plural selector (zero-one-few-other) - * - * @param int|null $n - * @return string */ - public static function csPlural(?int $n): string + public static function csPlural(?int $n): Plural { - return $n === 0 - ? IPlural::ZERO - : ($n === 1 - ? IPlural::ONE - : ($n >= 2 && $n < 5 - ? IPlural::FEW - : IPlural::OTHER - ) - ); + return match (true) { + $n === 0 => Plural::Zero, + $n === 1 => Plural::One, + $n >= 2 && $n <= 4 => Plural::Few, + default => Plural::Other, + }; } /** * Default plural detector (zero-one-other) - * - * @param int|null $n - * @return string */ - public static function enPlural(?int $n): string + public static function enPlural(?int $n): Plural { - return $n === 0 - ? IPlural::ZERO - : ($n === 1 - ? IPlural::ONE - : IPlural::OTHER - ); + return match (true) { + $n === 0 => Plural::Zero, + $n === 1 => Plural::One, + default => Plural::Other, + }; } /** * No plural detector (zero-other) - * - * @param int|null $n - * @return string */ - public static function zeroPlural(?int $n): string + public static function zeroPlural(?int $n): Plural { return $n === 0 - ? IPlural::ZERO - : IPlural::OTHER; + ? Plural::Zero + : Plural::Other; } /** @@ -109,6 +83,6 @@ public function getPlural(string $locale): callable if ($callable[1] && is_callable($callable)) { return $callable; } - return [$this, self::DEFAULT]; + return [$this, self::Default]; } } diff --git a/src/Translator.php b/src/Translator.php index 7309f7e..64389a8 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -14,14 +14,11 @@ 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,38 +26,19 @@ * * @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; + public function __construct( + private readonly Catalogue $catalogue, + private readonly ?Diagnostics $diagnostics = null + ) { $this->normalizeCallback = [$this, 'normalize']; - if ($this->diagnostics = $diagnostics) { - $this->diagnostics->setLocale($catalogue->locale()); - } + $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( @@ -70,96 +48,50 @@ public function normalize(string $string): 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 + public function translate(string|Stringable $message, mixed ...$parameters): string { + $message = (string) $message; + if (empty($message)) { return ''; } - $form = null; + $translation = $this->catalogue->get($message); + if (!$translation) { + $this->untranslated($message); + return $message; + } - $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)); + // 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); } - $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); + + if ($parameters) { + $translation = ($this->normalizeCallback)($translation); + $translation = @vsprintf($translation, $parameters); } - return $result; + return $translation; } - /** - * @param string $message - * @param string|array $translation - * @param string|null $form - * @return string - */ - private function getVariant(string $message, $translation, string $form = null): string + public function getVariant(string $message, array $translations, Plural $plural): string { - if (!is_array($translation)) { - return $translation; - } - - if ($form === null || !array_key_exists($form, $translation)) { + if (!array_key_exists($plural->value, $translations)) { $this->warn( 'Plural form not defined. (message: %s, form: %s)', - (string)$message, - (string)$form + $message, + $plural->value, ); - 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; + return $translations[$plural->value] ?? $translations[array_key_last($translations)]; } /** @@ -167,9 +99,7 @@ protected function getMessage($message, ?string &$plural): ?string */ protected function untranslated(string $message): void { - if ($this->diagnostics !== null) { - $this->diagnostics->untranslated($message); - } + $this->diagnostics?->untranslated($message); } /** @@ -183,25 +113,8 @@ protected function warn(string $message, ...$parameters): string $message = @vsprintf($message, $parameters); } // Intentionally @ as parameter count can mismatch - if ($this->diagnostics !== null) { - $this->diagnostics->warning($message); - } + $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]; - } - } } diff --git a/src/TranslatorProvider.php b/src/TranslatorProvider.php index b340bda..278b723 100644 --- a/src/TranslatorProvider.php +++ b/src/TranslatorProvider.php @@ -14,58 +14,36 @@ 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 ITranslator[] */ - protected $translators = []; - /** - * TranslatorProvider constructor. - * - * @param string[] $languages - * @param IDiagnostics|null $diagnostics + * @var array */ - public function __construct(array $languages, IDiagnostics $diagnostics = null) - { - $this->diagnostics = $diagnostics; - $this->languages = $languages; - } + protected array $catalogues = []; /** - * @param string $locale - * @param BuilderCatalogue $builder - * @return void + * @var array */ - public function addCatalogue(string $locale, BuilderCatalogue $builder): void - { - $locale = strtolower($locale); - $this->catalogues[$locale] = $builder; + protected array $translators = []; + + public function __construct( + protected array $languages, + protected ?Interfaces\Diagnostics $diagnostics = null + ) { } - /** - * @param string $locale - * @return ITranslator - * @throws \Throwable - */ - public function getTranslator(string $locale): ITranslator + public function addCatalogue( + string $locale, + CatalogueBuilder $builder + ): void { + $this->catalogues[strtolower($locale)] = $builder; + } + + public function getTranslator(string $locale): Interfaces\Translator { $locale = strtolower($locale); if (!isset($this->translators[$locale])) { @@ -76,16 +54,17 @@ public function getTranslator(string $locale): ITranslator } /** - * @param string $locale - * @return ITranslator - * @throws \Throwable + * @throws BuilderException */ - protected function createTranslator(string $locale): ITranslator + 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); From 37388da80b0261e5eb21ce7244ab5a176e66b494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 02:01:02 +0200 Subject: [PATCH 02/12] added phpstan --- .travis.yml | 74 -------------------------------------- composer.json | 6 ++++ phpstan.neon | 5 +++ src/Catalogue.php | 2 +- src/CatalogueBuilder.php | 3 +- src/PluralProvider.php | 36 +++++-------------- src/Translator.php | 3 ++ src/TranslatorProvider.php | 3 ++ 8 files changed, 27 insertions(+), 105 deletions(-) delete mode 100644 .travis.yml create mode 100644 phpstan.neon 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/composer.json b/composer.json index 8b54153..b55f34e 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,12 @@ "src/" ] }, + "scripts": { + "phpstan": "phpstan analyse", + "tests": "tester tests -s", + "tests-watch": "tester tests -s -w src", + "psalm": "psalm" + }, "require-dev": { "roave/security-advisories": "dev-latest", "squizlabs/php_codesniffer": "*", 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/src/Catalogue.php b/src/Catalogue.php index a7dd692..83aac05 100644 --- a/src/Catalogue.php +++ b/src/Catalogue.php @@ -21,7 +21,7 @@ */ abstract class Catalogue { - /** @var array */ + /** @var array> */ protected static array $messages; /** diff --git a/src/CatalogueBuilder.php b/src/CatalogueBuilder.php index fdc9035..4cdc623 100644 --- a/src/CatalogueBuilder.php +++ b/src/CatalogueBuilder.php @@ -25,7 +25,6 @@ use SplFileInfo; use Throwable; -use function class_exists; use function file_exists; use function file_get_contents; use function file_put_contents; @@ -190,7 +189,7 @@ protected function compileCode(): string // Setup plural method $method = $class->addMethod('plural'); - $plural = Method::from((array)$this->plural->getPlural($this->locale)); + $plural = Method::from($this->plural->getPlural($this->locale)); $method->setParameters($plural->getParameters()); $parameters = $method->getParameters(); $method->setBody('return Bckp\Translator\PluralProvider::?($?);', [$plural->getName(), key($parameters)]); diff --git a/src/PluralProvider.php b/src/PluralProvider.php index 1783060..965f546 100644 --- a/src/PluralProvider.php +++ b/src/PluralProvider.php @@ -14,26 +14,11 @@ namespace Bckp\Translator; +use Closure; use function strtolower; final class PluralProvider { - public const Default = 'enPlural'; - private array $plurals = [ - 'cs' => '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) */ @@ -70,19 +55,14 @@ public static function zeroPlural(?int $n): Plural } /** - * Get plural method - * - * @param string $locale - * @return callable(int|null $n): string + * @return array{self, 'csPlural'|'enPlural'|'zeroPlural'} */ - public function getPlural(string $locale): callable + public function getPlural(string $locale): array { - $locale = strtolower($locale); - $callable = [$this, $this->plurals[$locale] ?? null]; - - if ($callable[1] && is_callable($callable)) { - return $callable; - } - return [$this, self::Default]; + 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 64389a8..71f0f15 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -81,6 +81,9 @@ public function translate(string|Stringable $message, mixed ...$parameters): str return $translation; } + /** + * @param array $translations + */ public function getVariant(string $message, array $translations, Plural $plural): string { if (!array_key_exists($plural->value, $translations)) { diff --git a/src/TranslatorProvider.php b/src/TranslatorProvider.php index 278b723..201d139 100644 --- a/src/TranslatorProvider.php +++ b/src/TranslatorProvider.php @@ -30,6 +30,9 @@ class TranslatorProvider */ protected array $translators = []; + /** + * @param array $languages + */ public function __construct( protected array $languages, protected ?Interfaces\Diagnostics $diagnostics = null From d59da466a83631b47c3452be69d9836068c4d421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 02:20:41 +0200 Subject: [PATCH 03/12] small fixes --- composer.json | 4 +--- src/CatalogueBuilder.php | 2 +- src/PluralProvider.php | 2 +- src/Translator.php | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index b55f34e..4ec7482 100644 --- a/composer.json +++ b/composer.json @@ -28,12 +28,10 @@ "scripts": { "phpstan": "phpstan analyse", "tests": "tester tests -s", - "tests-watch": "tester tests -s -w src", - "psalm": "psalm" + "tests-watch": "tester tests -s -w src" }, "require-dev": { "roave/security-advisories": "dev-latest", - "squizlabs/php_codesniffer": "*", "phpstan/phpstan": "^2", "nette/utils": "^4.0", "nette/tester": "^2.5" diff --git a/src/CatalogueBuilder.php b/src/CatalogueBuilder.php index 4cdc623..5fbe36d 100644 --- a/src/CatalogueBuilder.php +++ b/src/CatalogueBuilder.php @@ -52,7 +52,7 @@ final class CatalogueBuilder private bool $debug = false; private bool $loaded = false; - private readonly string $locale; + private string $locale; public function __construct( diff --git a/src/PluralProvider.php b/src/PluralProvider.php index 965f546..0aa504a 100644 --- a/src/PluralProvider.php +++ b/src/PluralProvider.php @@ -55,7 +55,7 @@ public static function zeroPlural(?int $n): Plural } /** - * @return array{self, 'csPlural'|'enPlural'|'zeroPlural'} + * @return array{0: self, 1: 'csPlural'|'enPlural'|'zeroPlural'} */ public function getPlural(string $locale): array { diff --git a/src/Translator.php b/src/Translator.php index 71f0f15..5c08f91 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -73,7 +73,7 @@ public function translate(string|Stringable $message, mixed ...$parameters): str $translation = $this->getVariant($message, $translation, $plural); } - if ($parameters) { + if (!empty($parameters)) { $translation = ($this->normalizeCallback)($translation); $translation = @vsprintf($translation, $parameters); } From fb524d5d9b6c78fdd3e8faaebade01c3445235a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 07:09:01 +0200 Subject: [PATCH 04/12] cleanup --- src/Catalogue.php | 2 +- src/CatalogueBuilder.php | 4 +--- src/PluralProvider.php | 5 +---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Catalogue.php b/src/Catalogue.php index 83aac05..7fd8369 100644 --- a/src/Catalogue.php +++ b/src/Catalogue.php @@ -25,7 +25,7 @@ abstract class Catalogue protected static array $messages; /** - * @return string|string[] + * @return string|array */ public function get(string $message): array|string { diff --git a/src/CatalogueBuilder.php b/src/CatalogueBuilder.php index 5fbe36d..1ea851f 100644 --- a/src/CatalogueBuilder.php +++ b/src/CatalogueBuilder.php @@ -183,13 +183,11 @@ protected function compileCode(): string $file->setComment('This file was auto-generated'); $class = new ClassType(); - //$class = $file->addClass($className); $class->setExtends(Catalogue::class); - //$class->addComment("This file was auto-generated"); // Setup plural method $method = $class->addMethod('plural'); - $plural = Method::from($this->plural->getPlural($this->locale)); + $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)]); diff --git a/src/PluralProvider.php b/src/PluralProvider.php index 0aa504a..8ef0749 100644 --- a/src/PluralProvider.php +++ b/src/PluralProvider.php @@ -54,10 +54,7 @@ public static function zeroPlural(?int $n): Plural : Plural::Other; } - /** - * @return array{0: self, 1: 'csPlural'|'enPlural'|'zeroPlural'} - */ - public function getPlural(string $locale): array + public function getPlural(string $locale): callable { return match(strtolower($locale)) { 'cs' => [$this, 'csPlural'], From 710e269b6ea756a0c20c5555c78181bbf4f1d154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 07:19:22 +0200 Subject: [PATCH 05/12] phpcs --- .gitignore | 1 + .php-cs-fixer.dist.php | 15 +++++++++++++++ composer.json | 4 +++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .php-cs-fixer.dist.php 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/composer.json b/composer.json index 4ec7482..4f2ba46 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,9 @@ "scripts": { "phpstan": "phpstan analyse", "tests": "tester tests -s", - "tests-watch": "tester tests -s -w src" + "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", From 583a6c36f0dec3708741ed6409a032c9ced110a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 07:20:15 +0200 Subject: [PATCH 06/12] phpcs --- src/Catalogue.php | 32 +- src/CatalogueBuilder.php | 602 ++++++++++++------------ src/Diagnostics/Diagnostics.php | 72 +-- src/Exceptions/BuilderException.php | 4 +- src/Exceptions/FileInvalidException.php | 4 +- src/Exceptions/PathInvalidException.php | 4 +- src/Exceptions/TranslatorException.php | 4 +- src/Interfaces/Diagnostics.php | 10 +- src/Interfaces/Translator.php | 10 +- src/Plural.php | 16 +- src/PluralProvider.php | 81 ++-- src/Translator.php | 185 ++++---- src/TranslatorProvider.php | 86 ++-- 13 files changed, 554 insertions(+), 556 deletions(-) diff --git a/src/Catalogue.php b/src/Catalogue.php index 7fd8369..3f829e1 100644 --- a/src/Catalogue.php +++ b/src/Catalogue.php @@ -21,23 +21,23 @@ */ abstract class Catalogue { - /** @var array> */ - protected static array $messages; + /** @var array> */ + protected static array $messages; - /** - * @return string|array - */ - public function get(string $message): array|string - { - return static::$messages[$message] ?? ''; - } + /** + * @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); - } + 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; + 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 index 1ea851f..4ab25fc 100644 --- a/src/CatalogueBuilder.php +++ b/src/CatalogueBuilder.php @@ -36,305 +36,305 @@ 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; - } + /** @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 184452a..aae364c 100644 --- a/src/Diagnostics/Diagnostics.php +++ b/src/Diagnostics/Diagnostics.php @@ -20,48 +20,48 @@ class Diagnostics implements Interfaces\Diagnostics { - /** @var string */ - private string $locale = ''; + /** @var string */ + private string $locale = ''; - /** @var array */ - private array $messages = []; + /** @var array */ + private array $messages = []; - /** @var array */ - private array $untranslated = []; + /** @var array */ + private array $untranslated = []; - public function getLocale(): string - { - return $this->locale; - } + public function getLocale(): string + { + return $this->locale; + } - /** - * @return string[] - */ - public function getUntranslated(): array - { - return array_unique($this->untranslated); - } + /** + * @return string[] + */ + public function getUntranslated(): array + { + return array_unique($this->untranslated); + } - /** - * @return string[] - */ - public function getWarnings(): array - { - return array_unique($this->messages); - } + /** + * @return string[] + */ + public function getWarnings(): array + { + return array_unique($this->messages); + } - public function setLocale(string $locale): void - { - $this->locale = $locale; - } + public function setLocale(string $locale): void + { + $this->locale = $locale; + } - public function untranslated(string $message): void - { - $this->untranslated[] = $message; - } + public function untranslated(string $message): void + { + $this->untranslated[] = $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 b7ae6ca..69ce201 100644 --- a/src/Exceptions/BuilderException.php +++ b/src/Exceptions/BuilderException.php @@ -14,6 +14,4 @@ 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 70333f2..56d4279 100644 --- a/src/Exceptions/FileInvalidException.php +++ b/src/Exceptions/FileInvalidException.php @@ -16,6 +16,4 @@ use Nette\InvalidStateException; -class FileInvalidException extends InvalidStateException -{ -} +class FileInvalidException extends InvalidStateException {} diff --git a/src/Exceptions/PathInvalidException.php b/src/Exceptions/PathInvalidException.php index 9353d0b..79e0ded 100644 --- a/src/Exceptions/PathInvalidException.php +++ b/src/Exceptions/PathInvalidException.php @@ -18,6 +18,4 @@ 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 066ff34..c88f8f5 100644 --- a/src/Exceptions/TranslatorException.php +++ b/src/Exceptions/TranslatorException.php @@ -16,6 +16,4 @@ use RuntimeException; -class TranslatorException extends RuntimeException -{ -} +class TranslatorException extends RuntimeException {} diff --git a/src/Interfaces/Diagnostics.php b/src/Interfaces/Diagnostics.php index 4cb6006..1f24369 100644 --- a/src/Interfaces/Diagnostics.php +++ b/src/Interfaces/Diagnostics.php @@ -1,4 +1,6 @@ - Plural::Zero, - $n === 1 => Plural::One, - $n >= 2 && $n <= 4 => Plural::Few, - default => Plural::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, + }; + } - /** - * 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, - }; - } + /** + * 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, + }; + } - /** - * No plural detector (zero-other) - */ - public static function zeroPlural(?int $n): Plural - { - return $n === 0 - ? Plural::Zero - : Plural::Other; - } + /** + * No plural detector (zero-other) + */ + public static function zeroPlural(?int $n): Plural + { + return $n === 0 + ? Plural::Zero + : Plural::Other; + } - 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'], - }; - } + 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 5c08f91..bc110d2 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -16,6 +16,7 @@ use Bckp\Translator\Interfaces\Diagnostics; use Stringable; + use function array_key_exists; use function array_key_last; use function is_array; @@ -28,96 +29,96 @@ */ class Translator implements Interfaces\Translator { - /** @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; - } + /** @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 201d139..3e8b7cd 100644 --- a/src/TranslatorProvider.php +++ b/src/TranslatorProvider.php @@ -16,58 +16,58 @@ use Bckp\Translator\Exceptions\BuilderException; use Bckp\Translator\Exceptions\TranslatorException; + use function strtolower; class TranslatorProvider { - /** - * @var array - */ - protected array $catalogues = []; + /** + * @var array + */ + protected array $catalogues = []; - /** - * @var array - */ - protected array $translators = []; + /** + * @var array + */ + protected array $translators = []; - /** - * @param array $languages - */ - public function __construct( - protected array $languages, - protected ?Interfaces\Diagnostics $diagnostics = null - ) { - } + /** + * @param array $languages + */ + public function __construct( + protected array $languages, + protected ?Interfaces\Diagnostics $diagnostics = null + ) {} - public function addCatalogue( - string $locale, - CatalogueBuilder $builder - ): void { - $this->catalogues[strtolower($locale)] = $builder; - } + public function addCatalogue( + string $locale, + CatalogueBuilder $builder + ): void { + $this->catalogues[strtolower($locale)] = $builder; + } - public function getTranslator(string $locale): Interfaces\Translator - { - $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]; + } - /** - * @throws BuilderException - */ - protected function createTranslator(string $locale): Interfaces\Translator - { - 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 + ); + } } From 07e14ab0c818985f4358eadeb1abdc6dade19cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 07:24:46 +0200 Subject: [PATCH 07/12] phpcs ci/cd --- .github/workflows/tests.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b407b1d..350603f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -29,6 +29,20 @@ jobs: - 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 From b08322ed075fe821236096a181dc71e260595ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 08:20:58 +0200 Subject: [PATCH 08/12] Update composer.json --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4f2ba46..c6fb6a4 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "roave/security-advisories": "dev-latest", "phpstan/phpstan": "^2", "nette/utils": "^4.0", - "nette/tester": "^2.5" + "nette/tester": "^2.5", + "friendsofphp/php-cs-fixer": "^3.75" } } From 794a85db7f8949d1805af0c92d4b2df188de138b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 08:30:33 +0200 Subject: [PATCH 09/12] badge change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3c3441..68b623f 100644 --- a/README.md +++ b/README.md @@ -2,7 +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) +[![Build Status](https://github.com/bckp/translator-core)](https://github.com/bckp/translator-core/actions/workflows/tests/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/bckp/translator-core/badge.svg?branch=master)](https://coveralls.io/github/bckp/translator-core?branch=master) [![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) From 10066be7828d7010a1cda3663b04decb0563446a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 08:47:52 +0200 Subject: [PATCH 10/12] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 68b623f..9a4036d 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://github.com/bckp/translator-core)](https://github.com/bckp/translator-core/actions/workflows/tests/badge.svg) -[![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) From 29258a3932ef8f525cbb22d00992bafeee5dd378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 08:48:24 +0200 Subject: [PATCH 11/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a4036d..e46cc46 100644 --- a/README.md +++ b/README.md @@ -24,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" ``` From d0bc4428796f7922fcdbd59d86813c60421968b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Kep=C3=A1k?= Date: Mon, 7 Apr 2025 09:00:49 +0200 Subject: [PATCH 12/12] Delete ruleset.xml --- ruleset.xml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 ruleset.xml 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. - - - - -