shoyim/latin-to-cyrillic
A Laravel/PHP package for converting Latin text to Cyrillic. It offers simple transliteration helpers for turning Uzbek Latin script into Cyrillic, making it easy to localize names, content, and user input in Laravel applications.
This release targets the 21 October 2026 alphabet reform, so treat it as a content-migration release first and a feature release second. Start by reading CHANGELOG.md (the notes are a summary) and diffing the mapping tables against the version you have pinned.
php >=7.0. That is a minimum, not a target — if you are on Laravel 10/11 you are already on PHP 8.1+, so the constraint is harmless, but it does mean the package cannot use typed properties, enums, or match, and you should not expect PHP 8-only fixes. Confirm your own composer.json platform config does not resolve to an ancient lock."require": {"vendor/package": "1.2.*"}) in a branch and generate a golden-file fixture set before upgrading. You need a "before" snapshot to diff against, because the alphabet change alters output for inputs that previously worked fine.Oʻ→Ö, Gʻ→Ğ, Sh→Ş, Ch→Ç) and the apostrophe/e fixes.fromNewLatin() to validate round-trips.Do not skip step 3. The bug fixes for apostrophes and e mean some conversions that "looked wrong" now look different, and some that looked right may also change.
Single conversion boundary. Pick one canonical storage form — I recommend Cyrillic or the new Latin — and convert only at the edges (presentation, export, search indexing). Do not mix old-Latin and new-Latin in the same column; you will never be able to tell them apart afterwards.
Accessor / cast for display:
protected function displayName(): Attribute
{
return Attribute::get(fn () => $this->latin_name
?? app(Converter::class)->toLatin($this->name));
}
Keep the converter resolved from the container rather than new-ing it, so config (and any future mapping overrides) applies consistently.
Backfill as a chunked, idempotent command. Conversion is pure, so it is safe to re-run, but do it in chunks and write a converted_at / orthography_version column so you can resume and audit:
Model::whereNull('latin_v2')->chunkById(500, function ($rows) use ($converter) {
foreach ($rows as $row) {
$row->forceFill([
'latin_v2' => $converter->toLatin($row->name_cyr),
'orthography_version' => 2,
])->saveQuietly();
}
});
saveQuietly() avoids firing observers that might re-trigger conversion.
Round-trip validation. Use fromNewLatin() as a checker, not just a parser. For each backfilled row, convert forward, then back, and flag rows where the round-trip does not return the source. Those are exactly the strings where the apostrophe or e fixes changed behaviour, and they are where your manual review should go.
Dual-read during rollout. Add a latin_v2 column rather than overwriting. Render from latin_v2 when present, fall back to live conversion. This lets you roll back the release without a data restore.
Search and slugs need their own mapping. Str::slug() will ASCII-fold Ö Ğ Ş Ç to O G S C, which is usually acceptable but will silently collapse distinct names. If you need stable slugs, generate them from the pre-conversion source and store them; do not regenerate after the orthography change or every URL shifts.
Queue the bulk work, and make the job accept an explicit converter version so retries after a deploy do not mix mapping tables.
Ş where it used to produce Sh. Any test asserting exact transliterated strings will fail, and any snapshot/export diff will be noisy. Budget for it.Oʻ may arrive as U+02BB (modifier letter turned comma), U+2018, U+2019, a plain ASCII ', or a backtick, depending on the input source (Word, Excel, copy-paste, an old CMS). Normalise to a single codepoint before calling the converter, and normalise to NFC. The release fixed apostrophe handling, but it cannot fix your upstream data.fromNewLatin() is not a guaranteed inverse. Diacritics remove information: the new alphabet encodes digraphs as single letters, and the reverse mapping has to guess case and word boundaries. Round-trip Ş/Ç across morpheme seams (e.g. ch spanning a prefix and root) and verify. Treat round-trip failures as expected for a small tail, not as a bug in your code.Sh, SH, sh, and sH are different inputs. Check whether the converter case-folds internally; if not, use mb_strtolower/mb_convert_case(..., MB_CASE_TITLE) with the correct locale, and be aware MB_CASE_TITLE behaves oddly on digraphs.mbstring. Multibyte functions are mandatory; add ext-mbstring to your composer.json explicitly rather than relying on it being present in the base image.Ö Ğ Ş Ç are fine in utf8mb4, but a utf8mb3/latin1 column or a ..._general_ci collation will mangle or over-match them. Verify the columns you backfill are utf8mb4_unicode_ci (or unicode_520_ci).php artisan vendor:publish and check whether mapping tables or a "legacy mode" toggle are exposed. If a legacy/pre-2026 mapping exists, keep it config-driven rather than forking the package.readonly, enums, or named arguments in vendor code — and if you extend its classes, keep your overrides signature-compatible.>=7.0 floor usually means the CI matrix starts at 7.0 and may not exercise PHP 8.3/8.4. Run the suite locally on your actual PHP version before trusting green.bin2hex() on the input first. Nine out of ten "the converter is wrong" reports are a non-breaking space, a curly apostrophe, or an NFD-decomposed Ö (O + U+0308).latin_v2 column and the old code path intact. Ship the column first, backfill second, flip reads third.How can I help you explore Laravel packages today?