Generate
Back to Blog
World map with localized profile data samples for different countries and regions

A SaaS company with servers in Virginia launches its product in the United States. Every test passes. The registration form works. The profile page renders. The invoicing system generates correct documents. Six months later, the company expands to Germany. Within the first week, the support queue fills with tickets: a customer named Muller sees "M?ller" on their invoice because the PDF generator doesn't handle umlauts. A customer in Munich enters a postal code with a leading zero, and the system stores it as a four-digit number because the database column is an integer rather than a string. A customer in Stuttgart expects the price to read "49,99 EUR" but instead sees "$49.99" because the currency-formatting logic was hardcoded for US conventions.

None of these bugs appeared during testing. They couldn't, because the test data was entirely American. US names, US addresses, US postal codes, US phone formats, US currency. The test suite proved that the application worked for Americans. It proved nothing about anyone else, and the company is now discovering that fact through customer complaints rather than through test failures. The pattern is common enough that internationalisation engineers have a shorthand for it: "works in one country." The fix isn't to write more tests. It's to write tests against different data.

A Taxonomy of Localisation Failures

Character encoding failures are the most visible. ASCII covers the 26 letters of the English alphabet, digits, and common punctuation. It can't represent an umlaut (u), a cedilla (c), an accent grave (e), or any non-Latin character. UTF-8 handles all of these, but applications often have points in the pipeline where UTF-8 data is passed through an ASCII-only process: a PDF library, an email template engine, a CSV export, a legacy API endpoint, or a database column defined as LATIN1 instead of UTF8MB4. Any of these chokepoints will corrupt characters that fall outside the ASCII range.

The corruption patterns are distinctive. A replacement character (?) appears when the system can't represent the byte. Mojibake (garbled characters like "Müller" instead of "Muller") appears when UTF-8 bytes are interpreted as Latin-1. Silent truncation occurs when a multibyte character is split across a fixed-width buffer. Each pattern points to a different failure in the encoding pipeline, and each requires a different fix. Testing with ASCII-only data exercises none of these code paths. The irony is that UTF-8 support is usually straightforward to implement. What's missing isn't engineering capability. It's test data that would have revealed the problem before deployment.

Date and time formatting varies across regions in ways that cause silent data errors rather than visible crashes. "01/02/2024" is January 2nd in the US and February 1st in the UK and most of Europe. An application that parses date input without explicitly specifying the locale will get the wrong date for roughly half its international users and never throw an error about it. The date parses correctly. It just parses to the wrong day.

Number formatting is equally treacherous. The US uses a period as the decimal separator and a comma as the thousands separator: 1,234.56. Germany uses a comma as the decimal separator and a period (or a thin space) as the thousands separator: 1.234,56. An input field that accepts "1.234" will store 1234 in a US-locale parser and 1.234 in a German-locale parser. A financial application that gets this wrong misplaces the decimal point by a factor of a thousand.

Currency and Payment-Method Localisation

Currency formatting goes beyond swapping the dollar sign for a euro sign. The placement of the currency symbol varies: "$49.99" in the US, "49,99 EUR" in Germany (symbol after the number), "49,99 CHF" in Switzerland. Japan uses no decimal places for yen (4,999 JPY, not 49.99 JPY). Kuwait uses three decimal places for the dinar (49.990 KWD). An application that hardcodes two decimal places for all currencies produces incorrect output for both.

Payment methods also vary by market. Credit cards dominate in the US and UK. iDEAL is the most common online payment method in the Netherlands. Bancontact is standard in Belgium. Giropay and Sofort were common in Germany (now largely replaced by bank transfers). A checkout flow that only offers Visa and Mastercard will have a significantly higher abandonment rate in markets where those aren't the preferred payment methods.

Tax display rules differ too. In the US, prices are typically displayed before tax, with tax added at checkout. In the EU, prices must include VAT. In Australia, GST is included in the displayed price. An e-commerce application that shows pre-tax prices to European customers isn't just confusing, it's potentially violating consumer-protection regulations. The test data needs to include transactions in multiple currencies and tax regimes to catch these inconsistencies.

Right-to-Left Script Support

Arabic, Hebrew, Farsi, and Urdu are written right to left. Supporting these scripts isn't just a matter of translating the text and calling it done. The entire layout needs to mirror: navigation that sits on the left moves to the right, text alignment flips, progress bars fill from right to left, and mixed-direction content (a paragraph in Arabic containing an English brand name) needs bidirectional text handling.

CSS has supported RTL layouts through the "direction" property for decades, and modern frameworks offer RTL-aware utility classes. But testing requires actually rendering the page with RTL content and checking that the layout makes sense. A form label that sits to the left of its input field in LTR mode should sit to the right in RTL mode. A "next" button with a right-pointing arrow should have a left-pointing arrow in RTL mode. A date picker that advances forward by clicking the right arrow should advance forward by clicking the left arrow.

The bugs here are almost always visual. The application doesn't crash. It just looks wrong, and "wrong" in this case means unusable for the user. A right-to-left user looking at a left-to-right layout is reading a page that's structured backwards from their perspective. Important information is in the wrong corner. The reading flow contradicts the layout flow. These aren't cosmetic issues. They're usability failures that make the application feel hostile to the user. The developer who never tested with RTL content has no way of knowing these bugs exist until an Arabic-speaking customer reports them, by which point the application has been live in that market for weeks.

Name-Order Conventions

In Japan, South Korea, China, and Hungary, the family name comes first. Tanaka Yuki in Japanese puts the family name (Tanaka) before the given name (Yuki). An application that assumes the first name field contains the given name and the last name field contains the family name will address a Japanese customer incorrectly in emails, sort them wrong in alphabetical lists, and display their name backwards on every profile page and invoice.

Some cultures use mononyms (a single name with no family name). Some use patronymic naming systems where the "last name" changes by generation (Icelandic naming, where Bjork Gudmundsdottir is the daughter of Gudmundur, not a member of the Gudmundsdottir family). Some names contain particles that affect sorting: "van der Berg" in Dutch is sorted under B, not V. "de Souza" in Portuguese is sorted under S, not D.

Testing with "Jane Doe" and "John Smith" exercises exactly zero of these conventions. The test data needs to include names from cultures that use different ordering, mononyms, patronymics, and particles. The assertions need to verify that display, sorting, and search all handle these conventions correctly. A fixture set generated from a single locale can't do this.

Generators like Faker support multiple locales for name generation, but the locale-specific rules (name order, particle handling, sorting conventions) need to be implemented in the application, not in the generator. The generator's role is to supply names that exercise the application's handling of these rules. Tools like Another.IO produce full profiles tied to specific countries, so a Japanese profile comes with a family-name-first convention, a German profile includes compound surnames with hyphens, and a Brazilian profile includes multi-part names with "de" and "da" particles.

Building a Multi-Country Test Matrix

The test matrix should include at least one country from each of the following categories: Western European (France or Germany, for accented characters, comma-decimal separators, and GDPR implications), East Asian (Japan or South Korea, for non-Latin scripts and family-name-first ordering), right-to-left (Saudi Arabia or Israel, for layout mirroring and bidirectional text), South American (Brazil, for long compound names and different date formats), and a country with unusual postal code formatting (UK, with its alphanumeric postcodes of variable length, or Canada with alternating letters and digits).

Each country in the matrix should have at least twenty fixture records to provide statistical coverage. A single German record might not trigger a truncation bug if the name happens to be short. Twenty German records, with names ranging from four to forty characters, will exercise the field-length boundaries. The fixture set should include names at the extremes of what's common in each culture, not just the median-length names that don't stress the layout.

The fixture records should be internally consistent: a German name with a German address, a German phone number, and a German postal code. A record that mixes a Japanese name with a Brazilian address and a UK phone number doesn't test real-world usage because no actual user produces that combination. Internal consistency ensures that the test exercises the same code paths that production traffic will follow. This seems obvious when stated explicitly, but the default in most test suites is the opposite: random fields pulled from whichever locale the generator defaults to, assembled without regard for whether the combination makes sense as a real person's record.

Localisation vs. Internationalisation

Internationalisation (i18n) is the engineering work that makes localisation possible. It's the extraction of hardcoded strings into translation files, the use of locale-aware date and number formatters, the implementation of bidirectional text support, and the use of Unicode throughout the data pipeline. This work needs to happen once, in the codebase, and it's invisible to the end user when done correctly.

Localisation (l10n) is the market-specific work that uses the i18n infrastructure. It's the actual translations, the currency-display rules for each market, the address-format templates for each country, and the cultural adaptations (colour associations, icon conventions, imagery). This work happens per-market, and it's visible to the end user as the application feeling "native" in their locale.

The distinction matters for testing because i18n bugs and l10n bugs have different symptoms. An i18n bug is structural: the PDF library doesn't support UTF-8, so no locale with non-ASCII characters works. An l10n bug is content-specific: the German translation uses the formal "Sie" where the brand voice requires the informal "du," but the French translation is fine. I18n bugs are caught by testing with any non-ASCII locale. L10n bugs require testing each locale individually.

The cost of localisation bugs scales with the size of the user base in the affected market. A character-encoding bug that corrupts German names affects every German user from the moment of launch. Fixing the bug, deploying the fix, notifying affected users, and correcting corrupted records takes engineering time and erodes user trust. Finding the bug during development, before a single German user has registered, costs one test run and one code change. The economics aren't subtle.