1.. role:: html(code) 2 :language: html 3 4.. role:: js(code) 5 :language: javascript 6 7============================= 8Fluent for Firefox Developers 9============================= 10 11 12This tutorial is intended for Firefox engineers already familiar with the previous 13localization systems offered by Gecko - `DTD`_ and `StringBundle`_ - and assumes 14prior experience with those systems. 15 16 17Using Fluent in Gecko 18===================== 19 20`Fluent`_ is a modern localization system introduced into 21the Gecko platform with a focus on quality, performance, maintenance and completeness. 22 23The legacy DTD system is deprecated, and Fluent should be used where possible. 24 25Getting a Review 26---------------- 27 28If you work on any patch that touches FTL files, you'll need to get a review 29from `fluent-reviewers`__. There's a Herald hook that automatically sets 30that group as a blocking reviewer. 31 32__ https://phabricator.services.mozilla.com/tag/fluent-reviewers/ 33 34Guidelines for the review process are available `here`__. 35 36__ ./fluent_review.html 37 38Major Benefits 39============== 40 41Fluent `ties tightly`__ into the domain of internationalization 42through `Unicode`_, `CLDR`_ and `ICU`_. 43 44__ https://github.com/projectfluent/fluent/wiki/Fluent-and-Standards 45 46More specifically, the most observable benefits for each group of consumers are 47 48 49Developers 50---------- 51 52 - Support for XUL, XHTML, HTML, Web Components, React, JS, Python and Rust 53 - Strings are available in a single, unified localization context available for both DOM and runtime code 54 - Full internationalization (i18n) support: date and time formatting, number formatting, plurals, genders etc. 55 - Strong focus on `declarative API via DOM attributes`__ 56 - Extensible with custom formatters, Mozilla-specific APIs etc. 57 - `Separation of concerns`__: localization details, and the added complexity of some languages, don't leak onto the source code and are no concern for developers 58 - Compound messages link a single translation unit to a single UI element 59 - `DOM Overlays`__ allow for localization of DOM fragments 60 - Simplified build system model 61 - No need for pre-processing instructions 62 - Support for pseudolocalization 63 64__ https://github.com/projectfluent/fluent/wiki/Get-Started 65__ https://github.com/projectfluent/fluent/wiki/Design-Principles 66__ https://github.com/projectfluent/fluent.js/wiki/DOM-Overlays 67 68 69Product Quality 70------------------ 71 72 - A robust, multilevel, `error fallback system`__ prevents XML errors and runtime errors 73 - Simplified l10n API reduces the amount of l10n specific code and resulting bugs 74 - Runtime localization allows for dynamic language changes and updates over-the-air 75 - DOM Overlays increase localization security 76 77__ https://github.com/projectfluent/fluent/wiki/Error-Handling 78 79 80Fluent Translation List - FTL 81============================= 82 83Fluent introduces a file format designed specifically for easy readability 84and the localization features offered by the system. 85 86At first glance the format is a simple key-value store. It may look like this: 87 88.. code-block:: fluent 89 90 home-page-header = Home Page 91 92 # The label of a button opening a new tab 93 new-tab-open = Open New Tab 94 95But the FTL file format is significantly more powerful and the additional features 96quickly add up. In order to familiarize yourself with the basic features, 97consider reading through the `Fluent Syntax Guide`_ to understand 98a more complex example like: 99 100.. code-block:: fluent 101 102 ### These messages correspond to security and privacy user interface. 103 ### 104 ### Please choose simple and non-threatening language when localizing 105 ### to help user feel in control when interacting with the UI. 106 107 ## General Section 108 109 -brand-short-name = Firefox 110 .gender = masculine 111 112 pref-pane = 113 .title = 114 { PLATFORM() -> 115 [windows] Options 116 *[other] Preferences 117 } 118 .accesskey = C 119 120 # Variables: 121 # $tabCount (Number) - number of container tabs to be closed 122 containers-disable-alert-ok-button = 123 { $tabCount -> 124 [one] Close { $tabCount } Container Tab 125 *[other] Close { $tabCount } Container Tabs 126 } 127 128 update-application-info = 129 You are using { -brand-short-name } Version: { $version }. 130 Please read the <a>privacy policy</a>. 131 132The above, of course, is a particular selection of complex strings intended to exemplify 133the new features and concepts introduced by Fluent. 134 135.. important:: 136 137 While in Fluent it’s possible to use both lowercase and uppercase characters in message 138 identifiers, the naming convention in Gecko is to use lowercase and hyphens, avoiding 139 CamelCase and underscores. For example, `allow-button` should be preferred to 140 `allow_button` or `allowButton`, unless there are technically constraints – like 141 identifiers generated at run-time from external sources – that make this impractical. 142 143In order to ensure the quality of the output, a lot of checks and tooling 144is part of the build system. 145`Pontoon`_, the main localization tool used to translate Firefox, also supports 146Fluent and its features to help localizers in their work. 147 148 149.. _fluent-tutorial-social-contract: 150 151Social Contract 152=============== 153 154Fluent uses the concept of a `social contract` between developer and localizers. 155This contract is established by the selection of a unique identifier, called :js:`l10n-id`, 156which carries a promise of being used in a particular place to carry a particular meaning. 157 158The use of unique identifiers is shared with legacy localization systems in 159Firefox. 160 161.. important:: 162 163 An important part of the contract is that the developer commits to treat the 164 localization output as `opaque`. That means that no concatenations, replacements 165 or splitting should happen after the translation is completed to generate the 166 desired output. 167 168In return, localizers enter the social contract by promising to provide an accurate 169and clean translation of the messages that match the request. 170 171In Fluent, the developer is not to be bothered with inner logic and complexity that the 172localization will use to construct the response. Whether `declensions`__ or other 173variant selection techniques are used is up to a localizer and their particular translation. 174From the developer perspective, Fluent returns a final string to be presented to 175the user, with no l10n logic required in the running code. 176 177__ https://en.wikipedia.org/wiki/Declension 178 179 180Markup Localization 181=================== 182 183To localize an element in Fluent, the developer adds a new message to 184an FTL file and then has to associate an :js:`l10n-id` with the element 185by defining a :js:`data-l10n-id` attribute: 186 187.. code-block:: html 188 189 <h1 data-l10n-id="home-page-header" /> 190 191 <button data-l10n-id="pref-pane" /> 192 193Fluent will take care of the rest, populating the element with the message value 194in its content and all localizable attributes if defined. 195 196The developer provides only a single message to localize the whole element, 197including the value and selected attributes. 198 199The value can be a whole fragment of DOM: 200 201.. code-block:: html 202 203 <p data-l10n-id="update-application-info" data-l10n-args='{"version": "60.0"}'> 204 <a data-l10n-name="privacy-url" href="http://www.mozilla.org/privacy" /> 205 </p> 206 207.. code-block:: fluent 208 209 -brand-short-name = Firefox 210 update-application-info = 211 You are using { -brand-short-name } Version: { $version }. 212 Please read the <a data-l10n-name="privacy-url">privacy policy</a>. 213 214 215Fluent will overlay the translation onto the source fragment preserving attributes like 216:code:`class` and :code:`href` from the source and adding translations for the elements 217inside. The resulting localized content will look like this: 218 219.. code-block:: html 220 221 <p data-l10n-id="update-application-info" data-l10n-args='{"version": "60.0"}'"> 222 You are using Firefox Version: 60.0. 223 Please read the <a href="http://www.mozilla.org/privacy">privacy policy</a>. 224 </p> 225 226 227This operation is sanitized, and Fluent takes care of selecting which elements and 228attributes can be safely provided by the localization. 229The list of allowed elements and attributes is `maintained by the W3C`__, and if 230the developer needs to allow for localization of additional attributes, they can 231allow them using :code:`data-l10n-attrs` list: 232 233.. code-block:: html 234 235 <label data-l10n-id="search-input" data-l10n-attrs="style" /> 236 237The above example adds an attribute :code:`style` to be allowed on this 238particular :code:`label` element. 239 240 241External Arguments 242------------------ 243 244Notice in the previous example the attribute :code:`data-l10n-args`, which is 245a JSON object storing variables exposed by the developer to the localizer. 246 247This is the main channel for the developer to provide additional variables 248to be used in the localization. 249 250Arguments are rarely needed for situations where it’s currently possible to use 251DTD, since such variables would need to be computed from the code at runtime. 252It's worth noting that, when the :code:`l10n-args` are set in 253the runtime code, they are in fact encoded as JSON and stored together with 254:code:`l10n-id` as an attribute of the element. 255 256__ https://www.w3.org/TR/2011/WD-html5-20110525/text-level-semantics.html 257 258 259Runtime Localization 260==================== 261 262In almost every case the JS runtime code will operate on a particular document, either 263XUL, XHTML or HTML. 264 265If the document has its markup already localized, then Fluent exposes a new 266attribute on the :js:`document` element - :js:`document.l10n`. 267 268This property is an object of type :js:`DOMLocalization` which maintains the main 269localization context for this document and exposes it to runtime code as well. 270 271With a focus on `declarative localization`__, the primary method of localization is 272to alter the localization attributes in the DOM. Fluent provides a method to facilitate this: 273 274.. code-block:: javascript 275 276 document.l10n.setAttributes(element, "new-panel-header"); 277 278This will set the :code:`data-l10n-id` on the element and translate it before the next 279animation frame. 280 281The reason to use this API over manually setting the attribute is that it also 282facilitates encoding l10n arguments as JSON: 283 284.. code-block:: javascript 285 286 document.l10n.setAttributes(element, "containers-disable-alert-ok-button", { 287 tabCount: 5 288 }); 289 290__ https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers 291 292 293Non-Markup Localization 294----------------------- 295 296In rare cases, when the runtime code needs to retrieve the translation and not 297apply it onto the DOM, Fluent provides an API to retrieve it: 298 299.. code-block:: javascript 300 301 let [ msg ] = await document.l10n.formatValues([ 302 {id: "remove-containers-description"} 303 ]); 304 305 alert(msg); 306 307This model is heavily discouraged and should be used only in cases where the 308DOM annotation is not possible. 309 310.. note:: 311 312 This API is available as asynchronous. In case of Firefox, 313 the only non-DOM localizable calls are used where the output goes to 314 a third-party like Bluetooth, Notifications etc. 315 All those cases should already be asynchronous. If you can't avoid synchronous 316 access, you can use ``mozILocalization.formatMessagesSync`` with synchronous IO. 317 318 319Internationalization 320==================== 321 322The majority of internationalization issues are implicitly handled by Fluent without 323any additional requirement. Full Unicode support, `bidirectionality`__, and 324correct number formatting work without any action required from either 325developer or localizer. 326 327__ https://github.com/projectfluent/fluent/wiki/BiDi-in-Fluent 328 329.. code-block:: javascript 330 331 document.l10n.setAttributes(element, "welcome-message", { 332 userName: "اليسع", 333 count: 5 334 }); 335 336A message like this localized to American English will correctly wrap the user 337name in directionality marks, allowing the layout engine to determine how to 338display the bidirectional text. 339 340On the other hand, the same message localized to Arabic will use the Eastern Arabic 341numeral for number "5". 342 343 344Plural Rules 345------------ 346 347The most common localization feature is the ability to provide different variants 348of the same string depending on plural categories. Fluent ties into the Unicode CLDR 349standard called `Plural Rules`_. 350 351In order to allow localizers to use it, all the developer has to do is to pass 352an external argument number: 353 354.. code-block:: javascript 355 356 document.l10n.setAttributes(element, "unread-warning", { unreadCount: 5 }); 357 358Localizers can use the argument to build a multi variant message if their 359language requires that: 360 361.. code-block:: fluent 362 363 unread-warning = 364 { $unreadCount -> 365 [one] You have { $unreadCount } unread message 366 *[other] You have { $unreadCount } unread messages 367 } 368 369If the variant selection is performed based on a number, Fluent matches that 370number against literal numbers as well as its `plural category`__. 371 372If the given translation doesn't need pluralization for the string (for example 373Japanese often will not), the localizer can replace it with: 374 375.. code-block:: fluent 376 377 unread-warning = You have { $unreadCount } unread messages 378 379and the message will preserve the social contract. 380 381One additional feature is that the localizer can further improve the message by 382specifying variants for particular values: 383 384.. code-block:: fluent 385 386 unread-warning = 387 { $unreadCount -> 388 [0] You have no unread messages 389 [1] You have one unread message 390 *[other] You have { $unreadCount } unread messages 391 } 392 393The advantage here is that per-locale choices don't leak onto the source code 394and the developer is not affected. 395 396 397.. note:: 398 399 There is an important distinction between a variant keyed on plural category 400 `one` and digit `1`. Although in English the two are synonymous, in other 401 languages category `one` may be used for other numbers. 402 For example in `Bosnian`__, category `one` is used for numbers like `1`, `21`, `31` 403 and so on, and also for fractional numbers like `0.1`. 404 405__ https://unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html 406__ https://unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#bs 407 408Partially-formatted variables 409----------------------------- 410 411When it comes to formatting data, Fluent allows the developer to provide 412a set of parameters for the formatter, and the localizer can fine tune some of them. 413This technique is called `partially-formatted variables`__. 414 415For example, when formatting a date, the developer can just pass a JS :js:`Date` object, 416but its default formatting will be pretty expressive. In most cases, the developer 417may want to use some of the :js:`Intl.DateTimeFormat` options to select the default 418representation of the date in string: 419 420.. code-block:: javascript 421 422 document.l10n.formatValue("welcome-message", { 423 startDate: FluentDateTime(new Date(), { 424 year: "numeric", 425 month: "long", 426 day: "numeric" 427 }) 428 }); 429 430.. code-block:: fluent 431 432 welcome-message = Your session will start date: { $startDate } 433 434In most cases, that will be enough and the date would get formatted in the current 435Firefox as `February 28, 2018`. 436 437But if in some other locale the string would get too long, the localizer can fine 438tune the options as well: 439 440.. code-block:: fluent 441 442 welcome-message = Początek Twojej sesji: { DATETIME($startDate, month: "short") } 443 444This will adjust the length of the month token in the message to short and get formatted 445in Polish as `28 lut 2018`. 446 447At the moment Fluent supports two formatters that match JS Intl API counterparts: 448 449 * **NUMBER**: `Intl.NumberFormat`__ 450 * **DATETIME**: `Intl.DateTimeFormat`__ 451 452With time more formatters will be added. Also, this feature is not exposed 453to ``setAttributes`` at this point, as that serializes to JSON. 454 455__ https://projectfluent.org/fluent/guide/functions.html#partially-formatted-variables 456__ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat 457__ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat 458 459Registering New L10n Files 460========================== 461 462Fluent uses a wildcard statement, packaging all localization resources into 463their component's `/localization/` directory. 464 465That means that, if a new file is added to a component of Firefox already 466covered by Fluent like `browser`, it's enough to add the new file to the 467repository in a path like `browser/locales/en-US/browser/component/file.ftl`, and 468the toolchain will package it into `browser/localization/browser/component/file.ftl`. 469 470At runtime Firefox uses a special registry for all localization data. It will 471register the browser's `/localization/` directory and make all files inside it 472available to be referenced. 473 474To make the document localized using Fluent, all the developer has to do is add 475localizable resources for Fluent API to use: 476 477.. code-block:: html 478 479 <link rel="localization" href="branding/brand.ftl"/> 480 <link rel="localization" href="browser/preferences/preferences.ftl"/> 481 482The URI provided to the :html:`<link/>` element are relative paths within the localization 483system. 484 485 486Custom Localizations 487==================== 488 489The above method creates a single localization context per document. 490In almost all scenarios that's sufficient. 491 492In rare edge cases where the developer needs to fetch additional resources, or 493the same resources in another language, it is possible to create additional 494Localization object manually using the `Localization` class: 495 496.. code-block:: javascript 497 498 const { Localization } = 499 ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); 500 501 502 const myL10n = new Localization([ 503 "branding/brand.ftl", 504 "browser/preferences/preferences.ftl" 505 ]); 506 507 508 let [isDefaultMsg, isNotDefaultMsg] = 509 await myL10n.formatValues({id: "is-default"}, {id: "is-not-default"}); 510 511 512.. admonition:: Example 513 514 An example of a use case is the Preferences UI in Firefox, which uses the 515 main context to localize the UI but also to build a search index. 516 517 It is common to build such search index both in a current language and additionally 518 in English, since a lot of documentation and online help exist only in English. 519 520 A developer may create manually a new context with the same resources as the main one, 521 but hardcode it to `en-US` and then build the search index using both contexts. 522 523 524By default, all `Localization` contexts are asynchronous. It is possible to create a synchronous 525one by passing an `sync = false` argument to the constructor, or calling the `SetIsSync(bool)` method 526on the class. 527 528 529.. code-block:: javascript 530 531 const { Localization } = 532 ChromeUtils.import("resource://gre/modules/Localization.jsm", {}); 533 534 535 const myL10n = new Localization([ 536 "branding/brand.ftl", 537 "browser/preferences/preferences.ftl" 538 ], false); 539 540 541 let [isDefaultMsg, isNotDefaultMsg] = 542 myL10n.formatValuesSync({id: "is-default"}, {id: "is-not-default"}); 543 544 545Synchronous contexts should be always avoided as they require synchronous I/O. If you think your use case 546requires a synchronous localization context, please consult Gecko, Performance and L10n Drivers teams. 547 548 549Designing Localizable APIs 550========================== 551 552When designing localizable APIs, the most important rule is to resolve localization as 553late as possible. That means that instead of resolving strings somewhere deep in the 554codebase and then passing them on, or even caching, it is highly recommended to pass 555around :code:`l10n-id` or :code:`[l10n-id, l10n-args]` pairs until the top-most code 556resolves them or applies them onto the DOM element. 557 558 559Testing 560======= 561 562When writing tests that involve both I18n and L10n, the general rule is that 563result strings are opaque. That means that the developer should not assume any particular 564value and should never test against it. 565 566In case of raw i18n the :js:`resolvedOptions` method on all :js:`Intl.*` formatters 567makes it relatively easy. In case of localization, the recommended way is to test that 568the code sets the right :code:`l10n-id`/:code:`l10n-args` attributes like this: 569 570.. code-block:: javascript 571 572 testedFunction(); 573 574 const l10nAttrs = document.l10n.getAttributes(element); 575 576 deepEquals(l10nAttrs, { 577 id: "my-expected-id", 578 args: { 579 unreadCount: 5 580 } 581 }); 582 583If the code really has to test for particular values in the localized UI, it is 584always better to scan for a variable: 585 586.. code-block:: javascript 587 588 testedFunction(); 589 590 equals(element.textContent.contains("John")); 591 592.. important:: 593 594 Testing against whole values is brittle and will break when we insert Unicode 595 bidirectionality marks into the result string or adapt the output in other ways. 596 597 598Pseudolocalization 599================== 600 601When working with a Fluent-backed UI, the developer gets a new tool to test their UI 602against several classes of problems. 603 604Pseudolocalization is a mechanism which transforms messages on the fly, using 605specific logic to help emulate how the UI will look once it gets localized. 606 607The three classes of potential problems that this can help with are: 608 609 - Hardcoded strings. 610 611 Turning on pseudolocalization should expose any strings that were left 612 hardcoded in the source, since they won't get transformed. 613 614 615 - UI space not adapting to longer text. 616 617 Many languages use longer strings than English. For example, German strings 618 may be 30% longer (or more). Turning on pseudolocalization is a quick way to 619 test how the layout handles such locales. 620 621 622 - Bidi adaptation. 623 624 For many developers, testing the UI in right-to-left mode is hard. 625 Pseudolocalization shows how a right-to-left locale will look like. 626 627To turn on pseudolocalization, add a new string pref :js:`intl.l10n.pseudo` and 628select the strategy to be used: 629 630 - :js:`accented` - Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ 631 632 This strategy replaces all Latin characters with their accented equivalents, 633 and duplicates some vowels to create roughly 30% longer strings. 634 635 636 - :js:`bidi` - ɥsıʅƃuƎ ıpıԐ 637 638 This strategy replaces all Latin characters with their 180 degree rotated versions 639 and enforces right to left text flow using Unicode UAX#9 `Explicit Directional Embeddings`__. 640 In this mode, the UI directionality will also be set to right-to-left. 641 642__ https://www.unicode.org/reports/tr9/#Explicit_Directional_Embeddings 643 644Inner Structure of Fluent 645========================= 646 647The inner structure of Fluent in Gecko is out of scope of this tutorial, but 648since the class and file names may show up during debugging or profiling, 649below is a list of major components, each with a corresponding file in `/intl/l10n` 650modules in Gecko. 651 652FluentBundle 653-------------- 654 655FluentBundle is the lowest level API. It's fully synchronous, contains a parser for the 656FTL file format and a resolver for the logic. It is not meant to be used by 657consumers directly. 658 659In the future we intend to offer this layer for standardization and it may become 660part of the :js:`mozIntl.*` or even :js:`Intl.*` API sets. 661 662That part of the codebase is also the first that we'll be looking to port to Rust. 663 664 665Localization 666------------ 667 668Localization is a higher level API which uses :js:`FluentBundle` internally but 669provides a full layer of compound message formatting and robust error fall-backing. 670 671It is intended for use in runtime code and contains all fundamental localization 672methods. 673 674 675DOMLocalization 676--------------- 677 678DOMLocalization extends :js:`Localization` with functionality to operate on HTML, XUL 679and the DOM directly including DOM Overlays and Mutation Observers. 680 681DocumentL10n 682------------ 683 684DocumentL10n implements the DocumentL10n WebIDL API and allows Document to 685communicate with DOMLocalization. 686 687L10nRegistry 688------------ 689 690L10nRegistry is our resource management service. It 691maintains the state of resources packaged into the build and language packs, 692providing an asynchronous iterator of :js:`FluentBundle` objects for a given locale set 693and resources that the :js:`Localization` class uses. 694 695 696.. _Fluent: https://projectfluent.org/ 697.. _DTD: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Tutorial/Localization 698.. _StringBundle: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Tutorial/Property_Files 699.. _Firefox Preferences: https://bugzilla.mozilla.org/show_bug.cgi?id=1415730 700.. _Unprivileged Contexts: https://bugzilla.mozilla.org/show_bug.cgi?id=1407418 701.. _System Add-ons: https://bugzilla.mozilla.org/show_bug.cgi?id=1425104 702.. _CLDR: http://cldr.unicode.org/ 703.. _ICU: http://site.icu-project.org/ 704.. _Unicode: https://www.unicode.org/ 705.. _Fluent Syntax Guide: https://projectfluent.org/fluent/guide/ 706.. _Pontoon: https://pontoon.mozilla.org/ 707.. _Plural Rules: http://cldr.unicode.org/index/cldr-spec/plural-rules 708