Building Bilingual Apps for Canada: French Accents, UTF-8 and Bill 96
Quick answer
Use UTF-8 everywhere and add a byte order mark to CSV exports so Excel shows é correctly. Normalize text to NFC, sort with Intl.Collator('fr-CA'), and format money and dates with Intl for fr-CA (1 234,56 $). Strip accents from slugs and validate postal codes with the real letter rules. Quebec's Bill 96 is why many teams are adding French now.
Quebec's Bill 96 amended the Charter of the French Language, and its business requirements have been fully in force since June 2025. Many Canadian teams are now shipping French versions of sites, apps and automated emails that used to be English only. Translation is the visible part of the work. The bugs tend to come from the parts nobody scoped: Montréal turning into Montréal in a CSV export, "École" sorting after "Zoo", two identical-looking strings that don't compare equal, and prices formatted the anglophone way on the French site. This guide covers the technical side.
This is an engineering guide, not legal advice. For what Bill 96 requires of your business, see the Office québécois de la langue française (OQLF) and your counsel.
1. Mojibake: why Montréal becomes Montréal
In UTF-8, é is stored as two bytes: C3 A9. If software reads those bytes as
Windows-1252 or Latin-1, which is still a common default in older tools, each byte becomes its own
character. C3 becomes à and A9 becomes ©:
"Montréal" → UTF-8 bytes 4D 6F 6E 74 72 C3 A9 61 6C read as Latin-1 → "Montréal"
The usual trigger is a CSV export that someone double-clicks in Excel. Excel on Windows often doesn't
assume a CSV is UTF-8 unless the file starts with a byte order mark (BOM,
EF BB BF). You can fix it at either end:
- Exporting: write a UTF-8 BOM before the header row. In Node,
res.write('\uFEFF' + csv). In Python, open the file withencoding="utf-8-sig". - Opening: use Excel's Data → From Text/CSV and choose 65001: Unicode (UTF-8) instead of double-clicking.
- Everywhere else: declare
<meta charset="UTF-8">, sendContent-Type: text/csv; charset=utf-8, and make sure your database and connection use UTF-8. In MySQL that'sutf8mb4, not the older three-byteutf8.
If you see é, è or ç in your data, the bytes are almost
always intact UTF-8 that was decoded with the wrong encoding. Check the tool that read the data before
you "fix" the data. The Unicode Converter shows the code
points behind a string, so you can see what's really stored.
2. Two ways to write é
Unicode can represent é as one precomposed code point (U+00E9) or as
e followed by a combining acute accent (U+0301). They look identical on screen
but are different strings:
const a = 'Qu\u00e9bec'; // precomposed é
const b = 'Que\u0301bec'; // e + combining accent
a === b; // false
a.length, b.length; // 6, 7
a.normalize('NFC') === b.normalize('NFC'); // true
Decomposed text comes from copy-pasting out of some PDFs, from file names created on macOS's older HFS+ filesystem, and from some input methods. The symptoms: a search for "Québec" misses records, a unique constraint lets two "identical" usernames through, and a length check counts one character too many. Normalize to NFC when text enters your system (form input, imports, API payloads) and you avoid the whole class of bug.
3. Sorting: École should not come after Zoo
JavaScript's default sort() compares UTF-16 code units, and every accented capital sorts
after Z. Uppercase also sorts before lowercase. Use a locale-aware collator instead:
const words = ['Zoo', 'École', 'eau', 'Île', 'abri'];
words.sort();
// ['Zoo', 'abri', 'eau', 'École', 'Île'] ✗
words.sort(new Intl.Collator('fr-CA').compare);
// ['abri', 'eau', 'École', 'Île', 'Zoo'] ✓
Do the same in the database. Use a proper Unicode collation (for example an ICU or
utf8mb4_unicode_ci-style collation) for columns that users sort by name or city.
4. Numbers, money and dates in fr-CA
Canadian French formats numbers differently from Canadian English. Let Intl handle it
instead of concatenating strings:
new Intl.NumberFormat('en-CA', { style: 'currency', currency: 'CAD' }).format(1234.56);
// "$1,234.56"
new Intl.NumberFormat('fr-CA', { style: 'currency', currency: 'CAD' }).format(1234.56);
// "1 234,56 $"
new Intl.DateTimeFormat('fr-CA', { dateStyle: 'long' }).format(date);
// "18 septembre 2026"
The French version uses a comma as the decimal separator, puts the dollar sign after the amount, and uses a no-break space (not a regular space) between thousands. That last detail causes bugs: code that parses user input by stripping regular spaces won't strip the no-break space, and tests that compare against a string typed with a normal space will fail. Keep numbers numeric in your data and format them only for display.
5. URLs and slugs with accents
You have two options for French URLs. You can keep the accents, in which case browsers display
/fr/montréal but send the percent-encoded form /fr/montr%C3%A9al, or you can
strip them to plain ASCII. Stripping is the more robust choice for slugs. The
Slug Generator decomposes accented letters and removes the
accents, so "Crème brûlée à Montréal" becomes creme-brulee-a-montreal.
One catch: the ligature œ (as in cœur or œuvre) isn't an accented letter,
so decomposition leaves it alone and a strict ASCII filter deletes it. "Le cœur de Québec" becomes
le-cur-de-quebec. If your content uses œ or æ, map them to
oe and ae before generating the slug. The
URL Encoder shows the exact bytes a browser will send for any
accented path.
6. Validating Canadian postal codes
Canadian postal codes follow the pattern A1A 1A1, but not every letter is allowed. The
letters D, F, I, O, Q and U never appear, because they're easily misread. W and Z are also never used as
the first letter. A stricter pattern catches more typos:
/^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d$/i K1A 0B1 ✓ H2X1Y4 ✓ V6B 4Y8 ✓ D1A 1A1 ✗ K1A 0D1 ✗ W1A 1A1 ✗
Accept input with or without the space and in either case, then store it in one canonical form (uppercase with a single space). A regex only checks the format. It can't tell you whether the code exists, so use Canada Post's data for that. You can test the pattern against your own samples in the Regex Tester. For more patterns, see the regex cheat sheet.
7. Tell the browser which language it's reading
- Set
<html lang="fr-CA">on French pages andlang="en-CA"on English ones. Screen readers use it to pick a voice, and browsers use it for hyphenation and spell-check. - Link the two versions with
<link rel="alternate" hreflang="fr-CA">andhreflang="en-CA"so search engines show the right one. - Leave room in layouts: French text often runs noticeably longer than English. Buttons with fixed widths and one-line headings are usually the first things to break.
- Check automated emails, PDFs, error messages and
alttext, not just page content. These are easy to miss. - If your app sends notifications with times, Canada's time zones follow their own rules: Saskatchewan mostly doesn't change its clocks, and Yukon has stayed on UTC−7 year-round since 2020. Our daylight saving time guide covers the details.
Useful ToolzYard tools
Conclusion
Going bilingual is mostly an encoding and locale project. Use UTF-8 everywhere, and add a BOM on CSV
exports people will open in Excel. Normalize text to NFC when it enters the system. Sort with a
fr-CA collator and format numbers and dates with Intl. Handle
œ in slugs, validate postal codes with the real letter rules, and mark every page with its
language. Do that and the French version will behave as well as the English one.
Frequently Asked Questions
Why do French accents show up as é in my CSV file?
The file is UTF-8, but the program that opened it read it as Windows-1252 or Latin-1, so the two bytes of é were shown as two characters. Add a UTF-8 byte order mark when exporting, or import the file in Excel with Data → From Text/CSV and choose UTF-8.
What is Unicode normalization and when do I need it?
Some accented letters can be stored either as one precomposed character or as a base letter plus a combining accent. Normalizing to NFC converts both to the same form, so comparisons, searches and uniqueness checks work. Normalize text when it enters your system.
How do I sort French words correctly in JavaScript?
Use a locale-aware collator such as new Intl.Collator('fr-CA').compare instead of the default sort, which compares code units and puts accented capitals like É after Z.
What is a valid Canadian postal code format?
Letter, digit, letter, space, digit, letter, digit, for example K1A 0B1. The letters D, F, I, O, Q and U are never used, and W and Z never appear as the first letter. A regex checks format only, not whether the code exists.
Should French URLs keep their accents?
Either works, but accented URLs are sent percent-encoded (é becomes %C3%A9), which can be awkward in logs, emails and analytics. Plain ASCII slugs with accents removed are usually the more robust choice.