Back to Blog
AngularJuly 4, 202610 min read

Bilingual Apps in Canada: Technical and UX Challenges

DR

Doug Ribeiro

10 min read

I worked remotely from Montreal for a Toronto-based bank, where bilingualism was not a feature request. It was a baseline requirement. On that work, every screen had to hold up equally well in English and French, not just in translated labels, but in legal compliance, formatting, accessibility, and UX quality. The French-speaking experience cannot be a second-class citizen of the same codebase.

Most i18n tutorials cover the mechanics of translation files. This post covers what those tutorials do not: the edge cases, the UX decisions that differ between languages, the legal requirements specific to bilingual Canadian applications, and the Angular implementation patterns that have worked for me in production.

The ngx-translate Setup That Actually Scales

ngx-translate is the translation library I reach for in Angular, combined with lazy-loaded translation files per feature module. Loading all translations upfront in a monolithic JSON file works for small applications but becomes a performance problem as the translation file grows. In a large banking application with hundreds of screens, the translation file can grow to several hundred kilobytes.

My pattern is to split translations by feature module and load them alongside the module's lazy-loaded code. Each feature registers its own translation namespace with a resolver on its root route, so the router holds the navigation until the strings arrive and nobody sees raw keys. Do not reach for APP_INITIALIZER here: app initializers run once at bootstrap, so one declared in a lazy route's providers never fires. The root translations cover shared elements like navigation, common button labels, and global error messages. Feature translations cover domain-specific content.

typescript
// Route resolver: the router waits for it before the feature renders
export function featureTranslations(feature: string): ResolveFn<boolean> {
  return () => {
    const translate = inject(TranslateService);
    const http = inject(HttpClient);
    return forkJoin([
      http.get<Record<string, unknown>>(`/assets/i18n/${feature}/en.json`),
      http.get<Record<string, unknown>>(`/assets/i18n/${feature}/fr.json`),
    ]).pipe(
      tap(([en, fr]) => {
        translate.setTranslation('en', en, true); // merge: true
        translate.setTranslation('fr', fr, true);
      }),
      map(() => true)
    );
  };
}

// accounts.routes.ts (lazy loaded via loadChildren)
export const ACCOUNTS_ROUTES: Routes = [
  {
    path: '',
    resolve: { i18n: featureTranslations('accounts') },
    children: [
      // feature routes
    ],
  },
];

// Translation key convention: feature.section.element
// accounts.dashboard.welcomeMessage
// accounts.transactions.emptyState

Date and Number Formatting: Not Just a Locale Setting

Canadian English and Canadian French agree on numeric dates and split on long ones. Both en-CA and fr-CA carry a short pattern of y-MM-dd in the CLDR data Angular bundles, so DatePipe renders 2025-07-13 in both languages. The long form is where they diverge: July 13, 2025 in English, 13 juillet 2025 in French. That makes the locale data the safe path and hand-rolled formatting the dangerous one. The moment someone writes their own MM/DD helper, or prints a US-style short date straight off an API response, 07/06/2025 reads as July 6 to a reader expecting MM/DD and as June 7 to a reader expecting DD/MM. On a loan maturity date or a payment due date, that misreading has real financial consequences. Run every date through DatePipe with the registered locale and the ambiguity never gets a chance to appear.

Angular's built-in DatePipe handles locale-aware formatting correctly when configured properly, but it requires that you register the locale data explicitly:

typescript
// main.ts
import { registerLocaleData } from '@angular/common';
import localeFrCA from '@angular/common/locales/fr-CA';
import localeEnCA from '@angular/common/locales/en-CA';

registerLocaleData(localeFrCA, 'fr-CA');
registerLocaleData(localeEnCA, 'en-CA');

// In your app config. LOCALE_ID is resolved once, when the injector creates it,
// so this sets the startup locale; it does not follow a runtime language switch
{
  provide: LOCALE_ID,
  useFactory: (languageService: LanguageService) => languageService.currentLocaleId(),
  deps: [LanguageService],
}

// Usage in templates. Pass the current locale explicitly so a language switch
// re-formats without a reload (currentLocale tracks LanguageService)
// {{ transactionDate | date:'longDate':undefined:currentLocale }} → July 13, 2025 (en-CA)
// {{ transactionDate | date:'longDate':undefined:currentLocale }} → 13 juillet 2025 (fr-CA)

// Currency formatting
// {{ amount | currency:'CAD':'symbol-narrow':'1.2-2':currentLocale }}
// en-CA: $1,234.56 | fr-CA: 1 234,56 $

Note the currency symbol position difference. In Canadian French, the currency symbol goes after the number with a space. This is not just stylistic. It is the established convention that French-speaking users expect. Getting it wrong reads as amateur to a bilingual user.

Legal Compliance for Bilingual Interfaces in Canada

Canadian federal institutions are subject to the Official Languages Act, which requires that services be available in both official languages of equal quality. Banks are not federal institutions, but the Use of French in Federally Regulated Private Businesses Act (passed in 2023, not yet in force) will give consumers in Quebec and in regions with a strong francophone presence the right to be served in French by federally regulated businesses, banks included. For digital products, this has specific implications that go beyond having a language toggle.

Every user-facing communication must be available in both languages simultaneously or on demand. This includes: error messages (including server-side errors), legal disclosures and terms, notification emails, PDF documents generated by the application, and accessibility announcements made to screen readers. It is not sufficient to translate the UI if the PDF generated by the UI is only in English.

In practice, bilingual content has to be maintained for every content type a flow touches. The rule I recommend, and this is my recommendation rather than a description of any employer's process: content is not done until both language versions are complete, enforced at the workflow level, not just as a convention. On the front end, request content from the API with a language parameter and expect a complete response in the requested language.

Text Expansion: The Hidden Layout Problem

French text runs longer than its English equivalent: around 20% to 30% on running text, and often much more on short labels. This is a well-documented i18n challenge, but its practical implications in financial UI design are underappreciated. A label that reads 'Account balance' in English becomes 'Solde du compte' in French. Similar length in this case. But 'View transaction details' becomes 'Afficher les détails de la transaction', which is almost 60% longer. In a compact dashboard with tight button spacing, that length difference causes layout problems.

EnglishSubmit → 6 charsView details → 12 charsAccount settings → 16 charsTransaction history → 19 charsForgot your password? → 21 charsNext payment due → 16 charsCanadian FrenchSoumettre → 9 chars (+50%)Afficher les détails → 20 chars…Paramètres du compte → 20 chars…Historique des transactions → 2…Mot de passe oublié? → 20 chars…Prochain paiement dû → 20 chars…VS

My design approach for Canadian bilingual applications is to design in French first when working on text-heavy components. If the layout works in French, it almost always works in English. The reverse is not true. An English-optimized layout will frequently break under French text.

Practically, I use CSS techniques that handle text expansion gracefully: flex-based layouts with overflow ellipsis for truncation where appropriate, min-width constraints on button elements instead of fixed widths, and container queries on card components to reflow content when the text-to-container ratio becomes unfavorable.

Idiomatic Differences Beyond Word-for-Word Translation

Translation is not word-for-word replacement. Financial French in Canada has specific terminology conventions that differ from both general French and European French. I do not make those calls from my own French. I check the Office québécois de la langue française. Its Vitrine linguistique carries compte chèques as the Canadian term for a chequing account, while compte courant reads as European French to a Quebec user. Using the wrong regional term signals to that user that the interface was not written for them.

This is why I put financial translation passes with a professional translator who specializes in Canadian French, followed by a review from a native Quebec French speaker. That is the chain I recommend, not an account of how any employer runs it. Automated translation, even high-quality machine translation, consistently produces European French idioms for financial terminology. In a Quebec regulatory context, that is not acceptable.

The tone and formality conventions also differ. Canadian English financial interfaces use relatively casual, direct language, 'You owe' rather than 'Your outstanding balance is'. Canadian French financial interfaces run more formal, and a phrasing like 'Le montant dû est' sits closer to the register the OQLF's finance terminology uses than a literal translation of the casual English does. That is a call for your translator and the OQLF's fiches, not for the front-end developer. It is a content strategy decision that affects the entire interface.

Language Toggle UX: Persistent, Predictable, Accessible

The language toggle itself is a UX design decision that carries more implications than it appears to. The conventions worth following:

  • The language toggle should always display the language you will switch TO, not the language you are currently in. If the interface is in English, the toggle says 'Français'. If it is in French, it says 'English'. This is the Canadian government standard and the convention bilingual users expect.
  • Language preference should persist across sessions when the user is authenticated. Storing it in a user profile server-side rather than browser storage ensures consistency across devices.
  • Language changes should take effect immediately without a full page reload wherever possible. In Angular, this means the TranslateService change propagates reactively through the component tree without navigation.
  • The language toggle must be reachable by keyboard and must be announced correctly by screen readers. It is not a button that performs an action in the current page. It changes the entire interface language, which is a significant enough state change to warrant an aria-live announcement.
  • Place the language toggle consistently in a predictable location. In Canadian government-inspired designs, the top-right corner of the header is the established convention. Do not move it to the footer or bury it in a settings menu.

Handling Dynamic Content and User-Generated Content

Static UI labels are the easy part. The harder problem is user-generated content and dynamic data. If a user enters their address in French, does the English version of the interface display it correctly? If a financial advisor writes a note in English, and a French-speaking client reads it through the French interface, what happens?

There is no clean technical solution to this. The honest answer is that user-generated content is language-neutral. It is stored and displayed as entered, without translation. What you can control is the surrounding UI elements and the labeling of that content. You can also, in appropriate contexts, provide machine translation as an assistive feature with a clear indication that it is machine-translated and not official.

For system-generated content like application status messages, automated notifications, and validation errors, the content should always be available in both languages. A pattern that works: store system content such as status codes and document requirements with both an English and a French description, and let the display layer select the language based on the current user preference. This requires the back-end data model to be designed with bilingualism in mind from the start, not retrofitted after the fact.

Planning for RTL: Future-Proofing Without Overbuilding

Canada does not have RTL language requirements in the same way that applications targeting Arabic or Hebrew markets do. But if you are building a component library for a large financial institution that operates internationally, make RTL-compatible decisions from the start rather than retrofitting them later.

The practical implication for EN/FR Canadian apps is to use logical CSS properties (margin-inline-start instead of margin-left, padding-inline-end instead of padding-right) in shared component styles, and to ensure that the dir attribute is set correctly on the html element when language changes. These habits cost very little to establish and provide meaningful optionality for future internationalization.

Bilingualism in Canadian software is not a feature. It is the minimum viable product for serving the country's population.

Building genuinely bilingual applications, not just translated ones, requires treating EN and FR as co-equal design concerns from the first day of a project. The technical implementation is straightforward once the design and content strategy foundations are correct. The hard work is organizational: ensuring that every team member, from the product owner to the backend developer, understands that French is not an afterthought.

References

i18nCanadaAngularUX/UIngx-translate