Skip to content

RTL and Persian localization

rtl-and-persian-localization

Localize a storefront for Persian (fa_IR) and right-to-left layout: logical CSS, digits, Yeh and Kaf, ZWNJ, currency display, and Gregorian storage. Use when building or reviewing Persian, Farsi, or RTL commerce UI.

Version 0.3.2  ·  MIT  ·  Source  ·  Rule

Install this skill

Copy the skill and its rule into your project:

npx @sadrmanesh/commerce-agent-skills add rtl-and-persian-localization

The skill goes to .agents/skills/ and the rule to .agents/rules/. See Command line for other directories.

Use this skill for a Persian storefront or any right-to-left commerce UI. Search matching uses the marketplace search skill. The normalizer in scripts/normalize-persian.mjs is for display and for search text. Letter and digit rules are in the Persian text notes.

Procedure

  1. Set the document language and direction. For Persian, lang="fa" and dir="rtl". In WordPress, the fa_IR locale is RTL and is_rtl() is the check to use in PHP.
  2. Lay out with CSS logical properties. Do not mirror the whole page with a plugin that flips left and right in existing files.
  3. Store catalog text as Unicode with Persian Yeh and Kaf and with ZWNJ where the word needs it. Store dates as Gregorian timestamps. Store money as a decimal amount plus an ISO currency code.
  4. Format digits for display only. APIs, SKUs, payment amounts, and HTML attributes stay ASCII.
  5. Translate user-facing strings with the plugin text domain. Do not hard-code Persian in a PHP string that should go through gettext, and do not hard-code English in a template that Persian shoppers see.

Direction

  • Prefer margin-inline-start, padding-inline, inset-inline-start, border-inline-end, and text-align: start.
  • Use flex and grid. flex-direction: row follows the document direction. Use row-reverse only when the order is intentionally opposite to the language.
  • Mirror directional icons such as back and forward. Do not mirror logos, payment marks, product photos, or numbers.
  • Form focus order follows the visual order. Test with the keyboard, not only with a screenshot.
  • WooCommerce emails that render the same template need the same direction. A storefront dir="rtl" does not change an email that is a separate HTML document.

Persian text

  • Locale code: fa_IR.
  • Fold Arabic Yeh ي (U+064A) to Persian Yeh ی (U+06CC) and Arabic Kaf ك (U+0643) to Persian Kaf ک (U+06A9) when you normalize input. Shoppers and old content use both.
  • Keep ZWNJ (U+200C) in stored and displayed words (می‌روم). Search normalization may remove it so the two spellings match. Display normalization must not.
  • Persian digits ۰۱۲۳۴۵۶۷۸۹ are a display format. Also accept Arabic-Indic digits ٠١٢٣٤٥٦٧٨٩ on input and normalize them.
  • Use a font that covers Arabic script. A Latin-only stack renders Persian as missing glyphs.

Run the script:

node scripts/normalize-persian.mjs --file title.txt
node scripts/normalize-persian.mjs --search "كالاي ۱۲"

--search prints the match form. The default prints the display form and keeps ZWNJ. --persian-digits converts ASCII digits to Persian digits for display.

Money, dates, addresses

  • WooCommerce currency for the Iranian rial is the ISO code IRR. Format the amount with the store's decimal settings (wc_get_price_decimals()), not with a hard-coded exponent.
  • Toman is a display and pricing decision: 1 toman equals 10 rials. If the shop charges in toman, say that in the currency label and store a consistent unit. Do not divide amounts by 10 only in the theme while checkout charges rials.
  • WordPress and WooCommerce store order dates in Gregorian, with a site timezone and a GMT field. A Jalali calendar is a presentation layer. Do not write Jalali strings into order date columns.
  • Iranian addresses need country IR, province, city, street, and a 10-digit postal code. Do not require a state from a country list that has no Iranian provinces unless those provinces are actually configured. Add them through WooCommerce's country locale and state list rather than a one-off checkout hack.
  • Phone numbers: accept a local mobile number without a North American mask. Store a normalized form you can dial, and display the local form.

WordPress and WooCommerce

  • Put every new string in gettext with the plugin text domain. Load private translations on init.
  • Switch field order and labels with woocommerce_get_country_locale and woocommerce_localisation_address_formats for IR, not by editing core templates first.
  • body gets the rtl class from WordPress when the locale is RTL. Theme CSS should key off dir or .rtl and still prefer logical properties so the same stylesheet serves both directions.
  • Prices in templates go through wc_price(). That helper uses the store currency settings. Do not concatenate a hard-coded ریال or $ in front of a raw number.

Gotchas

  • direction: rtl on a single inner box, while the document is LTR, traps punctuation and mixed English SKUs. Set direction on the document, and on emails, not on random cards.
  • left and right in copied CSS survive an RTL locale and pin totals to the wrong side.
  • Replacing every digit in the HTML document breaks CSS, JSON, and payment signatures. Convert digits in text nodes you control.
  • Sorting Persian with a binary code-point order is not alphabetical. Use a Persian collation when the list is a dictionary. Do not use it for SKUs.
  • Translating a string that includes the price ("Pay 19.00") needs a placeholder, not concatenation: sprintf( __( 'Pay %s', 'example' ), $formatted ).

Verification

  • fa_IR renders the shopper-facing checkout RTL, with the total and the pay control aligned to the inline end.
  • A title that contains Arabic Yeh and Persian digits round-trips through display normalization without losing ZWNJ.
  • Search normalization makes كالا and کالا match. The stored title is unchanged.
  • An order date stored by WooCommerce remains Gregorian. Any Jalali label is display-only.
  • A test payment uses the same major units the shopper saw. Toman and rial are not mixed inside one order.

Evaluation

Worked examples and the checks they must satisfy are in evals/tasks.json.

References

Persian text handling

Use this note when you change strings, search tokens, or formatted amounts. The functions in scripts/normalize-persian.mjs implement the folds below.

Letters

Input Code point Fold to
Arabic Yeh ي U+064A Persian Yeh ی U+06CC
Persian Yeh ی U+06CC unchanged
Arabic Kaf ك U+0643 Persian Kaf ک U+06A9
Persian Kaf ک U+06A9 unchanged

Apply this fold on input and before search. Display the folded Persian letters.

ZWNJ (U+200C) is part of Persian orthography. Display and storage keep it. Search text removes it so می‌روم and میروم match. Do not replace ZWNJ with a normal space. That splits one word into two.

Arabic Teh Marbuta ة is not a Persian letter. Do not fold it to Heh unless you have a specific migration from Arabic copy. Leave it and show it.

Digits

Glyphs Code points Use
0-9 U+0030–U+0039 APIs, SKUs, JSON, payment minor units, HTML attributes
۰-۹ U+06F0–U+06F9 Optional Persian display
٠-٩ U+0660–U+0669 Accept on input, then fold to ASCII or to Persian digits

toAsciiDigits maps both Persian and Arabic-Indic digits. toPersianDigits maps ASCII to Persian. Do not run toPersianDigits on a whole HTML or JSON document.

Normalization entry points

  • normalizePersianDisplay(text) folds Yeh and Kaf, keeps ZWNJ, and leaves digits unless you pass { digits: "persian" } or { digits: "ascii" }.
  • normalizePersianSearch(text) folds letters, removes ZWNJ, converts digits to ASCII, lowercases Latin letters, and collapses whitespace.

Latin lowercasing is for mixed catalog titles. Persian letters have no case.

Currency display

ISO 4217 code in WooCommerce: IRR for the Iranian rial.

If the merchant prices in toman, pick one stored unit and label it. A theme-only division by 10 will not match the amount sent to the payment provider. There is no separate ISO code to assume for toman in this skill. Do not invent a code in payment requests.

Format through wc_price() so the decimal separator and currency symbol come from WooCommerce settings.

Dates

Store date_created_gmt as WooCommerce provides it. Format for fa_IR with wp_date() for Gregorian output in the locale. A Jalali formatter, if the shop uses one, reads that timestamp and must not write back a Jalali string.

Addresses

Postal codes in Iran are 10 digits. Validate the length after converting digits to ASCII. Province names should come from the configured state list for IR so shipping zones and the address form agree.

Evaluation

Fixtures from evals/tasks.json. See evaluation fixtures for how the checks work.

document-direction

Render the Persian storefront in the fa_IR locale with right-to-left document direction and logical CSS.

Must include: fa_IR, dir="rtl"

Must not include: margin-left

Worked example

Set the document for Persian.

<html lang="fa" dir="rtl">

Use the fa_IR locale so is_rtl() is true. Space the total with margin-inline-start, not a physical left margin.

rial-display

Show an Iranian rial price through WooCommerce formatting without converting to toman only in the theme.

Must include: IRR, wc_price

Must not include: / 10

Worked example

Format the stored amount with the store currency.

echo wp_kses_post( wc_price( $amount ) );

The ISO code is IRR when the shop charges rials. Do not divide the charged amount in the template.