Workshop Project: A Multi-Country Invoice System for Saudi Arabia, UAE, and Egypt
Build a localization-aware invoice system step by step in JavaScript — automatic currencies, dates, and tax rates for Saudi Arabia, UAE, and Egypt using the Intl API.
Word count: ~2400 • Reading time: 12 minutes
Workshop Project: A Multi-Country Invoice System
We build it step by step — then put the complete code together at the end
Note: This article is the final workshop project of the series. It stands on its own, but draws on concepts built across the three previous articles — Digits and Fonts, the Intl API, and database dilemmas.
In this article from Zy Yazan Platform, we reach the real test: does everything we learned across the previous three articles translate into a working system? The answer is yes — and we’ll prove it with real code, one step at a time.
The project: a simple invoice system that renders the same invoice in three distinct formats — Saudi Arabia, the United Arab Emirates, and Egypt — at the press of a button. Currencies, dates (including Hijri), percentages, and currency symbol placement all adapt automatically.
Step One: Define Country Settings (Config Layer)
The first architectural decision in any localization system: separate each region’s settings from the display logic. This means adding a new country later requires nothing more than a new config object — no changes to any rendering code.
// ── Step 1: Country Configuration ────────────────────────
const COUNTRY_CONFIG = {
'SA': {
label: 'Saudi Arabia',
locale: 'ar-SA',
currency: 'SAR',
vatRate: 0.15, // 15% VAT
showHijri: true, // show Hijri date
decimalDigits: 2, // decimal places for currency
},
'AE': {
label: 'United Arab Emirates',
locale: 'ar-AE',
currency: 'AED',
vatRate: 0.05, // 5% VAT
showHijri: false,
decimalDigits: 2,
},
'EG': {
label: 'Egypt',
locale: 'ar-EG',
currency: 'EGP',
vatRate: 0.14, // 14% VAT
showHijri: false,
decimalDigits: 2,
},
};
Three decisions are embedded here: VAT rates are stored as decimal fractions (0.15) to plug directly into Intl, currency codes follow ISO 4217, and showHijri is a separate flag because Saudi Arabia is the only country in this system that defaults to Hijri dating.
Step Two: Build the Formatter Layer
Instead of creating Intl objects scattered across the codebase, we build a single function that creates them once per country and returns ready-to-use formatters — solving the performance problem discussed in article two:
// ── Step 2: Formatter Layer ───────────────────────────────
function buildFormatters(countryCode) {
const config = COUNTRY_CONFIG[countryCode];
if (!config) throw new Error(`Unknown country: ${countryCode}`);
const { locale, currency, decimalDigits } = config;
return {
// Currency amounts
currency: new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
minimumFractionDigits: decimalDigits,
maximumFractionDigits: decimalDigits,
}),
// Percentages
percent: new Intl.NumberFormat(locale, {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: 1,
}),
// Plain numbers (for quantities)
number: new Intl.NumberFormat(locale, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}),
// Gregorian date
dateGregorian: new Intl.DateTimeFormat(`${locale}-u-ca-gregory`, {
day: '2-digit',
month: 'long',
year: 'numeric',
}),
// Hijri date (for Saudi Arabia)
dateHijri: new Intl.DateTimeFormat(`${locale}-u-ca-islamic`, {
day: '2-digit',
month: 'long',
year: 'numeric',
}),
};
}
Step Three: Invoice Calculator
Now we build the function that takes raw invoice line items and produces computed totals. This function works exclusively on raw numbers — no formatting, ever:
// ── Step 3: Invoice Totals ────────────────────────────────
function calculateInvoice(items, vatRate) {
// Sum raw numbers — no formatting here, ever
const subtotal = items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice,
0
);
// Tax calculated on the full subtotal (documented decision)
const vatAmount = Math.round(subtotal * vatRate * 100) / 100;
const total = subtotal + vatAmount;
return { subtotal, vatAmount, total };
}
Notice the comment “no formatting here, ever.” That explicit reminder in the code prevents a future teammate from slipping an Intl.format() call inside a calculation function — a common mistake that’s surprisingly hard to track down.
Step Four: Dual-Date Formatting
Saudi Arabia requires both dates on an official invoice — Hijri and Gregorian. We built a simple version of this function in article two; here we extend it to integrate with the config system:
// ── Step 4: Date Formatting by Country ───────────────────
function formatInvoiceDate(date, countryCode, formatters) {
const config = COUNTRY_CONFIG[countryCode];
const gregorian = formatters.dateGregorian.format(date);
if (!config.showHijri) {
// UAE and Egypt: Gregorian only
return gregorian;
}
// Saudi Arabia: Hijri first, then Gregorian
const hijri = formatters.dateHijri.format(date);
return `${hijri} — ${gregorian}`;
}
// Sample outputs for May 15, 2026:
// SA → 'Dhu al-Qi'dah 17, 1447 AH — May 15, 2026'
// AE → 'May 15, 2026'
// EG → 'May 15, 2026'
Step Five: Building the Invoice HTML
Now we bring everything together in a function that produces the complete invoice HTML. It takes invoice data and a country code, and returns HTML ready to inject into the page. Below the code block we’ll show what the rendered output actually looks like:
// ── Step 5: Build Invoice HTML ────────────────────────────
function buildInvoiceHTML(invoiceData, countryCode) {
const config = COUNTRY_CONFIG[countryCode];
const formatters = buildFormatters(countryCode);
const { subtotal, vatAmount, total } =
calculateInvoice(invoiceData.items, config.vatRate);
const dateStr = formatInvoiceDate(
invoiceData.date, countryCode, formatters
);
// Build line item rows
const itemRows = invoiceData.items.map(item => {
const lineTotal = item.quantity * item.unitPrice;
return `
<tr>
<td style="padding:10px; border:1px solid #ddd;">
${item.name}
</td>
<td style="padding:10px; border:1px solid #ddd;
text-align:center; font-variant-numeric:tabular-nums;">
${formatters.number.format(item.quantity)}
</td>
<td style="padding:10px; border:1px solid #ddd;
text-align:end; font-variant-numeric:tabular-nums;">
${formatters.currency.format(item.unitPrice)}
</td>
<td style="padding:10px; border:1px solid #ddd;
text-align:end; font-variant-numeric:tabular-nums;">
${formatters.currency.format(lineTotal)}
</td>
</tr>`;
}).join('');
// Build complete HTML
return `
<div dir="rtl" style="font-family:'Amiri',serif; max-width:700px;
margin:0 auto; padding:30px; border:1px solid #ddd; border-radius:8px;">
<div style="display:flex; justify-content:space-between;
align-items:flex-start; margin-bottom:30px;">
<div>
<h2 style="color:#c0392b; margin:0;">فاتورة ضريبية</h2>
<p style="color:#666; margin:5px 0; font-size:0.9em;">
رقم الفاتورة: ${invoiceData.number}
</p>
<p style="color:#666; margin:5px 0; font-size:0.9em;">
التاريخ: ${dateStr}
</p>
</div>
<div style="text-align:left;">
<p style="font-weight:bold; margin:0;">${config.label}</p>
<p style="color:#666; margin:5px 0; font-size:0.9em;">
ضريبة القيمة المضافة:
${formatters.percent.format(config.vatRate)}
</p>
</div>
</div>
<div style="background:#f9f9f9; padding:15px; border-radius:4px;
margin-bottom:25px;">
<p style="margin:0;">
<strong>العميل:</strong> ${invoiceData.client.name}
</p>
<p style="margin:5px 0; color:#666; font-size:0.9em;">
${invoiceData.client.address}
</p>
</div>
<div style="overflow-x:auto; margin-bottom:25px;">
<table style="width:100%; border-collapse:collapse; font-size:0.95em;">
<thead>
<tr style="background:#1a3a5c; color:white;">
<th style="padding:10px; border:1px solid #ddd; text-align:right;">
البند
</th>
<th style="padding:10px; border:1px solid #ddd;">الكمية</th>
<th style="padding:10px; border:1px solid #ddd;">سعر الوحدة</th>
<th style="padding:10px; border:1px solid #ddd;">الإجمالي</th>
</tr>
</thead>
<tbody>${itemRows}</tbody>
</table>
</div>
<div style="width:280px; margin-right:auto; margin-left:0;">
<div style="display:flex; justify-content:space-between;
padding:8px 0; border-bottom:1px solid #eee;">
<span>المجموع قبل الضريبة</span>
<span style="font-variant-numeric:tabular-nums;">
${formatters.currency.format(subtotal)}
</span>
</div>
<div style="display:flex; justify-content:space-between;
padding:8px 0; border-bottom:1px solid #eee; color:#666;">
<span>
ضريبة القيمة المضافة
(${formatters.percent.format(config.vatRate)})
</span>
<span style="font-variant-numeric:tabular-nums;">
${formatters.currency.format(vatAmount)}
</span>
</div>
<div style="display:flex; justify-content:space-between;
padding:10px 0; font-weight:bold; font-size:1.1em;
color:#c0392b; border-top:2px solid #c0392b;">
<span>الإجمالي النهائي</span>
<span style="font-variant-numeric:tabular-nums;">
${formatters.currency.format(total)}
</span>
</div>
</div>
<p style="text-align:center; color:#999; font-size:0.8em;
margin-top:30px; border-top:1px solid #eee; padding-top:15px;">
تم إنشاء هذه الفاتورة آلياً بواسطة نظام التوطين
</p>
</div>`;
}
Figure 1 — Output of buildInvoiceHTML(invoiceData, 'SA'): dual date (Hijri + Gregorian), symbol before number, Arabic-Indic digits, 15% VAT. The invoice content remains in Arabic because that is the target locale.
Step Six: Running the System
We define the sample invoice data and run the system for all three countries:
// ── Step 6: Invoice Data and Execution ────────────────────
// Raw invoice data — no formatting, no currency symbols
const invoiceData = {
number: 'INV-2026-0042',
date: new Date(2026, 4, 15), // May 15, 2026
client: {
name: 'Future Technology Co.',
address: 'Financial District, 12th Floor',
},
items: [
{ name: 'UI/UX Design', quantity: 1, unitPrice: 4500.00 },
{ name: 'Frontend Development', quantity: 1, unitPrice: 8200.00 },
{ name: 'Review & Follow-up Sessions', quantity: 4, unitPrice: 750.00 },
{ name: 'Technical Documentation', quantity: 1, unitPrice: 1200.00 },
],
};
// Run for all three countries
const countries = ['SA', 'AE', 'EG'];
countries.forEach(code => {
const html = buildInvoiceHTML(invoiceData, code);
console.log(`\n=== Invoice: ${COUNTRY_CONFIG[code].label} ===`);
console.log(html);
// In a real app: document.getElementById(`invoice-${code}`).innerHTML = html;
});
Two lines of execution code produce three completely distinct invoices. Adding Kuwait, Bahrain, or Morocco later means adding one object to COUNTRY_CONFIG — nothing else.
That’s what scalable design means: don’t code for three countries — code for any number of countries.
Step Seven: Verifying the Output
Before assembling the full code, let’s confirm what the system should produce for a single line item worth 1,000 currency units:
| Field | Saudi Arabia (ar-SA) | UAE (ar-AE) | Egypt (ar-EG) |
|---|---|---|---|
| Amount | ر.س ١٬٠٠٠٫٠٠ | د.إ ١٬٠٠٠٫٠٠ | ١٬٠٠٠٫٠٠ ج.م |
| VAT Rate | ١٥٪ | ٥٪ | ١٤٪ |
| VAT Amount | ر.س ١٥٠٫٠٠ | د.إ ٥٠٫٠٠ | ١٤٠٫٠٠ ج.م |
| Total | ر.س ١٬١٥٠٫٠٠ | د.إ ١٬٠٥٠٫٠٠ | ١٬١٤٠٫٠٠ ج.م |
| Date Format | Hijri + Gregorian | Gregorian only | Gregorian only |
| Symbol Position | Before number | Before number | After number |
Notice that Egypt alone places the currency symbol after the number. That’s not a decision we made in code — it’s what CLDR data specifies for ar-EG, applied automatically by Intl.
The Complete Code — Ready to Copy and Run
Here’s the full system in a single JavaScript file. It runs in the browser and in Node.js with no external dependencies:
// ════════════════════════════════════════════════════════
// Multi-Country Invoice System — Financial Data Localization Guide
// Zy Yazan Platform | zyyazan.sy
// ════════════════════════════════════════════════════════
// ── 1. Country Configuration ──────────────────────────────
const COUNTRY_CONFIG = {
'SA': {
label: 'Saudi Arabia',
locale: 'ar-SA',
currency: 'SAR',
vatRate: 0.15,
showHijri: true,
decimalDigits: 2,
},
'AE': {
label: 'United Arab Emirates',
locale: 'ar-AE',
currency: 'AED',
vatRate: 0.05,
showHijri: false,
decimalDigits: 2,
},
'EG': {
label: 'Egypt',
locale: 'ar-EG',
currency: 'EGP',
vatRate: 0.14,
showHijri: false,
decimalDigits: 2,
},
};
// ── 2. Formatter Layer ────────────────────────────────────
function buildFormatters(countryCode) {
const config = COUNTRY_CONFIG[countryCode];
if (!config) throw new Error(`Unknown country: ${countryCode}`);
const { locale, currency, decimalDigits } = config;
return {
currency: new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
minimumFractionDigits: decimalDigits,
maximumFractionDigits: decimalDigits,
}),
percent: new Intl.NumberFormat(locale, {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: 1,
}),
number: new Intl.NumberFormat(locale, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}),
dateGregorian: new Intl.DateTimeFormat(`${locale}-u-ca-gregory`, {
day: '2-digit', month: 'long', year: 'numeric',
}),
dateHijri: new Intl.DateTimeFormat(`${locale}-u-ca-islamic`, {
day: '2-digit', month: 'long', year: 'numeric',
}),
};
}
// ── 3. Invoice Totals ─────────────────────────────────────
function calculateInvoice(items, vatRate) {
const subtotal = items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice, 0
);
const vatAmount = Math.round(subtotal * vatRate * 100) / 100;
const total = subtotal + vatAmount;
return { subtotal, vatAmount, total };
}
// ── 4. Date Formatting by Country ─────────────────────────
function formatInvoiceDate(date, countryCode, formatters) {
const gregorian = formatters.dateGregorian.format(date);
if (!COUNTRY_CONFIG[countryCode].showHijri) return gregorian;
const hijri = formatters.dateHijri.format(date);
return `${hijri} — ${gregorian}`;
}
// ── 5. Build Invoice HTML ─────────────────────────────────
function buildInvoiceHTML(invoiceData, countryCode) {
const config = COUNTRY_CONFIG[countryCode];
const formatters = buildFormatters(countryCode);
const { subtotal, vatAmount, total } =
calculateInvoice(invoiceData.items, config.vatRate);
const dateStr = formatInvoiceDate(
invoiceData.date, countryCode, formatters
);
const itemRows = invoiceData.items.map(item => {
const lineTotal = item.quantity * item.unitPrice;
return `
<tr>
<td style="padding:10px;border:1px solid #ddd;">
${item.name}
</td>
<td style="padding:10px;border:1px solid #ddd;
text-align:center;font-variant-numeric:tabular-nums;">
${formatters.number.format(item.quantity)}
</td>
<td style="padding:10px;border:1px solid #ddd;
text-align:end;font-variant-numeric:tabular-nums;">
${formatters.currency.format(item.unitPrice)}
</td>
<td style="padding:10px;border:1px solid #ddd;
text-align:end;font-variant-numeric:tabular-nums;">
${formatters.currency.format(lineTotal)}
</td>
</tr>`;
}).join('');
return `
<div dir="rtl" style="font-family:'Amiri',serif;max-width:700px;
margin:0 auto;padding:30px;border:1px solid #ddd;border-radius:8px;">
<div style="display:flex;justify-content:space-between;
align-items:flex-start;margin-bottom:30px;">
<div>
<h2 style="color:#c0392b;margin:0;">فاتورة ضريبية</h2>
<p style="color:#666;margin:5px 0;font-size:0.9em;">
رقم الفاتورة: ${invoiceData.number}
</p>
<p style="color:#666;margin:5px 0;font-size:0.9em;">
التاريخ: ${dateStr}
</p>
</div>
<div style="text-align:left;">
<p style="font-weight:bold;margin:0;">${config.label}</p>
<p style="color:#666;margin:5px 0;font-size:0.9em;">
ضريبة القيمة المضافة:
${formatters.percent.format(config.vatRate)}
</p>
</div>
</div>
<div style="background:#f9f9f9;padding:15px;border-radius:4px;
margin-bottom:25px;">
<p style="margin:0;">
<strong>العميل:</strong> ${invoiceData.client.name}
</p>
<p style="margin:5px 0;color:#666;font-size:0.9em;">
${invoiceData.client.address}
</p>
</div>
<div style="overflow-x:auto;margin-bottom:25px;">
<table style="width:100%;border-collapse:collapse;font-size:0.95em;">
<thead>
<tr style="background:#1a3a5c;color:white;">
<th style="padding:10px;border:1px solid #ddd;text-align:right;">
البند
</th>
<th style="padding:10px;border:1px solid #ddd;">الكمية</th>
<th style="padding:10px;border:1px solid #ddd;">سعر الوحدة</th>
<th style="padding:10px;border:1px solid #ddd;">الإجمالي</th>
</tr>
</thead>
<tbody>${itemRows}</tbody>
</table>
</div>
<div style="width:280px;margin-right:auto;margin-left:0;">
<div style="display:flex;justify-content:space-between;
padding:8px 0;border-bottom:1px solid #eee;">
<span>المجموع قبل الضريبة</span>
<span style="font-variant-numeric:tabular-nums;">
${formatters.currency.format(subtotal)}
</span>
</div>
<div style="display:flex;justify-content:space-between;
padding:8px 0;border-bottom:1px solid #eee;color:#666;">
<span>
ضريبة القيمة المضافة
(${formatters.percent.format(config.vatRate)})
</span>
<span style="font-variant-numeric:tabular-nums;">
${formatters.currency.format(vatAmount)}
</span>
</div>
<div style="display:flex;justify-content:space-between;
padding:10px 0;font-weight:bold;font-size:1.1em;
color:#c0392b;border-top:2px solid #c0392b;">
<span>الإجمالي النهائي</span>
<span style="font-variant-numeric:tabular-nums;">
${formatters.currency.format(total)}
</span>
</div>
</div>
<p style="text-align:center;color:#999;font-size:0.8em;
margin-top:30px;border-top:1px solid #eee;padding-top:15px;">
تم إنشاء هذه الفاتورة آلياً بواسطة نظام التوطين
</p>
</div>`;
}
// ── 6. Invoice Data and Execution ─────────────────────────
const invoiceData = {
number: 'INV-2026-0042',
date: new Date(2026, 4, 15),
client: {
name: 'Future Technology Co.',
address: 'Financial District, 12th Floor',
},
items: [
{ name: 'UI/UX Design', quantity: 1, unitPrice: 4500.00 },
{ name: 'Frontend Development', quantity: 1, unitPrice: 8200.00 },
{ name: 'Review & Follow-up Sessions', quantity: 4, unitPrice: 750.00 },
{ name: 'Technical Documentation', quantity: 1, unitPrice: 1200.00 },
],
};
['SA', 'AE', 'EG'].forEach(code => {
const container = document.getElementById(`invoice-${code}`);
if (container) {
container.innerHTML = buildInvoiceHTML(invoiceData, code);
}
});
// ════════════════════════════════════════════════════════
// End of invoice system — Zy Yazan Platform | zyyazan.sy
// ════════════════════════════════════════════════════════
What Can Be Added Next?
This system is intentionally simple to keep the concepts clear. In a production project, the logical next steps would be:
- Live exchange rates: plug in a currency API and convert amounts between currencies before rendering
- Input validation: verify that amounts are positive numbers and currency codes exist in ISO 4217
- PDF export: convert the invoice HTML to PDF using libraries like
puppeteerorjsPDF - Kuwait and Bahrain support: add two new objects to
COUNTRY_CONFIG— nothing else changes
Series Wrap-Up
Across four articles, we covered a complete arc: from the difference between two code points in a Unicode table, to a production-ready invoice system serving three countries at the press of a button.
The principle that ties the whole series together is simple: separate storage from display, document your decisions, and let international standards do the heavy lifting. The Intl API isn’t a clever trick — it’s the product of hundreds of specialists who built CLDR to document the numeric and date conventions of every culture on earth. Using it is the smartest call any developer can make when building for an Arabic-speaking audience.
And if you’re curious about how a computer sees Arabic text in the first place — before any number gets formatted — that’s a different story, one we told in our earlier series on Unicode logic and bidirectional text.
References and Sources:
- MDN Docs —
Intl.NumberFormat: developer.mozilla.org - MDN Docs —
Intl.DateTimeFormat: developer.mozilla.org - CLDR Repository — Locale Data: cldr.unicode.org
- VAT Regulations — Saudi Arabia (ZATCA): zatca.gov.sa
- VAT Regulations — UAE: tax.gov.ae
- ISO 4217 — Currency Codes: iso.org
Zy Yazan Platform © 2026
Localization Series
Financial Data Localization Guide — 4 Articles
Series: Financial Data Localization Guide — 4 Articles | Zyyazan Platform © 2026





