Consider this scenario: You're a person from the US, staying in Canada, and looking to book a hotel room in Jaipur, India from my Travel site. You're looking at a card like this:
Jaipur grand hotel --- 04/05 -> 04/07 --- $1,12,107.43 --- फ्री कैंसिलेशन"ok, why is 2 days of stay showing up as a million Dollars?"
"what does it say in Hindi?"
"wait, that's not a million, why are the commas all wrong?"
"is the rate in Canadian Dollars or US Dollars?"
"aaah I'm going to book from a different website!"
If my site happened to have supported good localization (l10n), you would've switched to the exact format that you'd understand, and seen this card instead:
Jaipur grand hotel --- 05/04 -> 07/04 --- CAD 112,107.43 --- Free cancellationYou'd realise that you had accidentally chosen 2 months of stay instead of 2 days, that the currency was in Canadian Dollars, and that it said "Free cancellation". And perhaps would've continued booking a room on my Travel OTA[1].
This is a small glimpse into why supporting l10n is super important if you want to take your product global. i18n or Internationalization is the codebase architecture set up part of supporting l10n.
Step 0
Never hardcode user-facing strings. Always pass them through a localization function.
An important note is to also never force preferences onto a user. For example, a person located in Japan could be a tourist and not be able to understand Japanese text. Or an Indian located in India may not be able to understand Hindi. Always give the user an option to change their interface localisation settings, and make the setting obvious and accessible.
Translation
This one starts easy. A bunch of JSON files per key, categorised and boxed into directories based on function, think Listing.Cancellation.Free_Cancellation, with sub-keys being languages and then the values.
One thing to note here is that you should probably not load all the translations into memory, as this is going to end up being very very fat as you scale.
| Locale | Listing.Cancellation.Free_Cancellation |
|---|---|
| en-US | Free Cancellation |
| en-IN | Free Cancellation |
| hi-IN | फ्री कैंसिलेशन |
| ko-KR | 예약 무료 취소 |
Conditionals
Complexity starts when you realise languages have their own rules for plurals, grammar, etc. Apart from not making the silly mistake of adding an "s" at the end, a way to solve this is to have conditions in your translations. This way you can abstract away conditional complexity into your translation function instead of muddying your code.
Think something like this:
// this is a terrible idea
const label = `Book ${roomCount} room${roomCount === 1 ? "" : "s"}`;
// try something like
<Translate id="Booking.Confirmation.Book_rooms" count={roomCount} />;{
"Booking.Confirmation.Book_rooms": [
{
"condition": "count === 1",
"value": [
{ "lang": "en-US", "value": "Book {count} room." },
{ "lang": "ja-JP", "value": "部屋を{count}部屋予約する。" }
]
},
{
"condition": "count > 1",
"value": [
{ "lang": "en-US", "value": "Book all {count} rooms." },
{ "lang": "ja-JP", "value": "{count}部屋予約する。" }
]
}
]
}The conditions can also be for other booleans and enums.
Layout
Then come text length and layout considerations. You'd ideally want to let your components grow with padding and margins instead of tight fixed widths.
| Locale | Translation |
|---|---|
| ko-KR | 123 조회 |
| en-US | 123 views |
| it-IT | 123 visualizzazioni |
Similarly, with truncation, you should consider if the text is important information or not, and let it perhaps wrap to the next line gracefully.
RTL (Arabic, Hebrew) is a whole different beast that I'm going to skip at the moment.
CJK Zenkaku
When Keyboard layout is set to these, afaik the characters that are output use "Zenkaku" or extra-width, such that the individual characters are as wide as a typical Lating letter.
The problem is when a CJK user types in English in the middle, all your validation is going to fail!
| Type | Narrow / halfwidth | Fullwidth |
|---|---|---|
| Letters and digits | ABC123 | ABC123 |
| Katakana | カタカナ | カタカナ |
Numbers
Numbers of any kind must always pass through a formatter function.
Numbers can be monetary values, distances, ratings, percentages.
Example keys in your config to save: measurementSystem, numberSeparator.decimals, currency.decimalPlaces, currency.symbol.domestic.
Separator
Comma separators are different in different countries, and yes they're not always commas!
Many countries group digits in threes, some European ones swap commas with dots, and India has its own lakh and crore system.
| Locale | Separation |
|---|---|
| en-US | 1,234,567.89 |
| de-DE | 1.234.567,89 |
| en-IN | 12,34,567.89 |
| fr-FR | 1 234 567,89 |
Money
Money notation differs by placement of currency symbols, space between symbol and amount, local-global symbols, and fraction amount or the lack of.
Some currencies have a local version - Japanese use "円" locally, while yen is denoted by "¥" internationally. Also, many countries using dollars use "$" locally, but for an international traveler it's important to know which dollar they are looking at - USD, SGD, HKD, etc.
The way to sort this out is to typically show the local currency symbol if a combination of market, language, and currency matches a default.
It's also important to let users choose their currency regardless of where they are located. For example, Indians would generally prefer to read a number in a 2,2,3.2 format.
// assume standard value in db is USD.
const systemAmount = 123456.78;
const formattedAmount = l10nMoney(systemAmount, locale, currency);| Locale | Currency | Display |
|---|---|---|
| en-US | USD | $123,456.78 |
| en-IN | INR | ₹1,16,65,492.87 |
| de-DE | EUR | 106.300,61 € |
| fr-FR | EUR | 106 300,61 € |
| en-JP | JPY | ¥19,253,084 |
| ja-JP | JPY | 19,253,084円 |
| vi-VN | VND | 3.217.282.465 ₫ |
Dates and times
Back to our 2 months vs 2 days confusion. Some countries use DD/MM, some MM/DD. Also, some use an AM/PM format, while others a 24-hr one.
It's generally a good idea to also denote what timezone (IST, JST, PST) a time is in, so that it's super clear (think: should check-in time be user's timezone or hotel's timezone?).
For hours some prefer HH, some H, etc etc. There's also meridians: some countries have their own version of "AM" and "PM" and their placements!
| Locale | Display |
|---|---|
| en-US | 05/04, 03:30 PM |
| en-GB | 04/05, 15:30 |
| ja-JP | 05/04 15:30 |
| vi-VN | 04/05, CH 3:30 |
Countries also have their own first day of the week, what's considered a weekend, and what have you.
Names and Addresses
Japan, China, and few other Asian countries follow a lastName, firstName system, while rest of the world follows the opposite.
Also addresses and the order in which country, zip or postcode, street, state or "prefecture" are presented varies too!
| Country | Postal Field | Example |
|---|---|---|
| USA | ZIP code | 02108 |
| UK | Postcode | SW1A 1AA |
| India | PIN code | 560001 |
These are even more important when setting up your input forms - think a numeric regex validator for postcodes. Would fail for the UK.
| Country | State Equivalent |
|---|---|
| USA | State |
| Canada | Province |
| Japan | Prefecture |
| Russia | Oblast |
| UAE | Emirate |
These are from a codebase perspective. I should perhaps write a bit about how to set your architecture up to be performant with allll the permutations and combinations. Non-technical stuff is not covered here (think legal requirements like GDPR and other EU laws).
Hope this was helpful. If you know of a unique format that I've missed here, let me know!
- [1]
OTA: Online Travel Agency. A web based platform to book a hotel room / flight / taxi etc.
↩