Wicked Smart Data
LearnArticlesAbout
Sign InSign Up
LearnArticlesAboutContact
Sign InSign Up
Wicked Smart Data

The go-to platform for professionals who want to master data, automation, and AI — from Excel fundamentals to cutting-edge machine learning.

Platform

  • Learning Paths
  • Articles
  • About
  • Contact

Connect

  • Contact Us
  • RSS Feed

© 2026 Wicked Smart Data. All rights reserved.

Privacy PolicyTerms of Service
All Articles
Localizing Canvas Apps for Global Teams: Multi-Language Support with Language(), JSON Tables, and Dynamic Label Switching

Localizing Canvas Apps for Global Teams: Multi-Language Support with Language(), JSON Tables, and Dynamic Label Switching

Power Apps⚡ Practitioner23 min readJul 25, 2026Updated Jul 25, 2026
Table of Contents
  • Introduction
  • Prerequisites
  • Understanding Language() and What It Actually Returns
  • Designing the Translation Table Structure
  • Building the JSON Translation Table
  • The Core Lookup Pattern
Wiring Up the Interface: A Complete Screen Example
  • Supporting Manual Language Override
  • Using SharePoint as the Translation Backend
  • Handling Date, Number, and Currency Formatting
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Performance Implications and When to Choose This Approach
  • Summary & Next Steps
  • Localizing Canvas Apps for Global Teams: Implementing Multi-Language Support with Language(), JSON Tables, and Dynamic Label Switching

    Introduction

    You've built a clean, functional Canvas App for your operations team. The workflows are solid, the data connections are reliable, and the UX actually makes sense. Then someone from the Barcelona office joins the rollout call and asks, in very polite but pointed terms, why the entire interface is in English. Three hours later, someone from the Tokyo team sends the same question in a Slack message. Suddenly "we'll add languages later" is a real project on your backlog.

    Localization in Canvas Apps isn't just about translating button labels. Done properly, it means your app detects the user's language automatically, pulls the correct string for every visible element, and does it without you maintaining seventeen separate screens or a rats' nest of nested If() statements. Done poorly, it becomes the kind of technical debt that haunts every future feature request. This lesson is about doing it properly.

    By the end of this article, you'll be able to build a fully localized Canvas App that detects language at runtime, stores all translatable strings in a maintainable JSON table, and dynamically switches every label, button, and placeholder in the app without reloading or duplicating screens. We'll build a realistic HR self-service app that supports English, Spanish, and Japanese — enough complexity to expose all the real challenges.

    What you'll learn:

    • How Language() works in Power Apps and what its output actually looks like
    • How to structure a JSON translation table and load it into a collection at app startup
    • How to build a LookUp() pattern that retrieves the correct string for any key and any language
    • How to handle fallback logic when a translation is missing
    • How to extend the pattern to support right-to-left languages, number formatting, and date display conventions

    Prerequisites

    This lesson assumes you're comfortable with Canvas App fundamentals: connecting data sources, writing formulas in the formula bar, and working with collections. You should know what ClearCollect(), LookUp(), and Filter() do. If you've worked through the earlier lessons in this learning path, you're ready. You don't need any prior experience with localization or internationalization concepts — we'll build that understanding as we go.


    Understanding Language() and What It Actually Returns

    Before you can build anything, you need to understand the raw material. The Language() function in Power Apps returns a string representing the user's language as configured in their Power Platform environment profile. It follows the IETF BCP 47 standard — the same one used by browsers, operating systems, and most modern APIs.

    The format looks like this:

    "en-US"     // English, United States
    "es-ES"     // Spanish, Spain
    "es-MX"     // Spanish, Mexico
    "ja-JP"     // Japanese, Japan
    "fr-FR"     // French, France
    "de-DE"     // German, Germany
    "pt-BR"     // Portuguese, Brazil
    "zh-Hans"   // Chinese, Simplified
    

    This is important to understand before you write a single formula. The tag has two parts: the language subtag (en, es, ja) and the region subtag (US, ES, JP). For most localization work, you'll work primarily with the language subtag, but you'll occasionally need the full tag when a single language has significant regional variation — Spanish in Spain versus Mexico, or Portuguese in Portugal versus Brazil.

    The function takes its value from the user's Power Platform profile language setting, not from their browser or operating system locale. This means a user who has configured their environment in Spanish will get "es-ES" regardless of what language their laptop is set to. That's actually a feature, not a limitation — it means your app respects the user's explicit professional language preference.

    To see what Language() returns for your current session, open any Canvas App, add a Label control, and set its Text property to Language(). You'll see the full tag rendered in the label. This is worth doing early in development so you know exactly what string you're working with.

    Now, here's the practical implication: you can't just match on Language() directly in most lookup patterns, because the full tag might not match what's in your translation table. If a Colombian user has their profile set to "es-CO" but your table only has "es-ES", they'd get no matches. The robust solution is to extract the primary language subtag using Left(Language(), 2), which strips the region qualifier and leaves you with just "en", "es", or "ja".

    Left(Language(), 2)
    // Returns "en" from "en-US"
    // Returns "es" from "es-MX"
    // Returns "ja" from "ja-JP"
    

    This two-character code becomes your primary lookup key throughout the app. Store it in a variable at startup so you're not recalculating it on every single formula evaluation:

    Set(varUserLanguage, Left(Language(), 2));
    

    You'll call this in App.OnStart, and every translation formula in the app will reference varUserLanguage instead of calling Language() directly.

    Performance note: Language() is a lightweight function and won't cause performance issues on its own. But wrapping it in Left() on dozens of LookUp() calls adds up. Storing the result in a variable is a clean habit regardless of scale.


    Designing the Translation Table Structure

    This is where most implementations go wrong. Developers reach for a SharePoint list, create columns like LabelName, English, Spanish, Japanese, and then realize that every time they add a language, they need to add a new column to their data source. That's a schema migration for every new language — not a maintenance strategy.

    The correct structure treats each translation as a row, not a column. You want a table that looks like this:

    key lang value
    lbl_welcome en Welcome to HR Portal
    lbl_welcome es Bienvenido al Portal de RRHH
    lbl_welcome ja HRポータルへようこそ
    btn_submit en Submit Request
    btn_submit es Enviar Solicitud
    btn_submit ja リクエストを送信
    lbl_vacationDays en Remaining Vacation Days
    lbl_vacationDays es Días de Vacaciones Restantes
    lbl_vacationDays ja 残りの有給休暇日数
    err_required en This field is required
    err_required es Este campo es obligatorio
    err_required ja このフィールドは必須です

    With this structure, adding a new language is purely a data operation — you add rows, not columns. Your app formulas don't change. Your lookup pattern doesn't change. This is the architecture you want.

    For a production app, you have two good options for where to store this table: a SharePoint list with exactly these three columns, or a JSON string embedded in the app itself. Each has tradeoffs.

    SharePoint list: Easy for non-developers to update translations without touching the app. Requires a network call at startup. Best when your translation team manages content independently.

    Embedded JSON: Zero latency, works offline, no connector required. Requires a developer to update the app when translations change. Best for apps where translation strings change infrequently and you want complete self-containment.

    We'll implement the JSON approach first because it teaches the underlying mechanics most clearly, then show you how to swap in the SharePoint approach with minimal formula changes.


    Building the JSON Translation Table

    Power Apps can parse a JSON string directly into a collection using ParseJSON(). This is your most self-contained option and the right choice for apps that need to work offline or where you want to eliminate external dependencies.

    In App.OnStart, you'll define your translation data as a JSON string and immediately parse it into a collection. Here's the full implementation for an HR self-service app:

    // App.OnStart
    
    // Step 1: Capture the user's language code
    Set(varUserLanguage, Left(Language(), 2));
    
    // Step 2: Define the translation table as JSON
    Set(varTranslationsJSON, "[
      {""key"":""app_title"", ""lang"":""en"", ""value"":""HR Self-Service Portal""},
      {""key"":""app_title"", ""lang"":""es"", ""value"":""Portal de Autoservicio de RRHH""},
      {""key"":""app_title"", ""lang"":""ja"", ""value"":""HRセルフサービスポータル""},
      {""key"":""nav_home"", ""lang"":""en"", ""value"":""Home""},
      {""key"":""nav_home"", ""lang"":""es"", ""value"":""Inicio""},
      {""key"":""nav_home"", ""lang"":""ja"", ""value"":""ホーム""},
      {""key"":""nav_timeoff"", ""lang"":""en"", ""value"":""Time Off""},
      {""key"":""nav_timeoff"", ""lang"":""es"", ""value"":""Tiempo Libre""},
      {""key"":""nav_timeoff"", ""lang"":""ja"", ""value"":""休暇申請""},
      {""key"":""nav_payslips"", ""lang"":""en"", ""value"":""Pay Slips""},
      {""key"":""nav_payslips"", ""lang"":""es"", ""value"":""Recibos de Pago""},
      {""key"":""nav_payslips"", ""lang"":""ja"", ""value"":""給与明細""},
      {""key"":""lbl_employee"", ""lang"":""en"", ""value"":""Employee Name""},
      {""key"":""lbl_employee"", ""lang"":""es"", ""value"":""Nombre del Empleado""},
      {""key"":""lbl_employee"", ""lang"":""ja"", ""value"":""従業員名""},
      {""key"":""lbl_department"", ""lang"":""en"", ""value"":""Department""},
      {""key"":""lbl_department"", ""lang"":""es"", ""value"":""Departamento""},
      {""key"":""lbl_department"", ""lang"":""ja"", ""value"":""部署""},
      {""key"":""lbl_startDate"", ""lang"":""en"", ""value"":""Start Date""},
      {""key"":""lbl_startDate"", ""lang"":""es"", ""value"":""Fecha de Inicio""},
      {""key"":""lbl_startDate"", ""lang"":""ja"", ""value"":""開始日""},
      {""key"":""lbl_endDate"", ""lang"":""en"", ""value"":""End Date""},
      {""key"":""lbl_endDate"", ""lang"":""es"", ""value"":""Fecha de Fin""},
      {""key"":""lbl_endDate"", ""lang"":""ja"", ""value"":""終了日""},
      {""key"":""lbl_reason"", ""lang"":""en"", ""value"":""Reason""},
      {""key"":""lbl_reason"", ""lang"":""es"", ""value"":""Motivo""},
      {""key"":""lbl_reason"", ""lang"":""ja"", ""value"":""理由""},
      {""key"":""btn_submit"", ""lang"":""en"", ""value"":""Submit Request""},
      {""key"":""btn_submit"", ""lang"":""es"", ""value"":""Enviar Solicitud""},
      {""key"":""btn_submit"", ""lang"":""ja"", ""value"":""申請を送信""},
      {""key"":""btn_cancel"", ""lang"":""en"", ""value"":""Cancel""},
      {""key"":""btn_cancel"", ""lang"":""es"", ""value"":""Cancelar""},
      {""key"":""btn_cancel"", ""lang"":""ja"", ""value"":""キャンセル""},
      {""key"":""err_required"", ""lang"":""en"", ""value"":""This field is required""},
      {""key"":""err_required"", ""lang"":""es"", ""value"":""Este campo es obligatorio""},
      {""key"":""err_required"", ""lang"":""ja"", ""value"":""このフィールドは必須です""},
      {""key"":""err_dateRange"", ""lang"":""en"", ""value"":""End date must be after start date""},
      {""key"":""err_dateRange"", ""lang"":""es"", ""value"":""La fecha de fin debe ser posterior a la de inicio""},
      {""key"":""err_dateRange"", ""lang"":""ja"", ""value"":""終了日は開始日より後でなければなりません""},
      {""key"":""msg_success"", ""lang"":""en"", ""value"":""Your request has been submitted successfully""},
      {""key"":""msg_success"", ""lang"":""es"", ""value"":""Su solicitud ha sido enviada exitosamente""},
      {""key"":""msg_success"", ""lang"":""ja"", ""value"":""リクエストが正常に送信されました""}
    ]");
    
    // Step 3: Parse JSON into a typed collection
    ClearCollect(
      colTranslations,
      ForAll(
        ParseJSON(varTranslationsJSON),
        {
          key: Text(ThisRecord.key),
          lang: Text(ThisRecord.lang),
          value: Text(ThisRecord.value)
        }
      )
    );
    

    Notice the double quotes ("") in the JSON string. Inside a Power Apps string literal, a double quote character must be escaped by doubling it. This trips up almost everyone the first time. If your varTranslationsJSON string looks syntactically wrong in the formula bar, this is almost certainly why.

    The ForAll() wrapper around ParseJSON() is necessary because ParseJSON() returns an untyped object, and LookUp() requires a typed table. The ForAll() with explicit Text() conversions produces a properly typed collection that your formulas can work with reliably.

    Important: ParseJSON() requires the Power Apps formula-level feature flag to be enabled in your environment settings. If you see a formula error referencing ParseJSON as an unknown function, go to Settings → Upcoming features → Formula-level features and enable it.


    The Core Lookup Pattern

    Now you have a collection. The next step is the formula you'll use in every single label, button text, and placeholder across the app. This is your translation function:

    LookUp(colTranslations, key = "lbl_employee" && lang = varUserLanguage, value)
    

    That's it. You're filtering the collection by both the key (which element you want) and the language (which translation you want), then returning the value field. This formula can go directly in the Text property of any Label control, the HintText of any Input control, or the Text property of a Button control.

    But this bare LookUp() has a problem: if the translation is missing — either because you forgot to add it or because the user's language isn't in your table — it returns Blank(). A blank label is an unhelpful and confusing UI. You need fallback logic.

    The professional pattern uses Coalesce() to fall back to English if the user's language translation is missing:

    Coalesce(
      LookUp(colTranslations, key = "lbl_employee" && lang = varUserLanguage, value),
      LookUp(colTranslations, key = "lbl_employee" && lang = "en", value),
      "lbl_employee"
    )
    

    This formula tries three things in order:

    1. Look for the translation in the user's language
    2. Fall back to English if that's missing
    3. Fall back to the raw key string if English is also missing

    The third fallback to the raw key string is a developer lifesaver. When a label shows lbl_someNewFeature in the UI, you immediately know that key exists in your app but hasn't been added to the translation table yet. It's a much better signal than a blank label.

    Writing this three-line Coalesce() on every single label gets tedious fast. The elegant solution is to wrap it in a named formula using the Named Formulas feature (enabled in Settings → Upcoming features → Formula-level features). However, since named formulas can't accept parameters in the current Canvas Apps implementation, the most practical approach for most apps is to create a helper label approach or to simply accept the three-line pattern on controls where fallback matters.

    For controls where you're confident the translation will always exist (your main navigation, primary buttons), you can use the shorter single-lookup form. For anything that might be added incrementally — error messages, tooltip text, dynamic content labels — always use the full Coalesce() pattern.


    Wiring Up the Interface: A Complete Screen Example

    Let's build the Time Off Request screen from our HR app with full localization. The screen has a header, a form with four fields, a submit button, a cancel button, and validation error messages. Here's how each element gets its translated text.

    Screen header label (Text property):

    LookUp(colTranslations, key = "nav_timeoff" && lang = varUserLanguage, value)
    

    Employee Name field label (Text property):

    Coalesce(
      LookUp(colTranslations, key = "lbl_employee" && lang = varUserLanguage, value),
      LookUp(colTranslations, key = "lbl_employee" && lang = "en", value),
      "lbl_employee"
    )
    

    Start Date input (HintText property):

    Coalesce(
      LookUp(colTranslations, key = "lbl_startDate" && lang = varUserLanguage, value),
      LookUp(colTranslations, key = "lbl_startDate" && lang = "en", value),
      "lbl_startDate"
    )
    

    Submit button (Text property):

    LookUp(colTranslations, key = "btn_submit" && lang = varUserLanguage, value)
    

    Validation error label (Text property, shown only when dates are invalid):

    Coalesce(
      LookUp(colTranslations, key = "err_dateRange" && lang = varUserLanguage, value),
      LookUp(colTranslations, key = "err_dateRange" && lang = "en", value),
      "Date range error"
    )
    

    The Visible property of that error label would be something like:

    And(
      !IsBlank(dtpStartDate.SelectedDate),
      !IsBlank(dtpEndDate.SelectedDate),
      dtpEndDate.SelectedDate <= dtpStartDate.SelectedDate
    )
    

    Notice that the localization logic is completely separate from the visibility logic. Don't mix them. Keep each control's localization in Text and its business logic in Visible, DisplayMode, and event handlers. Clean separation makes both easier to debug.


    Supporting Manual Language Override

    Language() gives you the user's profile language, which is correct behavior for most scenarios. But sometimes users want to temporarily switch the app language — for example, a bilingual manager who wants to see the Spanish version to verify it looks right for their team, or a trainer demonstrating the app in multiple languages.

    Supporting manual language switching requires only a small modification to your foundation. Instead of setting varUserLanguage once at startup from Language(), you set an initial value but allow it to be changed.

    Add a language selector control to your app — a Dropdown or a set of image buttons with flag icons. For a Dropdown, set its Items property to:

    ["Auto-detect", "English", "Español", "日本語"]
    

    Then add a second collection to App.OnStart that maps display names to language codes:

    ClearCollect(
      colLanguageMap,
      {display: "English", code: "en"},
      {display: "Español", code: "es"},
      {display: "日本語", code: "ja"}
    );
    
    // Set initial language from profile
    Set(varUserLanguage, Left(Language(), 2));
    

    When the user changes the dropdown selection, update varUserLanguage in the OnChange event:

    If(
      ddlLanguage.Selected.Value = "Auto-detect",
      Set(varUserLanguage, Left(Language(), 2)),
      Set(
        varUserLanguage,
        LookUp(colLanguageMap, display = ddlLanguage.Selected.Value, code)
      )
    )
    

    Because varUserLanguage is a global variable referenced in every translation formula, changing it triggers a recalculation across the entire app. Every label updates instantly. No screen navigation, no reload, no Navigate() calls — it just works, because Power Apps formula evaluation is reactive by design.

    Tip: If you add the language selector to a component and place it in your app header, it will appear on every screen automatically. This is the right UX pattern for language switching — always accessible, never intrusive.


    Using SharePoint as the Translation Backend

    For apps where translations need to be updated by a content team without touching the app itself, a SharePoint list is the right backend. The schema matches exactly what we designed: three columns — key, lang, and value — all single-line text.

    Replace the ClearCollect(colTranslations, ...) section of App.OnStart with:

    ClearCollect(
      colTranslations,
      Translations   // Your SharePoint list name
    );
    

    That's genuinely the only change. Every lookup formula in the app continues to reference colTranslations. The formulas don't know or care whether the collection was loaded from JSON or SharePoint.

    There's one important optimization: SharePoint lists have a default delegation limit of 500 rows (or 2000 with the setting adjusted). For a translation table with 50 keys and 5 languages, that's 250 rows — well within limits. For larger apps, you might approach that ceiling. If you have more than 400 rows in your translation table, set the data row limit to 2000 in App Settings and you'll have comfortable headroom for virtually any app.

    Startup performance: Loading a SharePoint list into a collection on App.OnStart adds latency to your startup time. For translation tables under 500 rows, this is typically 200–400ms on a good connection — barely noticeable. For very large translation sets, consider loading only the rows for the user's detected language using a Filter(): ClearCollect(colTranslations, Filter(Translations, lang = Left(Language(), 2) || lang = "en")). This cuts the loaded rows by a factor of your language count.


    Handling Date, Number, and Currency Formatting

    Translation isn't just about labels. The format of dates and numbers varies significantly across locales, and getting this wrong can cause real confusion — or in the case of financial data, real errors.

    Power Apps has a built-in Text() function with locale-aware formatting. When you provide a format string as the second argument, Power Apps interprets it in the context of the app's current language setting. However, since Canvas Apps uses the author's locale for format strings by default (not the user's locale), you need to be explicit.

    For dates, the safest pattern is to use Language() as the third argument to Text():

    Text(ThisItem.RequestDate, DateTimeFormat.ShortDate, Language())
    

    This renders the date in the short date format appropriate for the user's locale — 12/31/2025 for en-US, 31/12/2025 for es-ES, and 2025/12/31 for ja-JP.

    For numbers with decimal separators (which differ between US English and most European locales — a comma versus a period), use:

    Text(ThisItem.Salary, "#,##0.00", Language())
    

    For currency display, you'll want to combine a localized number format with a translated currency symbol from your translation table. Add currency keys to your translation table:

    {key: "fmt_currency", lang: "en", value: "$#,##0.00"}
    {key: "fmt_currency", lang: "es", value: "€#.##0,00"}
    {key: "fmt_currency", lang: "ja", value: "¥#,##0"}
    

    Then in your label:

    Text(
      ThisItem.Salary,
      LookUp(colTranslations, key = "fmt_currency" && lang = varUserLanguage, value)
    )
    

    This approach stores format strings as translation values, which is clever and maintainable. Your translation table handles both UI text and formatting conventions in a single consistent lookup pattern.


    Hands-On Exercise

    Build the Time Off Request screen of the HR Self-Service app from scratch. Here's exactly what to construct:

    Setup:

    1. Create a new Canvas App from blank, sized for tablet.
    2. In App.OnStart, implement the full ClearCollect(colTranslations, ...) formula from the JSON translation table section above.
    3. Set varUserLanguage to Left(Language(), 2) in App.OnStart.

    Screen construction: 4. Add a Rectangle at the top of the screen to serve as a header bar. Set its fill to a dark blue. 5. Add a Label inside the header. Set its Text to look up "nav_timeoff" from your translation collection. 6. Add a language Dropdown with items ["Auto-detect", "English", "Español", "日本語"]. Wire its OnChange to update varUserLanguage as described in the manual override section. 7. Add four form field labels for Employee Name, Department, Start Date, and End Date. Wire each Text property to the appropriate translation key. 8. Add a Text Input for the Reason field. Set its HintText to look up "lbl_reason". 9. Add a Submit button. Set its Text to look up "btn_submit". 10. Add a Cancel button. Set its Text to look up "btn_cancel". 11. Add a validation error label that displays the "err_required" message. Set its Visible to IsBlank(txtReason.Text) as a simple demonstration.

    Validation:

    • Change the language dropdown to "Español." Every label, button, and hint text on the screen should switch to Spanish instantly.
    • Change it to "日本語." Everything should switch to Japanese.
    • Change it back to "Auto-detect." It should return to whatever Language() returns for your profile.
    • Deliberately omit one translation (delete one row from colTranslations using Remove() in the formula bar — you can do this temporarily via an OnSelect formula on a test button). Observe that the missing label shows its key string as the fallback, rather than going blank.

    Stretch goal: Add a formatted date label that displays today's date in locale-appropriate format using Text(Today(), DateTimeFormat.ShortDate, Language()). Switch languages and observe how the date format changes.


    Common Mistakes & Troubleshooting

    Mistake 1: Using Language() directly in every formula instead of varUserLanguage

    This doesn't cause incorrect results, but it causes unnecessary recalculation overhead and makes the app slightly harder to reason about. Always cache it in a variable at startup.

    Mistake 2: JSON string with unescaped double quotes

    The most common cause of ParseJSON returning an error or Blank() is forgetting to double the quote characters inside a Power Apps string literal. Every " inside the JSON content must become "". If you're working with a long translation table, write your JSON in a proper editor first, then do a global find-and-replace of " with "" before pasting it into the formula bar.

    Mistake 3: Forgetting the ForAll(ParseJSON(...), {key: Text(...), lang: Text(...), value: Text(...)}) wrapper

    ParseJSON() returns an UntypedObject. You cannot call LookUp() on an UntypedObject — it expects a typed table. The ForAll() with explicit type conversions is not optional. If you skip it, you'll get a type mismatch error when you try to use LookUp().

    Mistake 4: Language codes with wrong length

    Some developers use Left(Language(), 3) accidentally, which returns "en-", "es-", "ja-" — the dash is included and your lookups silently fail. It should be Left(Language(), 2).

    Mistake 5: Not handling Blank() returns from LookUp()

    If a user's language is set to "fi" (Finnish) and your app has no Finnish translations, every bare LookUp() returns Blank(), and you get an interface full of empty labels. This looks broken and erodes trust in the app immediately. Always use the Coalesce() fallback pattern for any label that a user in an unsupported language might see.

    Mistake 6: Storing translations in a SharePoint list with extra columns

    Default SharePoint lists include system columns like Title, Modified, Created, and many more. When you ClearCollect() from a SharePoint list, all of these come along. They don't cause errors, but they bloat your collection. Use ClearCollect(colTranslations, ShowColumns(Translations, "key", "lang", "value")) to bring in only the three columns you need.

    Troubleshooting: My labels don't update when I change varUserLanguage

    First, verify that varUserLanguage is actually changing by adding a temporary label with Text set to varUserLanguage. If that label updates but your translation labels don't, check that those labels are referencing varUserLanguage in their formulas rather than having hardcoded language codes. If the temporary label also doesn't update, verify that the OnChange event on your dropdown is actually setting the variable — check for typos in the variable name.

    Troubleshooting: ParseJSON is undefined

    Navigate to Settings → Upcoming features → Formula-level features, find ParseJSON, and enable it. Save and republish the app. This feature flag must be enabled at the environment level and confirmed in the app settings.

    Troubleshooting: Japanese/Chinese characters display as boxes

    This is a font issue, not a translation issue. The default fonts in Canvas Apps support Latin characters but may not include full CJK glyph coverage on all platforms. Set your labels' Font property to "Segoe UI" or "Yu Gothic" for Japanese. Test on the actual target devices your Japanese users will be using, as the rendering environment matters more than the font setting in this case.


    Performance Implications and When to Choose This Approach

    The JSON collection approach has essentially zero query cost after App.OnStart completes — every lookup is an in-memory filter on a local collection, which is extremely fast. Even an app with 200 translation keys and 5 languages (1000 rows) will execute LookUp(colTranslations, ...) in single-digit milliseconds.

    The SharePoint approach adds a one-time startup cost. In exchange, you get the ability to update translations without redeploying the app. For most teams, this tradeoff is worth it.

    Where you should be thoughtful about performance is the Coalesce() pattern with two LookUp() calls. Each lookup scans the collection. For 100 controls all using the double-fallback pattern, that's up to 200 collection scans on every reactive recalculation. In practice this remains fast because Canvas Apps' formula engine is efficient and collections are in-memory, but if you're building an app with hundreds of visible controls that all need localization, profile your startup time before and after loading the translation collection.

    The alternative for very large apps is to pre-filter the collection to only the user's language at startup, then maintain an English fallback as a separate variable:

    ClearCollect(
      colMyLangTranslations,
      Filter(colTranslations, lang = varUserLanguage)
    );
    
    ClearCollect(
      colEnTranslations,
      Filter(colTranslations, lang = "en")
    );
    

    Your lookup then becomes:

    Coalesce(
      LookUp(colMyLangTranslations, key = "lbl_employee", value),
      LookUp(colEnTranslations, key = "lbl_employee", value)
    )
    

    Each lookup now scans a collection one-fifth the size (assuming five languages), which roughly quintuples lookup performance. For apps with a handful of languages and under 200 controls, you'll never notice the difference. For apps with many more controls and languages, this optimization matters.


    Summary & Next Steps

    You've built a complete, production-grade localization system for Canvas Apps. The architecture you now have — a row-per-translation table, a cached language variable, a consistent LookUp() pattern with Coalesce() fallback, and optional SharePoint-backed translation management — is the same pattern used in enterprise deployments supporting dozens of languages.

    The key principles to carry forward:

    • Language() returns a BCP 47 tag; extract the two-character subtag with Left(Language(), 2) and store it in a variable
    • Your translation table should be organized with one row per key-language pair, never one column per language
    • ParseJSON() with ForAll() is the right way to load embedded translations; ClearCollect() from SharePoint is right for externally-managed translations
    • Always implement Coalesce() fallback logic — your app will encounter languages you haven't translated, and graceful degradation to English is far better than blank labels
    • Date and number formatting is part of localization too; use Text(value, format, Language()) consistently

    Where to go from here:

    The natural next topic in localization is component-based UI architecture — building reusable components with localization built in so that every new screen inherits translation support automatically without re-implementing the lookup pattern. That's covered in the Advanced Canvas Apps Components lesson.

    You should also explore Power Automate integration for translation workflows — a flow that sends new translation keys to a review queue when developers add them to the app, notifying translators automatically. This closes the loop between developer activity and translation readiness, which is the operational challenge that always bites global teams in production.

    Finally, consider right-to-left (RTL) language support if your user base includes Arabic or Hebrew speakers. Canvas Apps doesn't have native RTL layout support, but there are workable patterns using mirrored layout containers and text alignment properties that can approximate RTL behavior — a topic worth a dedicated lesson of its own.

    Learning Path: Canvas Apps 101

    Previous

    Understanding Power Apps Licensing: Choosing the Right Plan for Your App and Users

    Related Articles

    Power Apps🌱 Foundation

    Understanding Power Apps Licensing: Choosing the Right Plan for Your App and Users

    18 min
    Power Apps🔥 Expert

    Canvas App Performance Profiling in Production: Monitor, Telemetry & Azure Application Insights

    29 min
    Power Apps⚡ Practitioner

    Canvas App Error Handling: Building Resilient Apps with IfError, Notify, and Graceful Failure Patterns*

    20 min

    On this page

    • Introduction
    • Prerequisites
    • Understanding Language() and What It Actually Returns
    • Designing the Translation Table Structure
    • Building the JSON Translation Table
    • The Core Lookup Pattern
    • Wiring Up the Interface: A Complete Screen Example
    • Supporting Manual Language Override
    • Using SharePoint as the Translation Backend
    • Handling Date, Number, and Currency Formatting
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Performance Implications and When to Choose This Approach
    • Summary & Next Steps