Building a Right-to-Left Invoice Template With AI
11 min read · updated August 11, 2026
Asking a model for “an Arabic invoice template” reliably produces an English invoice with Arabic labels: columns in left-to-right order, the currency symbol in front of the amount, the date in the wrong order, and the total in the wrong corner. All four are fixable in the prompt if you know what to ask for, and the parts that are locale-dependent should not be hand-written at all.
What mirrors and what must not
Setting dir="rtl" on the document mirrors more than people expect, and less than they need. Decide each of these deliberately:
- Table column order mirrors. Under an RTL table direction the first column in source order draws at the right, which is what an Arabic reader wants: description first, amount last, read right to left.
- Block layout mirrors. The seller block, the buyer block, the logo and the totals box swap sides — provided the layout uses logical properties or flex/grid, which mirror automatically, rather than absolute physical positioning, which does not.
- Numbers do not mirror. The digits of a total read left-to-right in Arabic as in English. Nothing to do.
- Identifiers must not mirror or reorder. The invoice number, VAT registration number, IBAN and any reference code are left-to-right strings that frequently contain hyphens or letters, and they will fragment exactly like a date does. Isolate every one.
- The logo and any signature image do not mirror. Their position moves; the image is not flipped.
The template skeleton
<!doctype html>
<html lang="ar" dir="rtl">
<head><meta charset="utf-8"><style>
body { font-family: "Noto Naskh Arabic", serif; }
.meta { text-align: start; }
.totals { margin-inline-start: auto; width: 320px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 6px 10px; text-align: start; }
td.amount { text-align: end; font-variant-numeric: tabular-nums; }
.ltr { direction: ltr; unicode-bidi: isolate; }
@page { size: A4; margin: 18mm; }
</style></head>
<body>
<header class="meta">
<h1>فاتورة</h1>
<p>رقم الفاتورة: <span class="ltr">INV-2026-00841</span></p>
<p>التاريخ: <span class="ltr">2026-08-11</span></p>
</header>
<table>
<thead>
<tr><th>الوصف</th><th>الكمية</th><th>سعر الوحدة</th><th>المجموع</th></tr>
</thead>
<tbody>
<tr>
<td>اشتراك شهري</td>
<td class="amount">2</td>
<td class="amount">150.00 ر.س</td>
<td class="amount">300.00 ر.س</td>
</tr>
</tbody>
</table>
<div class="totals">
<p>الإجمالي قبل الضريبة: <span class="amount">300.00 ر.س</span></p>
<p>ضريبة القيمة المضافة (15%): <span class="amount">45.00 ر.س</span></p>
<p><strong>الإجمالي: <span class="amount">345.00 ر.س</span></strong></p>
</div>
</body>
</html>Three things in that skeleton are doing the heavy lifting. dir="rtl" on the <html> element rather than on a wrapper, so nothing inherits the wrong default. text-align: start and end everywhere instead of left and right. And the .ltr class on every identifier, which is isolation rather than a direction override, so the identifier is opaque to the Arabic around it.
font-variant-numeric: tabular-nums on the amount cells is worth the line: proportional digits make a column of currency values fail to line up at the decimal point, which reads as sloppiness on a document somebody is going to pay.
Currency, digits, dates and the calendar trap
Currency symbol placement, decimal separator, thousands separator, digit shape and date order are all locale data. Hand-writing them is how you end up with a Saudi invoice formatted like a Moroccan one. Use Intl.NumberFormat and Intl.DateTimeFormat, which read the CLDR data your platform ships.
// Currency: let the locale decide symbol position and separators.
const money = new Intl.NumberFormat("ar-SA", {
style: "currency",
currency: "SAR",
});
money.format(345); // symbol placed per locale
// Digit shape is a locale extension, not a separate option.
// -u-nu-latn forces ASCII digits; -u-nu-arab gives U+0660..U+0669.
const latinDigits = new Intl.NumberFormat("ar-SA-u-nu-latn", {
style: "currency", currency: "SAR",
});
// Dates: ar-SA defaults to the Islamic calendar. If the invoice
// needs Gregorian dates, say so explicitly.
const date = new Intl.DateTimeFormat("ar-SA-u-ca-gregory-nu-latn", {
year: "numeric", month: "2-digit", day: "2-digit",
});The calendar line is the trap that catches people once. The ar-SA locale’s default calendar is the Islamic one, so a date formatted without an explicit calendar extension is not the date you passed in as far as the reader is concerned — it is the same instant expressed in a different era, and on a legal document that is a real problem. Ask for -u-ca-gregory when you mean Gregorian, or use a locale whose default is Gregorian, and state on the invoice which calendar it is.
Digit shape deserves an explicit decision too. Arabic-Indic digits are correct and idiomatic for prose; ASCII digits are safer for anything that will be re-keyed, matched against a bank record or read by software. Many businesses use ASCII digits on invoices for exactly that reason. The full argument is in Arabic-Indic numerals in AI output, and the placement question generally in currency symbol placement by locale.
Building it
- Ask the model for the template with the direction requirements stated, not implied. A prompt that works: “Produce a single-file HTML invoice template. Set
lang="ar"anddir="rtl"on the html element. Use only logical CSS properties —margin-inline-start,padding-inline-end,text-align: start/end— and noleftorrightanywhere. Wrap the invoice number, VAT number and IBAN in a span withdirection: ltr; unicode-bidi: isolate. Leave every amount and date as a placeholder token; do not format numbers in the HTML.” - Grep the result for the properties you banned. Models emit
text-align: rightunder an RTL document constantly, because it looks correct in the one case they were thinking about and breaks the moment the template is reused for a left-to-right locale. - Fill the placeholders from
Intlformatters in your own code, with the locale, currency, numbering system and calendar passed in explicitly. Do not let the model produce formatted numbers: it will produce plausible ones, and a plausible total on an invoice is a defect of a different magnitude from a misaligned column. - Isolate every identifier. Invoice numbers, order references, VAT registrations and IBANs are letter-digit-hyphen strings and are the single most likely thing on the page to fragment.
- Render to PDF through a headless browser rather than a direct PDF writer, so the bidi algorithm and the Arabic shaper both run. The reasons are in handling RTL text in an AI-generated PDF.
- Embed the Arabic font in the PDF and check the subsetting kept the OpenType layout tables. A subset without them renders every letter in its isolated form.
Verifying before you send one
Four checks, each of which catches a class of failure the others miss. Run them on a filled invoice, not on the empty template.
- Extract the text from the finished PDF and compare it to what you put in. If the invoice number comes back reordered or the Arabic comes back as presentation forms, the document looks right and is not machine-readable — which matters where e-invoicing regulations require a parseable file.
- Render the same template with
dir="ltr"and an English locale. If the layout is still sane, your CSS is genuinely logical. If it collapses, there is a physical property hiding somewhere. - Put a long English product description in one line item. Real invoices have them, and it is the line that will reveal a missing isolate.
- Check the totals arithmetic against your own numbers. Formatting is the model’s job here and arithmetic is not.