چرا تاریخها و اعداد نیاز به بومیسازی دارند؟

TABLE OF CONTENTS
اگر شما به صورت جهانی ارسال میکنید، یک رشته مشابه میتواند برای کاربران مختلف معانی متفاوتی داشته باشد: 12/01/2025
، 1.234
، 1,234
، 12:00
، 00:00
. این تفاوتهای کوچک تنها ظاهری نیستند—آنها بر اعتماد، درک و حتی رعایت قوانین تأثیر میگذارند. این راهنما توضیح میدهد که چرا تاریخها و اعداد باید بومیسازی شوند، چه چیزی واقعاً با توجه به محل تغییر میکند و چگونه به درستی در سراسر محصول خود پیادهسازی کنید.
چه چیزی با توجه به محل تغییر میکند
- تاریخها: ترتیب (MDY در مقابل DMY در مقابل YMD)، جداکنندهها، نامهای ماه/روز؛ برخی بازارها از تقویمهای غیرگرگوری استفاده میکنند.
- زمانها: ساعتهای ۱۲ ساعته در مقابل ۲۴ ساعته، نشانگرهای AM/PM، مناطق زمانی، قوانین صرفهجویی در زمان روز، شروع هفته (یکشنبه در مقابل دوشنبه).
- اعداد: جداکنندههای اعشاری (
.
در مقابل,
)، سبکهای گروهبندی (1,234
در مقابل1.234
در مقابل1 234
)، گروهبندی هندی (1,23,456
)، فضاهای غیرقابل شکستن، سبکهای منفی/مثبت (خط تیره در مقابل منفی واقعی−
). - ارز: نماد در مقابل کد ISO، جایگاه نماد (پیشوند/پسوند)، فاصله باریک در مقابل پهن، دقت اعشاری (۰، ۲، یا ۳+)، منفیهای حسابداری (مثلاً
(1 234,56 €)
). - درصد و واحدها: فاصله و جایگاه علامت (
50%
در مقابل50 %
)، نامهای محلی برای واحدها، متریک در مقابل امپریال.
تأثیر بر کسبوکار
- اعتماد و تبدیل: قیمتها، هزینهها و تاریخهایی که “اشتباه به نظر میرسند” خریدها را کاهش و نرخ ریزش را افزایش میدهند.
- ریسک عملیاتی: تاریخهای اشتباه خوانده شده میتوانند رزروها یا مهلتها را تغییر دهند؛ خطاهای تجزیه فاکتورها، صادرات و تحلیلها را خراب میکنند.
- رعایت قوانین: اسناد مالی با فرمتهای اشتباه میتوانند قوانین فاکتورسازی، مالیات یا گزارشدهی را نقض کنند.
- بار پشتیبانی: کاربران برای روشنسازی زمانها، ارزها و ورودیهای عددی که با انتظارات مطابقت ندارند، تیکت باز میکنند.
حادثه واقعی
یک پلتفرم رزرو سفر اروپایی تاریخهای خروج را به صورت 01/03/2024
بدون زمینه محلی نمایش داد. مشتریان آمریکایی این را به عنوان “۳ ژانویه” تفسیر کردند در حالی که مشتریان بریتانیا و اروپا آن را “۱ مارس” خواندند.
تأثیر:
- ۱۲٪ از رزروهای بینالمللی برای تاریخهای اشتباه انجام شدهاند
- خدمات مشتری بیش از ۳۴۰۰ درخواست توضیح در یک هفته دریافت کرده است
- ۲.۳ میلیون دلار در بازپرداخت و هزینههای رزرو مجدد
- ۸٪ از مشتریان تحت تأثیر به رقبا روی آوردهاند
علت اصلی: فرمت DD/MM/YYYY
به صورت سختکد شده در تأییدیههای ایمیل استفاده شده بود در حالی که وبسایت برای کاربران ایالات متحده از MM/DD/YYYY
استفاده میکرد. رفع این مشکل تنها ۳ ساعت زمان مهندسی نیاز داشت—ابهام ۸۰۰ برابر بیشتر از پیشگیری هزینه داشت.
درس: تاریخها بدون زمینه یک بمب ساعتی برای هر محصول جهانی هستند.
اشتباهات رایج
- فرمتهای سختکد شده:
MM/DD/YYYY
یا1,234.56
که در رابط کاربری، ایمیلها، PDFها یا خروجیهای CSV تعبیه شدهاند. - ذخیره رشتههای محلیسازی شده: ذخیره “Dec 1, 2025” به جای زمان ISO باعث مشکلات تجزیه و تغییر منطقه زمانی میشود.
- تجزیه سادهلوحانه: استفاده از پیشفرضهای محلی سرور؛ فرض کردن جداکنندهها یا ساعتهای ۱۲/۲۴ ساعته.
- الحاق رشته: ساخت “مقدار + ارز” مانند
"$" + 1234.56
به جای قالببندی آگاه به محلی. - ابهام منطقه زمانی: نمایش زمانهای محلی بدون مناطق صریح برای اقدامات حساس به زمان.
- ناسازگاری مرورگر: Safari، Firefox و Chrome ممکن است تاریخها/اعداد را بهطور متفاوتی قالببندی کنند؛ همیشه روی همه مرورگرهای هدف تست کنید.
- شکافهای رندر سمت سرور: Node.js بدون داده کامل ICU (
node --with-intl=full-icu
) قالببندی ناقص یا نادرست تولید میکند. - تلههای عملکرد: ایجاد نمونههای جدید
Intl.*
در حلقهها یا چرخههای رندر به جای ذخیرهسازی قالببندها.
بهترین شیوهها
- مقادیر استاندارد را ذخیره کنید: تاریخها/زمانها بهصورت ISO‑8601 در UTC؛ پول بهصورت واحدهای کوچک (سنت) یا اعشار با دقت بالا.
- نمایش بر اساس محل کاربر: از APIهای آگاه به محل (مانند
Intl.DateTimeFormat
,Intl.NumberFormat
) برای نمایش استفاده کنید. - اعتبارسنجی و تجزیه بر اساس محل: ورودی محل را در صورت لزوم بپذیرید؛ جایگزینها/نمونهها را برای راهنمایی کاربران نشان دهید.
- با زمان صریح باشید: اختصارات یا جابجاییهای منطقه زمانی را نشان دهید؛ اجازه انتخاب کاربر را بدهید در جایی که نتایج به زمان وابسته است.
- وضوح ارز: در جایی که ابهام وجود دارد، کدهای ISO را ترجیح دهید؛ محل قرارگیری و فاصلهگذاری نماد را بر اساس محل رعایت کنید.
- قالببندی را متمرکز کنید: یک لایه ابزاری برای تمام سطوح (رابط کاربری، ایمیلها، PDFها، صادرات) برای اطمینان از یکنواختی.
- قالببندها را کش کنید: فراخوانی سازنده
Intl.*
هزینهبر است؛ یک بار ایجاد کنید و نمونهها را بر اساس محل دوباره استفاده کنید.
زمانی که نباید محلیسازی کرد
همه چیز نباید محلیسازی شود. در اینجا سناریوهایی وجود دارد که فرمتهای ثابت و قابل خواندن توسط ماشین بهتر هستند:
- صادرات قابل خواندن توسط ماشین: فایلهای CSV که توسط خطوط داده یا ابزارهای تحلیلی مصرف میشوند باید از یک فرمت ثابت استفاده کنند (مثلاً تاریخهای ISO-8601، نقطه به عنوان جداکننده اعشار). این فرمت را به وضوح در هدرهای صادرات یا فایلهای README مستند کنید.
- پاسخهای API: APIهای REST/GraphQL باید تاریخها را بهصورت رشتههای ISO-8601 و اعداد را در فرمت استاندارد JSON بازگردانند. اجازه دهید مشتریان محلیسازی را بر اساس ترجیحات کاربر خود مدیریت کنند.
- لاگها و معیارهای داخلی: لاگها، داشبوردهای نظارتی و پرسوجوهای پایگاه داده از فرمتهای ثابت برای تجزیه، تجمیع و هشدار بهره میبرند. از ISO-8601 و فرمتهای عددی استاندارد استفاده کنید.
- شناسههای استاندارد: شناسههای تراکنش، شمارههای سفارش یا ارجاعاتی که کاربران ممکن است نیاز به ارتباط در سراسر محلها داشته باشند باید از فرمتبندی خاص محل اجتناب کنند.
قانون کلی: برای انسانها که محتوا را میخوانند محلیسازی کنید؛ از فرمتهای استاندارد برای ماشینهایی که دادهها را پردازش میکنند استفاده کنید.
برنامه پیادهسازی
- سطوح حسابرسی: تاریخها، زمانها، اعداد، ارز در رابط کاربری، ایمیلها، PDFها، خروجیهای CSV/Excel، تحلیلها، لاگها.
- تعریف محلیها: فهرست محلیهای پشتیبانیشده و بازگشتهای پیشفرض؛ سیاست ۱۲/۲۴ ساعته را برای هر محلی مشخص کنید.
- ساخت ابزارها: APIهای
Intl.*
(یا یک کتابخانه) را با کمکهای مشترک و تستها بپیچید؛ کش کردن فرمتکننده را پیادهسازی کنید. - استراتژی ورودی: فرمها را بهروزرسانی کنید تا فرمتهای محلی را بپذیرند و اعتبارسنجی کنند؛ مقادیر اصلی را در کنار ورودی خام ذخیره کنید.
- قوانین محتوا: سبک تاریخهای کوتاه/بلند، تاریخهای نسبی، نمایش ارز و فرمتبندی درصد را مستند کنید.
- اجرا: ابتدا سطوح با ترافیک و ریسک بالا را تبدیل کنید؛ در صورت نیاز پشت یک ویژگی پرچم ارسال کنید.
استراتژی تست
فراتر از کنترل کیفیت دستی، تستهای خودکار را برای جلوگیری از بازگشتها پیادهسازی کنید:
- تستهای عکسبرداری: خروجیهای فرمتشده را برای هر محلی پشتیبانیشده ثابت کنید. تغییرات باعث بازبینی میشوند تا اطمینان حاصل شود که عمدی هستند.
- تستهای رفتوبرگشت: اطمینان حاصل کنید که
format → parse → format
نتایج پایداری تولید میکند. از دست دادن دقت یا فرمتهای مبهم را میگیرد. - موارد حاشیهای: اعداد منفی، صفر، اعداد بسیار بزرگ (میلیاردها/تریلیونها)، انتقالهای DST، سالهای کبیسه و تاریخهای مرزی (۱ ژانویه، ۳۱ دسامبر) را تست کنید.
- اعتبارسنجی متقابل مرورگر: تستهای خودکار بر روی Chrome، Firefox، Safari و Edge برای گرفتن تفاوتهای رندرینگ.
- بازگشت محلی: اطمینان حاصل کنید که در صورت درخواست محلیهای پشتیبانینشده، کاهش با وقار انجام میشود.
- معیارهای عملکرد: هزینه ایجاد فرمتکننده را اندازهگیری کنید؛ اطمینان حاصل کنید که کش کردن در تولید کار میکند.
نکات عملکرد
APIهای Intl.*
قدرتمند هستند اما در صورت استفاده نادرست میتوانند کند باشند. از این دستورالعملها پیروی کنید:
// ❌ بد: فرمتکننده جدید در هر فراخوانی ایجاد میکند
function formatPrice(amount, locale) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(amount);
}
```js
// ✅ خوب: فرمتکنندهها را برای هر locale کش کنید
const formatters = new Map();
function getFormatter(locale) {
if (!formatters.has(locale)) {
formatters.set(locale, new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}));
}
return formatters.get(locale);
}
function formatPrice(amount, locale) {
return getFormatter(locale).format(amount);
}
- کش کردن نمونههای فرمتکننده: ساخت
Intl.*
هزینهبر است (~1–5ms)؛ فرمت کردن با یک نمونه موجود سریع است (~0.01ms). - اجتناب از حلقهها: هرگز فرمتکنندهها را داخل
map()
,forEach()
, یا حلقههای رندر ایجاد نکنید. - بارگذاری تنبل دادههای locale: برای اپلیکیشنهای وب، به تقسیم کد برای بارگذاری فقط localeهای مورد نیاز فکر کنید تا اندازه بسته کاهش یابد.
- سمت سرور: از یک فرمتکننده برای هر چرخه درخواست/پاسخ استفاده کنید؛ از ایجاد فرمتکننده برای هر آیتم در مجموعه دادههای بزرگ اجتناب کنید.
چکلیست QA
- تاریخها در
en-US
,en-GB
,de-DE
,fr-FR
,hi-IN
,ja-JP
,ar-EG
بدون ابهام هستند. - زمان از قرارداد صحیح 12/24 ساعته استفاده میکند؛ نشانگرهای AM/PM در جایی که انتظار میرود ظاهر میشوند؛ مناطق زمانی برای جریانهای حساس به زمان نشان داده میشوند.
- اعداد از جداکنندههای صحیح اعشاری و گروهبندی استفاده میکنند؛ فضاهای غیرقابلشکست در جایی که استاندارد است (مثلاً
fr-FR
). - ارز نماد/کد صحیح، جایگذاری و دقت اعشاری را برای هر locale نشان میدهد؛ منفیها به درستی نمایش داده میشوند.
- ورودیها دادههای locale کاربر را میپذیرند و تجزیه میکنند (یا به وضوح یک فرمت پشتیبانیشده را اعمال میکنند)؛ پیامهای اعتبارسنجی به locale آگاه هستند.
- صادرات (CSV/PDF) با انتظارات locale مطابقت دارند یا فرمتهای ثابت برای مصرف ماشینی را به وضوح مستند میکنند.
- مرورگرهای متقابل: تاریخها، زمانها و اعداد به طور مداوم در Chrome, Firefox, Safari و Edge رندر میشوند.
- عملکرد: هیچ فرمتکنندهای در حلقهها ایجاد نمیشود؛ نمونههای کششده در رندرها مجدداً استفاده میشوند.
مثالهای کد
جاوااسکریپت
// تاریخها
const date = new Date('2025-01-12T00:00:00Z');
console.log(new Intl.DateTimeFormat('en-US', { dateStyle: 'short', timeZone: 'UTC' }).format(date));
// → 1/12/25
console.log(new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeZone: 'UTC' }).format(date));
// → 12/01/2025 (day/month/year)
console.log(new Intl.DateTimeFormat('ja-JP', { dateStyle: 'medium', timeZone: 'UTC' }).format(date));
// → 2025/01/12
// زمانها با نمایش منطقه زمانی
const time = new Date('2025-01-12T18:05:00Z');
console.log(new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'America/New_York',
timeZoneName: 'short'
}).format(time));
// → 1/12/25, 1:05 PM EST
console.log(new Intl.DateTimeFormat('de-DE', {
timeStyle: 'short',
timeZone: 'Europe/Berlin',
hour12: false
}).format(time));
// → 19:05
// اعداد
const n = 1234.56;
console.log(new Intl.NumberFormat('en-US').format(n)); // → 1,234.56
console.log(new Intl.NumberFormat('de-DE').format(n)); // → 1.234,56
console.log(new Intl.NumberFormat('fr-FR').format(n)); // → 1 234,56 (NBSP به عنوان جداکننده گروه)
// شمارهگذاری هندی
console.log(new Intl.NumberFormat('hi-IN').format(1234567.89)); // → 12,34,567.89
// ارز
console.log(new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n));
// → $1,234.56
console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n));
// → 1.234,56 €
console.log(new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(1234));
// → ¥1,234 (بدون اعشار)
// درصد و واحدها
console.log(new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 0 }).format(0.5));
// → 50%
console.log(new Intl.NumberFormat('fr-FR', { style: 'percent', maximumFractionDigits: 0 }).format(0.5));
// → 50 %
// مثال ذخیرهسازی قالبساز
class LocaleFormatter {
constructor() {
this.cache = new Map();
}
getDateFormatter(locale, options) {
const key = `date:${locale}:${JSON.stringify(options)}`;
if (!this.cache.has(key)) {
this.cache.set(key, new Intl.DateTimeFormat(locale, options));
}
return this.cache.get(key);
}
formatDate(date, locale, options = { dateStyle: 'medium' }) {
return this.getDateFormatter(locale, options).format(date);
}
}
const formatter = new LocaleFormatter();
console.log(formatter.formatDate(new Date(), 'en-US')); // → Jan 12, 2025
console.log(formatter.formatDate(new Date(), 'fr-FR')); // → 12 janv. 2025
Python
from babel.dates import format_date, format_time, format_datetime
from babel.numbers import format_number, format_currency, format_percent
from datetime import datetime
import pytz
# Dates
date = datetime(2025, 1, 12)
print(format_date(date, format='short', locale='en_US')) # → 1/12/25
print(format_date(date, format='short', locale='en_GB')) # → 12/01/2025
print(format_date(date, format='medium', locale='ja_JP')) # → 2025/01/12
# Times with timezone
tz_ny = pytz.timezone('America/New_York')
tz_berlin = pytz.timezone('Europe/Berlin')
time = datetime(2025, 1, 12, 18, 5, tzinfo=pytz.UTC)
print(format_datetime(time.astimezone(tz_ny), 'short', tzinfo=tz_ny, locale='en_US'))
# → 1/12/25, 1:05 PM
print(format_time(time.astimezone(tz_berlin), format='short', tzinfo=tz_berlin, locale='de_DE'))
# → 19:05
# Numbers
n = 1234.56
print(format_number(n, locale='en_US')) # → 1,234.56
print(format_number(n, locale='de_DE')) # → 1.234,56
print(format_number(n, locale='fr_FR')) # → 1 234,56
# Indian numbering
print(format_number(1234567.89, locale='hi_IN')) # → 12,34,567.89
# Currency
print(format_currency(n, 'USD', locale='en_US')) # → $1,234.56
print(format_currency(n, 'EUR', locale='de_DE')) # → 1.234,56 €
print(format_currency(1234, 'JPY', locale='ja_JP')) # → ¥1,234
# Percent
print(format_percent(0.5, locale='en_US')) # → 50%
print(format_percent(0.5, locale='fr_FR')) # → 50 %
Java
import java.text.NumberFormat;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
import java.util.Currency;
// تاریخها
ZonedDateTime date = ZonedDateTime.of(2025, 1, 12, 0, 0, 0, 0, ZoneId.of("UTC"));
DateTimeFormatter usFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Locale.US);
System.out.println(usFormatter.format(date)); // → 1/12/25
DateTimeFormatter gbFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Locale.UK);
System.out.println(gbFormatter.format(date)); // → 12/01/2025
DateTimeFormatter jpFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.JAPAN);
System.out.println(jpFormatter.format(date)); // → 2025/01/12
// زمانها
ZonedDateTime time = ZonedDateTime.of(2025, 1, 12, 18, 5, 0, 0, ZoneId.of("UTC"));
ZonedDateTime timeNY = time.withZoneSameInstant(ZoneId.of("America/New_York"));
DateTimeFormatter usTimeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.US);
System.out.println(usTimeFormatter.format(timeNY)); // → 1:05 PM
// اعداد
double n = 1234.56;
NumberFormat usFormat = NumberFormat.getInstance(Locale.US);
System.out.println(usFormat.format(n)); // → 1,234.56
NumberFormat deFormat = NumberFormat.getInstance(Locale.GERMANY);
System.out.println(deFormat.format(n)); // → 1.234,56
NumberFormat frFormat = NumberFormat.getInstance(Locale.FRANCE);
System.out.println(frFormat.format(n)); // → 1 234,56
// ارز
NumberFormat usCurrency = NumberFormat.getCurrencyInstance(Locale.US);
usCurrency.setCurrency(Currency.getInstance("USD"));
System.out.println(usCurrency.format(n)); // → $1,234.56
NumberFormat deCurrency = NumberFormat.getCurrencyInstance(Locale.GERMANY);
deCurrency.setCurrency(Currency.getInstance("EUR"));
System.out.println(deCurrency.format(n)); // → 1.234,56 €
NumberFormat jpCurrency = NumberFormat.getCurrencyInstance(Locale.JAPAN);
jpCurrency.setCurrency(Currency.getInstance("JPY"));
System.out.println(jpCurrency.format(1234)); // → ¥1,234
Go
package main
import (
"fmt"
"time"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/number"
)
func main() {
// Numbers
n := 1234.56
pUS := message.NewPrinter(language.AmericanEnglish)
fmt.Println(pUS.Sprintf("%.2f", n)) // → 1,234.56
pDE := message.NewPrinter(language.German)
fmt.Println(pDE.Sprintf("%.2f", n)) // → 1.234,56
pFR := message.NewPrinter(language.French)
fmt.Println(pFR.Sprintf("%.2f", n)) // → 1 234,56
// Currency (using number package)
fmt.Println(pUS.Sprint(number.Decimal(n, number.Scale(2)))) // → 1,234.56
// Dates - Go's time package uses layouts instead of locale formatting
// For full i18n date formatting, use github.com/goodsign/monday or similar
date := time.Date(2025, 1, 12, 0, 0, 0, 0, time.UTC)
fmt.Println(date.Format("01/02/2006")) // US: 01/12/2025
fmt.Println(date.Format("02/01/2006")) // EU: 12/01/2025
fmt.Println(date.Format("2006/01/02")) // ISO: 2025/01/12
}
توجه: کتابخانه استاندارد Go پشتیبانی محدودی از محلیسازی دارد. برای استفاده در تولید، در نظر بگیرید:
golang.org/x/text
برای قالببندی اعدادgithub.com/goodsign/monday
برای قالببندی تاریخ/زمان محلیشدهgithub.com/bojanz/currency
برای مدیریت ارز
منابع مفید
- استانداردها: Unicode CLDR (استاندارد دادههای محلی)، IETF BCP 47 (شناسههای محلی)
- مستندات: MDN Intl Reference
- کتابخانهها:
- Luxon (کتابخانه مدرن تاریخ/زمان با i18n)
- date-fns با ماژولهای محلی
- Globalize.js (i18n جامع بر اساس CLDR)
- Format.js (i18n متمرکز بر React با نحو پیام ICU)
- آزمایش: Intl polyfill برای مرورگرهای قدیمیتر
پایان
درست کردن تاریخها و اعداد یک پیروزی با تلاش کم و تاثیر بالا است: اعتماد بیشتر، خطاهای کمتر، و تجربه جهانی روانتر. قالببندی را متمرکز کنید، مقادیر اصلی را ذخیره کنید، از APIهای آگاه به محلی در هر جایی که کاربران شما زمان، پول، یا اعداد را میخوانند یا تایپ میکنند استفاده کنید—و بدانید که چه زمانی باید از محلیسازی برای دادههای قابل خواندن توسط ماشین صرف نظر کنید. با ذخیرهسازی مناسب و آزمایش، شما سیستمی مقاوم خواهید ساخت که بدون قربانی کردن عملکرد، در مقیاسهای محلی گسترش مییابد.