Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion docs/sections/02-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 63 additions & 0 deletions src/Resources/portable-ascii-1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/*
* voku/portable-ascii 1.x compatibility.
*
* Laravel 7 and 8 pin portable-ascii to ^1.x. These are the only letters whose
* transliteration differs from portable-ascii 2.x (compared across every BMP
* letter and every language map). With 1.x installed, they are replaced by the
* 2.x result first, so a slug is the same whichever version is installed.
*
* "generic" applies when the language has no entry of its own for the letter
* (1.x also turns "э" into "e'", which split words such as "Мэр" into "me-r").
* The other keys are portable-ascii language codes.
*/

return [
'generic' => [
'ё' => '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',
],
];
47 changes: 46 additions & 1 deletion src/Transliteration/PortableAsciiTransliterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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<string, array<string, string>> 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<string, string>
*/
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<string, array<string, string>> $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];
}

/**
Expand Down
6 changes: 6 additions & 0 deletions tests/Unit/PortableAsciiTransliteratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
];
}

Expand Down
Loading