From 98a5e35bb6bd158b0e51e2b615673b7eee11df6a Mon Sep 17 00:00:00 2001 From: Moamen Eltouny Date: Wed, 7 Oct 2026 14:50:01 +0300 Subject: [PATCH 1/2] Support portable-ascii 1.x with the same output as 2.x --- composer.json | 2 +- src/Resources/portable-ascii-1.php | 63 +++++++++++++++++++ .../PortableAsciiTransliterator.php | 47 +++++++++++++- .../Unit/PortableAsciiTransliteratorTest.php | 6 ++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/Resources/portable-ascii-1.php diff --git a/composer.json b/composer.json index 9ab890f..699fd8a 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "php": ">=8.1 <8.2", "ext-mbstring": "*", "symfony/polyfill-intl-normalizer": "^1.26", - "voku/portable-ascii": "^2.0" + "voku/portable-ascii": "^1.6.1 || ^2.0" }, "require-dev": { "phpstan/phpstan": "^1.12 || ^2.0", diff --git a/src/Resources/portable-ascii-1.php b/src/Resources/portable-ascii-1.php new file mode 100644 index 0000000..4c9559b --- /dev/null +++ b/src/Resources/portable-ascii-1.php @@ -0,0 +1,63 @@ + [ + 'ё' => 'e', + 'Ё' => 'E', + 'ъ' => 'ie', + 'Ъ' => 'Ie', + 'ы' => 'y', + 'Ы' => 'Y', + 'э' => 'e', + 'Э' => 'E', + 'ю' => 'iu', + 'Ю' => 'Iu', + 'я' => 'ia', + 'Я' => 'Ia', + 'پ' => 'p', + ], + + 'fa' => [ + 'پ' => 'p', + 'ج' => 'j', + ], + + 'latin' => [ + 'й' => 'i', + 'Й' => 'i', + 'щ' => 'shh', + 'Щ' => 'Shh', + ], + + 'uk' => [ + 'г' => 'h', + 'Г' => 'H', + 'и' => 'y', + 'И' => 'Y', + 'й' => 'y', + 'Й' => 'Y', + 'х' => 'kh', + 'Х' => 'Kh', + 'ц' => 'ts', + 'Ц' => 'Ts', + 'ч' => 'ch', + 'Ч' => 'Ch', + 'ш' => 'sh', + 'Ш' => 'Sh', + 'щ' => 'shch', + 'Щ' => 'Shch', + ], +]; diff --git a/src/Transliteration/PortableAsciiTransliterator.php b/src/Transliteration/PortableAsciiTransliterator.php index 070a883..b5ebbea 100644 --- a/src/Transliteration/PortableAsciiTransliterator.php +++ b/src/Transliteration/PortableAsciiTransliterator.php @@ -9,6 +9,9 @@ /** * Transliterates through voku/portable-ascii, including its generic * fallback tables for scripts without a language map (CJK, Hebrew, ...). + * + * Works with portable-ascii 1.x and 2.x and gives the same result with both + * (see Resources/portable-ascii-1.php). */ final class PortableAsciiTransliterator implements Transliterator { @@ -17,9 +20,51 @@ final class PortableAsciiTransliterator implements Transliterator */ private static ?array $known = null; + /** + * Whether portable-ascii 1.x is installed; null until checked. + */ + private static ?bool $isLegacy = null; + + /** + * @var array> language => the 2.x results applied over 1.x + */ + private static array $legacy = []; + public function transliterate(string $value, ?string $language = null): string { - return ASCII::to_ascii($value, self::language($language), true, false, true); + $language = self::language($language); + + if (preg_match('/[^\x00-\x7F]/', $value) === 1 && ($map = self::legacy($language)) !== []) { + $value = strtr($value, $map); + } + + return ASCII::to_ascii($value, $language, true, false, true); + } + + /** + * The portable-ascii 2.x results for the letters 1.x transliterates differently. + * + * @return array + */ + private static function legacy(string $language): array + { + // portable-ascii 1.x transliterates "پ" (peh) as "b". + self::$isLegacy ??= ASCII::to_ascii('پ') === 'b'; + + if (!self::$isLegacy) { + return []; + } + + if (!isset(self::$legacy[$language])) { + /** @var array> $resource */ + $resource = require __DIR__ . '/../Resources/portable-ascii-1.php'; + $own = ASCII::charsArrayWithOneLanguage($language, false, false); + + self::$legacy[$language] = ($resource[$language] ?? []) + + array_diff_key($resource['generic'], $own); + } + + return self::$legacy[$language]; } /** diff --git a/tests/Unit/PortableAsciiTransliteratorTest.php b/tests/Unit/PortableAsciiTransliteratorTest.php index 4ddda80..47109ca 100644 --- a/tests/Unit/PortableAsciiTransliteratorTest.php +++ b/tests/Unit/PortableAsciiTransliteratorTest.php @@ -29,6 +29,12 @@ public function transliterationProvider(): array 'known regional variant' => ['Straße', 'de-AT', 'Strasze'], 'script subtag falls back to the language' => ['Đorđe', 'sr-Latn', 'Djordje'], 'unknown language is generic' => ['Äpfel', 'xx-YY', 'Apfel'], + // Same result with portable-ascii 1.x and 2.x (Resources/portable-ascii-1.php). + 'cyrillic e keeps the word whole' => ['Мэр', null, 'Mer'], + 'cyrillic generic letters' => ['ёлка объявление', null, 'elka obieiavlenie'], + 'cyrillic language map wins' => ['ёлка', 'ru', 'yolka'], + 'persian peh' => ['پ', null, 'p'], + 'persian language map' => ['پنجره', 'fa', 'pnjrh'], ]; } From b58d7e30f184ce68cd1a2dc705c3cdbe1c66586a Mon Sep 17 00:00:00 2001 From: Moamen Eltouny Date: Wed, 7 Oct 2026 15:02:58 +0300 Subject: [PATCH 2/2] Document portable-ascii 1.x support for 8.1.2 --- CHANGELOG.md | 10 ++++++++++ docs/sections/02-installation.md | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a89982..644bafb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## 8.1.2 - Unreleased + +### Added + +- Support for `voku/portable-ascii` 1.x (`^1.6.1 || ^2.0`), so the package installs alongside Laravel 8, which requires portable-ascii 1.x. + +### Fixed + +- With portable-ascii 1.x, the letters it transliterates differently from 2.x are given the 2.x result, so a slug is the same with either version: Persian `پ` (`p`, was `b`) and `ج` (`j`), the generic Cyrillic `ё ъ ы э ю я` (`Мэр` → `mer`, was `me-r`), and a few Ukrainian and Latin map entries. The list comes from comparing every BMP letter across every portable-ascii language. + ## 8.1.1 - 2026-10-06 ### Fixed diff --git a/docs/sections/02-installation.md b/docs/sections/02-installation.md index dccfcbb..46aa38a 100644 --- a/docs/sections/02-installation.md +++ b/docs/sections/02-installation.md @@ -6,7 +6,7 @@ Install the package with Composer. There is nothing to register or publish. - PHP 8.1.x (each `8.x` release line targets the matching PHP version) - `ext-mbstring` -- `voku/portable-ascii` ^2.0 and `symfony/polyfill-intl-normalizer` (installed automatically) +- `voku/portable-ascii` ^1.6.1 or ^2.0 and `symfony/polyfill-intl-normalizer` (installed automatically) - `ext-intl` *(optional)*: Unicode normalization works without it, but the native extension is faster than the polyfill. ### Composer Installation