enaxon / rtly-kit
Dates, numbers and checks for Persian and Arabic PHP apps: Jalali, Hijri and Hebrew calendars, Iranian validators, number words, holidays and prayer times. No required dependencies.
Fund package maintenance!
Requires
- php: ^8.2
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- illuminate/container: ^11.0||^12.0||^13.0
- illuminate/database: ^11.0||^12.0||^13.0
- illuminate/support: ^11.0||^12.0||^13.0
- illuminate/translation: ^11.0||^12.0||^13.0
- illuminate/validation: ^11.0||^12.0||^13.0
- infection/infection: ^0.32.6
- nesbot/carbon: ^3.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
Suggests
- illuminate/support: For Laravel auto-discovery: service provider, Jalali facade, validation rules and the JalaliCast Eloquent cast
- nesbot/carbon: For Carbon macros (toJalali, jformat, createFromJalali, ...)
Provides
None
Conflicts
None
Replaces
None
README
English | فارسی | العربية (guide)
RTLY-Kit
Dates, numbers and checks for Persian and Arabic PHP apps.
Jalali, Hijri and Hebrew calendars. Iranian validators. Number words, holidays and prayer times. One small package. No required dependencies.
Why this exists
Persian and Arabic PHP tools are spread over many packages. One handles Jalali dates. Another checks national codes. A third turns numbers into words. Most of them need Carbon or a whole framework.
RTLY-Kit puts the common pieces in one place. It is plain PHP. You install it, import a function and use it. There is no setup step, and the config file is optional.
Install
composer require enaxon/rtly-kit
require 'vendor/autoload.php'; use function RtlyKit\{jdate, to_persian, number_to_words, is_national_code}; echo jdate('2026-03-21')->format('l j F Y'); // شنبه 1 فروردین 1405 echo to_persian(1405); // ۱۴۰۵ echo number_to_words(1234); // یک هزار و دویست و سی و چهار var_dump(is_national_code('0499370899')); // bool(true)
The helpers are namespaced functions, so they cannot clash with your own code. If you like short global names, you can turn them on.
What is inside
| Area | What you get |
|---|---|
| Calendars | Jalali, Hijri (Umm al-Qura) and Hebrew. Immutable, comparable with each other, plain PHP |
| Validators | National code, Sheba, bank card, mobile, postal code, vehicle plate. Clear results with stable error codes |
| Numbers | Persian, Arabic and English digits. Separators, ordinals. Number words in Persian and Arabic, with gender, case and vowel marks for Arabic |
| Text | Arabic to Persian letters, ZWNJ cleanup, direction and script detection, Persian slugs |
| Holidays | Official Iranian holiday dates for 1394 and 1396 to 1405, reported dates for 1380 to 1393 and 1395, estimates for other years. Offsets and overrides, weekends and business days |
| Prayer times | Tehran, MWL, ISNA, Egypt, Makkah and Karachi methods, high-latitude rules and manual tuning |
| Carbon and Laravel | Carbon macros, six validation rules, a Jalali facade, an Eloquent cast and an optional config file |
What you can do
Work with three calendars
Jalali, Hijri and Hebrew share one contract, so you can compare dates from different calendars.
use RtlyKit\Calendar\Jalali; use function RtlyKit\{jdate, hdate, hebrew_date}; $date = jdate('2026-03-21'); echo $date->addMonths(1)->format('Y/m/d'); // 1405/02/01 echo Jalali::create(1405, 1, 1)->toGregorian()->format('Y-m-d'); // 2026-03-21 echo hdate('2026-03-21')->format('j F Y', 'en'); // 2 Shawwal 1447 echo hebrew_date('2026-03-21')->format('j F Y', 'en'); // 3 Nisan 5786
All three are plain PHP. You do not need ext-calendar. See the Jalali guide and the compare guide.
Check Iranian data
Each validator gives you a clear result, not just true or false.
use function RtlyKit\{validate_sheba, validate_national_code}; $result = validate_sheba('IR27 0170 0000 0010 0324 2000 01'); $result->isValid(); // true $result->details(); // ['normalized' => 'IR270170000000100324200001', 'bank_code' => '017', 'bank_name' => 'بانک ملی ایران'] $bad = validate_national_code('0499370898'); $bad->errors(); // ['invalid_checksum']
Error codes are stable strings, like invalid_format and invalid_checksum. You turn them into your own messages. Validators never throw. Odd input, such as null or an array, gives invalid_type. A mobile result also tells you if the prefix lies in a mobile block of the national numbering plan (allocated).
Write numbers and clean text
use RtlyKit\Number\NumberToWords; use function RtlyKit\{format_number, ordinal, normalize_text, text_direction, number_to_words}; echo format_number(1234567.5); // ۱٬۲۳۴٬۵۶۷٫۵ echo ordinal(30); // سیام echo normalize_text('كتاب ٣ يك'); // کتاب ۳ یک echo text_direction('سلام'); // rtl echo number_to_words(1234, 'ar'); // ألف ومئتان وأربعة وثلاثون // Arabic: a counted noun follows, and it is feminine echo NumberToWords::convert(3, 'ar', ['mode' => 'noun', 'gender' => 'f']); // ثلاث echo NumberToWords::ordinal(21, 'ar', ['gender' => 'f']); // الحادية والعشرون
Find holidays and prayer times
use RtlyKit\Holiday\HolidayCalendar; use RtlyKit\Prayer\PrayerTimes; use function RtlyKit\is_iran_holiday; var_dump(is_iran_holiday(1405, 1, 1)); // true // Correct an estimated year when the moon is seen a day later $calendar = HolidayCalendar::default()->withIslamicOffset(1); $calendar->sourceOf(1406)->value; // estimated $times = PrayerTimes::forCity('mecca', PrayerTimes::METHOD_MAKKAH) ->getTimes(new DateTimeImmutable('2026-06-01')); echo $times['fajr']; // 04:11
See Holiday calendar and Prayer times.
Use it with Carbon and Laravel
Carbon macros turn on by themselves when Carbon is installed.
use Carbon\Carbon; echo Carbon::parse('2026-03-21')->toJalali()->format('Y/m/d'); // 1405/01/01 echo Carbon::createFromJalali(1405, 1, 1)->toDateString(); // 2026-03-21 echo Carbon::parse('2026-03-21')->toHijri()->format('Y/m/d'); // 1447/10/02
Laravel finds the package on its own. You get six validation rules (mobile is a short alias of iran_mobile) with Persian, English and Arabic messages, a Jalali facade and an Eloquent cast. Publish the optional config file with php artisan vendor:publish --tag=rtly-kit-config.
$request->validate([ 'national_code' => ['required', 'national_code'], 'sheba' => ['required', 'sheba'], 'phone' => ['required', 'iran_mobile'], ]); protected $casts = ['published_at' => \RtlyKit\Laravel\Casts\JalaliCast::class];
More in the Laravel guide.
A full guide, in three languages
The guide has 29 pages in English, Persian and Arabic. It has search, a light and a dark theme, and works without JavaScript. Persian and Arabic pages are written right to left.
![]() |
![]() |
| English, light theme | Persian, dark theme, right to left |
| Topic | Read |
|---|---|
| Start | Quick start · Installation |
| Calendars | Jalali · Hijri · Hebrew · Convert and compare · Carbon macros |
| Validation | Validators overview · National code · Sheba and bank card · Mobile, postal code, plate |
| Numbers and text | Digits and format · Number words · Arabic number words · Text tools |
| Dates and times | Holidays · Holiday calendar · Prayer times |
| Laravel | Setup · Validation and cast |
| Reference | Helpers · Errors · API stability · Accuracy and data · Limits · Upgrade · Verifying releases · FAQ |
| Other languages | فارسی · العربية |
Helpers without clashes
The 27 helpers live in the RtlyKit namespace. Import what you need with use function. Nothing is added to the global scope.
Want short names like jdate()? Switch them on once, for example in your bootstrap file:
$skipped = \RtlyKit\Globals::register(); // names that were already taken
It only adds names that are free. It never replaces your functions and never throws. You can call it twice. The return value lists any names it skipped.
When something goes wrong
Every error the library throws extends RtlyKit\Exceptions\RtlyKitException. Catch that one class and you have them all. Each error has a stable code and some context:
use RtlyKit\Exceptions\RtlyKitException; use function RtlyKit\jdate; try { jdate('not a date'); } catch (RtlyKitException $e) { $e->getErrorCode(); // an ErrorCode case, here invalid_date $e->getContext(); // extra details, as an array }
Bad input never leaks a raw TypeError or ValueError. Years outside the supported range throw InvalidDateException. Read the error guide for the full list.
What we checked
On 2026-10-08 we compared the library with official sources wherever we could reach them.
- Jalali. The conversion gives the same Nowruz dates and leap years as the official table of the University of Tehran for every year from 1206 to 1497. It agrees with the astronomical definition for 1178 to 1502.
- Hijri. Every month start from AH 1318 to 1500 (2196 months) matches the official KACST Umm Al-Qura calendar. AH 1300 to 1317 use ICU/CLDR data.
- Holidays. Official dates for the Jalali years 1394 and 1396 to 1405, checked against two sources, and published dates for 1380 to 1393 and 1395. Other years are estimated from the Hijri calendar. Fixed holidays, like Nowruz, are exact.
- Prayer times. Sunrise and sunset match the NOAA Solar Calculator within a minute. Compared with published tables, the times agree within 1 to 2 minutes for Tehran (Fajr, sunrise, noon, Maghrib), Makkah (all times, including Isha in Ramadan), Egypt (Dar al-Ifta) and Karachi with Hanafi Asr (a Karachi institution, 31 days). The Fajr and Isha of Turkey's Diyanet fit the 18 and 17 degree angles. The Hanafi Asr rule and the ISNA 15 degree angles match statements of Darul Uloom Deoband and the Fiqh Council of North America.
- Data tables. The 19 Sheba bank codes in the Central Bank's published IBAN specification match our table. Every mobile prefix we list lies inside a mobile block of the national numbering plan. Bank BINs and other codes agree with several public pages.
Good to know
- Iran sets religious dates by moon sighting, so an estimated year can be a day or two off. You can correct it with HolidayCalendar.
- We found no timetable issued by ISNA, the Muslim World League or the University of Islamic Sciences in Karachi, and no official Tehran table with Isha and Asr.
- Bank, operator and place names come from public lists, not from an official registry. A valid card can return
nullfor the bank name. - A few Arabic number-word outputs still wait for review by a native speaker.
Found a mistake? Please open an issue with a source. The full list is in the accuracy guide.
Requirements
- PHP 8.2 or newer. That is all it needs. No
mbstringor other extension is required. - No required Composer packages.
nesbot/carbonandilluminate/supportare optional. - Tested in CI on PHP 8.2, 8.3, 8.4 and 8.5. Works with Laravel 11, 12 and 13 (Laravel 13 needs PHP 8.3 or newer) and Carbon 3.
Contributing
Bug reports, data corrections, translation fixes and small pull requests are welcome. Read CONTRIBUTING.md first. It explains the Docker workflow, the code style and how we handle data sources.
- Report a wrong result. Use the data-correction form and add a source. Every table entry needs at least two.
- Improve the words. Fixes for the Persian guide and for Arabic translations are very welcome.
- Star the repository if it saves you time. It helps other people find it.
If RTLY-Kit saves you time, you can buy me a coffee. It is never expected, and it is very much appreciated.
Security problems go through SECURITY.md, not public issues.
Contributors
Thanks to everyone who has helped. Your name appears here after your first merged contribution.
Moving from an early build? See UPGRADE.md. Recent changes are in CHANGELOG.md.
License
MIT. Data sources and their notes are in SOURCES.md and NOTICE.
About
Made by Ehsan Enaloo. RTLY-Kit is a toolkit for Jalali, Hijri and Hebrew calendars, Iranian validators, number words, holidays and prayer times in PHP.
Topics: jalali hijri hebrew persian arabic rtl php laravel carbon iran prayer-times


