legacy.rst (20918B)
1 .. role:: bash(code) 2 :language: bash 3 4 .. role:: js(code) 5 :language: javascript 6 7 .. role:: python(code) 8 :language: python 9 10 ======================== 11 Migrating Legacy Formats 12 ======================== 13 14 Migrating from legacy formats (.dtd, .properties) is different from migrating 15 Fluent to Fluent. When migrating legacy code paths, you'll need to adjust the 16 Fluent strings for the quirks Mozilla uses in the legacy code paths. You'll 17 find a number of specialized functionalities here. 18 19 Legacy Migration Tools 20 ---------------------- 21 22 To assist with legacy format migrations, some scripting tools are provided: 23 24 - `XUL+DTD to Fluent`_ 25 - `.properties to Fluent`_ 26 27 When creating a migration, one or both of these tools may provide a good 28 starting point for manual work by automating at least a part of the migration, 29 including recipe generation and refactoring the calling code. 30 31 .. _XUL+DTD to Fluent: https://github.com/zbraniecki/convert_xul_to_fluent 32 .. _.properties to Fluent: https://github.com/mozilla/properties-to-ftl 33 34 Basic Migration 35 --------------- 36 37 Let’s consider a basic example: one string needs to be migrated, without 38 any further change, from a DTD file to Fluent. 39 40 The legacy string is stored in :bash:`toolkit/locales/en-US/chrome/global/findbar.dtd`: 41 42 43 .. code-block:: dtd 44 45 <!ENTITY next.tooltip "Find the next occurrence of the phrase"> 46 47 48 The new Fluent string is stored in :bash:`toolkit/locales/en-US/toolkit/main-window/findbar.ftl`: 49 50 51 .. code-block:: properties 52 53 findbar-next = 54 .tooltiptext = Find the next occurrence of the phrase 55 56 57 This is how the migration recipe looks: 58 59 60 .. code-block:: python 61 62 # Any copyright is dedicated to the Public Domain. 63 # http://creativecommons.org/publicdomain/zero/1.0/ 64 65 from __future__ import absolute_import 66 import fluent.syntax.ast as FTL 67 from fluent.migrate.helpers import transforms_from 68 69 def migrate(ctx): 70 """Bug 1411707 - Migrate the findbar XBL binding to a Custom Element, part {index}.""" 71 72 ctx.add_transforms( 73 "toolkit/toolkit/main-window/findbar.ftl", 74 "toolkit/toolkit/main-window/findbar.ftl", 75 transforms_from( 76 """ 77 findbar-next = 78 .tooltiptext = { COPY(from_path, "next.tooltip") } 79 """, from_path="toolkit/chrome/global/findbar.dtd")) 80 81 82 The first important thing to notice is that the migration recipe needs file 83 paths relative to a localization repository, losing :bash:`locales/en-US/`: 84 85 - :bash:`toolkit/locales/en-US/chrome/global/findbar.dtd` becomes 86 :bash:`toolkit/chrome/global/findbar.dtd`. 87 - :bash:`toolkit/locales/en-US/toolkit/main-window/findbar.ftl` becomes 88 :bash:`toolkit/toolkit/main-window/findbar.ftl`. 89 90 The :python:`context.add_transforms` function takes 3 arguments: 91 92 - Path to the target l10n file. 93 - Path to the reference (en-US) file. 94 - An array of Transforms. Transforms are AST nodes which describe how legacy 95 translations should be migrated. 96 97 .. note:: 98 99 For migrations of Firefox localizations, the target and reference path 100 are the same. This isn't true for all projects that use Fluent, so both 101 arguments are required. 102 103 In this case there is only one Transform that migrates the string with ID 104 :js:`next.tooltip` from :bash:`toolkit/chrome/global/findbar.dtd`, and injects 105 it in the FTL fragment. The :python:`COPY` Transform allows to copy the string 106 from an existing file as is, while :python:`from_path` is used to avoid 107 repeating the same path multiple times, making the recipe more readable. Without 108 :python:`from_path`, this could be written as: 109 110 111 .. code-block:: python 112 113 ctx.add_transforms( 114 "toolkit/toolkit/main-window/findbar.ftl", 115 "toolkit/toolkit/main-window/findbar.ftl", 116 transforms_from( 117 """ 118 findbar-next = 119 .tooltiptext = { COPY("toolkit/chrome/global/findbar.dtd", "next.tooltip") } 120 """)) 121 122 123 This method of writing migration recipes allows to take the original FTL 124 strings, and simply replace the value of each message with a :python:`COPY` 125 Transform. :python:`transforms_from` takes care of converting the FTL syntax 126 into an array of Transforms describing how the legacy translations should be 127 migrated. This manner of defining migrations is only suitable to simple strings 128 where a copy operation is sufficient. For more complex use-cases which require 129 some additional logic in Python, it’s necessary to resort to the raw AST. 130 131 132 The example above is equivalent to the following syntax, which exposes 133 the underlying AST structure: 134 135 136 .. code-block:: python 137 138 ctx.add_transforms( 139 "toolkit/toolkit/main-window/findbar.ftl", 140 "toolkit/toolkit/main-window/findbar.ftl", 141 [ 142 FTL.Message( 143 id=FTL.Identifier("findbar-next"), 144 attributes=[ 145 FTL.Attribute( 146 id=FTL.Identifier("tooltiptext"), 147 value=COPY( 148 "toolkit/chrome/global/findbar.dtd", 149 "next.tooltip" 150 ) 151 ) 152 ] 153 ) 154 ] 155 ) 156 157 This creates a :python:`Message`, taking the value from the legacy string 158 :js:`findbar-next`. A message can have an array of attributes, each with an ID 159 and a value: in this case there is only one attribute, with ID :js:`tooltiptext` 160 and :js:`value` copied from the legacy string. 161 162 Notice how both the ID of the message and the ID of the attribute are 163 defined as an :python:`FTL.Identifier`, not simply as a string. 164 165 166 .. tip:: 167 168 It’s possible to concatenate arrays of Transforms defined manually, like in 169 the last example, with those coming from :python:`transforms_from`, by using 170 the :python:`+` operator. Alternatively, it’s possible to use multiple 171 :python:`add_transforms`. 172 173 The order of Transforms provided in the recipe is not relevant, the reference 174 file is used for ordering messages. 175 176 177 Replacing Content in Legacy Strings 178 ----------------------------------- 179 180 While :python:`COPY` allows to copy a legacy string as is, :python:`REPLACE` 181 (from `fluent.migrate`) allows to replace content while performing the 182 migration. This is necessary, for example, when migrating strings that include 183 placeholders or entities that need to be replaced to adapt to Fluent syntax. 184 185 Consider for example the following string: 186 187 188 .. code-block:: DTD 189 190 <!ENTITY aboutSupport.featuresTitle "&brandShortName; Features"> 191 192 193 Which needs to be migrated to: 194 195 196 .. code-block:: fluent 197 198 features-title = { -brand-short-name } Features 199 200 201 The entity :js:`&brandShortName;` needs to be replaced with a term reference: 202 203 204 .. code-block:: python 205 206 FTL.Message( 207 id=FTL.Identifier("features-title"), 208 value=REPLACE( 209 "toolkit/chrome/global/aboutSupport.dtd", 210 "aboutSupport.featuresTitle", 211 { 212 "&brandShortName;": TERM_REFERENCE("brand-short-name"), 213 }, 214 ) 215 ), 216 217 218 This creates an :python:`FTL.Message`, taking the value from the legacy string 219 :js:`aboutSupport.featuresTitle`, but replacing the specified text with a 220 Fluent term reference. 221 222 .. note:: 223 :python:`REPLACE` replaces all occurrences of the specified text. 224 225 226 It’s also possible to replace content with a specific text: in that case, it 227 needs to be defined as a :python:`TextElement`. For example, to replace 228 :js:`example.com` with HTML markup: 229 230 231 .. code-block:: python 232 233 value=REPLACE( 234 "browser/chrome/browser/preferences/preferences.properties", 235 "searchResults.sorryMessageWin", 236 { 237 "example.com": FTL.TextElement('<span data-l10n-name="example"></span>') 238 } 239 ) 240 241 242 The situation is more complex when a migration recipe needs to replace 243 :js:`printf` arguments like :js:`%S`. In fact, the format used for localized 244 and source strings doesn’t need to match, and the two following strings using 245 unordered and ordered argument are perfectly equivalent: 246 247 248 .. code-block:: properties 249 250 btn-quit = Quit %S 251 btn-quit = Quit %1$S 252 253 254 In this scenario, replacing :js:`%S` would work on the first version, but not 255 on the second, and there’s no guarantee that the localized string uses the 256 same format as the source string. 257 258 Consider also the following string that uses :js:`%S` for two different 259 variables, implicitly relying on the order in which the arguments appear: 260 261 262 .. code-block:: properties 263 264 updateFullName = %S (%S) 265 266 267 And the target Fluent string: 268 269 270 .. code-block:: fluent 271 272 update-full-name = { $name } ({ $buildID }) 273 274 275 As indicated, :python:`REPLACE` would replace all occurrences of :js:`%S`, so 276 only one variable could be set. The string needs to be normalized and treated 277 like: 278 279 280 .. code-block:: properties 281 282 updateFullName = %1$S (%2$S) 283 284 285 This can be obtained by calling :python:`REPLACE` with 286 :python:`normalize_printf=True`: 287 288 289 .. code-block:: python 290 291 FTL.Message( 292 id=FTL.Identifier("update-full-name"), 293 value=REPLACE( 294 "toolkit/chrome/mozapps/update/updates.properties", 295 "updateFullName", 296 { 297 "%1$S": VARIABLE_REFERENCE("name"), 298 "%2$S": VARIABLE_REFERENCE("buildID"), 299 }, 300 normalize_printf=True 301 ) 302 ) 303 304 305 .. attention:: 306 307 To avoid any issues :python:`normalize_printf=True` should always be used when 308 replacing :js:`printf` arguments. This is the default behaviour when working 309 with .properties files. 310 311 .. note:: 312 313 :python:`VARIABLE_REFERENCE`, :python:`MESSAGE_REFERENCE`, and 314 :python:`TERM_REFERENCE` are helper Transforms which can be used to save 315 keystrokes in common cases where using the raw AST is too verbose. 316 317 :python:`VARIABLE_REFERENCE` is used to create a reference to a variable, e.g. 318 :js:`{ $variable }`. 319 320 :python:`MESSAGE_REFERENCE` is used to create a reference to another message, 321 e.g. :js:`{ another-string }`. 322 323 :python:`TERM_REFERENCE` is used to create a reference to a `term`__, 324 e.g. :js:`{ -brand-short-name }`. 325 326 Both Transforms need to be imported at the beginning of the recipe, e.g. 327 :python:`from fluent.migrate.helpers import VARIABLE_REFERENCE` 328 329 __ https://projectfluent.org/fluent/guide/terms.html 330 331 332 Trimming Unnecessary Whitespaces in Translations 333 ------------------------------------------------ 334 335 .. note:: 336 337 This section was updated in May 2020 to reflect the change to the default 338 behavior: legacy translations are now trimmed, unless the :python:`trim` 339 parameter is set explicitly. 340 341 It’s not uncommon to have strings with unnecessary leading or trailing spaces 342 in legacy translations. These are not meaningful, don’t have practical results 343 on the way the string is displayed in products, and are added mostly for 344 formatting reasons. For example, consider this DTD string: 345 346 347 .. code-block:: DTD 348 349 <!ENTITY aboutAbout.note "This is a list of “about” pages for your convenience.<br/> 350 Some of them might be confusing. Some are for diagnostic purposes only.<br/> 351 And some are omitted because they require query strings."> 352 353 354 By default, the :python:`COPY`, :python:`REPLACE`, and :python:`PLURALS` 355 transforms will strip the leading and trailing whitespace from each line of the 356 translation, as well as the empty leading and trailing lines. The above string 357 will be migrated as the following Fluent message, despite copious indentation 358 on the second and the third line in the original: 359 360 361 .. code-block:: fluent 362 363 about-about-note = 364 This is a list of “about” pages for your convenience.<br/> 365 Some of them might be confusing. Some are for diagnostic purposes only.<br/> 366 And some are omitted because they require query strings. 367 368 369 To disable the default trimming behavior, set :python:`trim:"False"` or 370 :python:`trim=False`, depending on the context: 371 372 373 .. code-block:: python 374 375 transforms_from( 376 """ 377 about-about-note = { COPY("toolkit/chrome/global/aboutAbout.dtd", "aboutAbout.note", trim:"False") } 378 """) 379 380 FTL.Message( 381 id=FTL.Identifier("discover-description"), 382 value=REPLACE( 383 "toolkit/chrome/mozapps/extensions/extensions.dtd", 384 "discover.description2", 385 { 386 "&brandShortName;": TERM_REFERENCE("brand-short-name") 387 }, 388 trim=False 389 ) 390 ), 391 392 393 Concatenating Strings 394 --------------------- 395 396 It's best practice to only expose complete phrases to localization, and to avoid 397 stitching localized strings together in code. With `DTD` and `properties`, 398 there were few options. So when migrating to Fluent, you'll find 399 it quite common to concatenate multiple strings coming from `DTD` and 400 `properties`, for example to create sentences with HTML markup. It’s possible to 401 concatenate strings and text elements in a migration recipe using the 402 :python:`CONCAT` Transform. 403 404 Note that in case of simple migrations using :python:`transforms_from`, the 405 concatenation is carried out implicitly by using the Fluent syntax interleaved 406 with :python:`COPY()` transform calls to define the migration recipe. 407 408 Consider the following example: 409 410 411 .. code-block:: properties 412 413 # %S is replaced by a link, using searchResults.needHelpSupportLink as text 414 searchResults.needHelp = Need help? Visit %S 415 416 # %S is replaced by "Firefox" 417 searchResults.needHelpSupportLink = %S Support 418 419 420 In Fluent: 421 422 423 .. code-block:: fluent 424 425 search-results-need-help-support-link = Need help? Visit <a data-l10n-name="url">{ -brand-short-name } Support</a> 426 427 428 This is quite a complex migration: it requires to take 2 legacy strings, and 429 concatenate their values with HTML markup. Here’s how the Transform is defined: 430 431 432 .. code-block:: python 433 434 FTL.Message( 435 id=FTL.Identifier("search-results-help-link"), 436 value=REPLACE( 437 "browser/chrome/browser/preferences/preferences.properties", 438 "searchResults.needHelp", 439 { 440 "%S": CONCAT( 441 FTL.TextElement('<a data-l10n-name="url">'), 442 REPLACE( 443 "browser/chrome/browser/preferences/preferences.properties", 444 "searchResults.needHelpSupportLink", 445 { 446 "%1$S": TERM_REFERENCE("brand-short-name"), 447 }, 448 normalize_printf=True 449 ), 450 FTL.TextElement("</a>") 451 ) 452 } 453 ) 454 ), 455 456 457 :js:`%S` in :js:`searchResults.needHelpSupportLink` is replaced by a reference 458 to the term :js:`-brand-short-name`, migrating from :js:`%S Support` to :js:`{ 459 -brand-short-name } Support`. The result of this operation is then inserted 460 between two text elements to create the anchor markup. The resulting text is 461 finally used to replace :js:`%S` in :js:`searchResults.needHelp`, and used as 462 value for the FTL message. 463 464 465 .. important:: 466 467 When concatenating existing strings, avoid introducing changes to the original 468 text, for example adding spaces or punctuation. Each language has its own 469 rules, and this might result in poor migrated strings. In case of doubt, 470 always ask for feedback. 471 472 473 When more than 1 element is passed in to concatenate, :python:`CONCAT` 474 disables whitespace trimming described in the section above on all legacy 475 Transforms passed into it: :python:`COPY`, :python:`REPLACE`, and 476 :python:`PLURALS`, unless the :python:`trim` parameters has been set 477 explicitly on them. This helps ensure that spaces around segments are not 478 lost during the concatenation. 479 480 When only a single element is passed into :python:`CONCAT`, however, the 481 trimming behavior is not altered, and follows the rules described in the 482 previous section. This is meant to make :python:`CONCAT(COPY())` equivalent 483 to a bare :python:`COPY()`. 484 485 486 Plural Strings 487 -------------- 488 489 Migrating plural strings from `.properties` files usually involves two 490 Transforms from :python:`fluent.migrate.transforms`: the 491 :python:`REPLACE_IN_TEXT` Transform takes TextElements as input, making it 492 possible to pass it as the foreach function of the :python:`PLURALS` Transform. 493 494 Consider the following legacy string: 495 496 497 .. code-block:: properties 498 499 # LOCALIZATION NOTE (disableContainersOkButton): Semi-colon list of plural forms. 500 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals 501 # #1 is the number of container tabs 502 disableContainersOkButton = Close #1 Container Tab;Close #1 Container Tabs 503 504 505 In Fluent: 506 507 508 .. code-block:: fluent 509 510 containers-disable-alert-ok-button = 511 { $tabCount -> 512 [one] Close { $tabCount } Container Tab 513 *[other] Close { $tabCount } Container Tabs 514 } 515 516 517 This is how the Transform for this string is defined: 518 519 520 .. code-block:: python 521 522 FTL.Message( 523 id=FTL.Identifier("containers-disable-alert-ok-button"), 524 value=PLURALS( 525 "browser/chrome/browser/preferences/preferences.properties", 526 "disableContainersOkButton", 527 VARIABLE_REFERENCE("tabCount"), 528 lambda text: REPLACE_IN_TEXT( 529 text, 530 { 531 "#1": VARIABLE_REFERENCE("tabCount") 532 } 533 ) 534 ) 535 ) 536 537 538 The `PLURALS` Transform will take care of creating the correct number of plural 539 categories for each language. Notice how `#1` is replaced for each of these 540 variants with :js:`{ $tabCount }`, using :python:`REPLACE_IN_TEXT` and 541 :python:`VARIABLE_REFERENCE("tabCount")`. 542 543 In this case it’s not possible to use :python:`REPLACE` because it takes a file 544 path and a message ID as arguments, whereas here the recipe needs to operate on 545 regular text. The replacement is performed on each plural form of the original 546 string, where plural forms are separated by a semicolon. 547 548 Explicit Variants 549 ----------------- 550 551 Explicitly creating variants of a string is useful for platform-dependent 552 terminology, but also in cases where you want a one-vs-many split of a string. 553 It’s always possible to migrate strings by manually creating the underlying AST 554 structure. Consider the following complex Fluent string: 555 556 557 .. code-block:: fluent 558 559 use-current-pages = 560 .label = 561 { $tabCount -> 562 [1] Use Current Page 563 *[other] Use Current Pages 564 } 565 .accesskey = C 566 567 568 The migration for this string is quite complex: the :js:`label` attribute is 569 created from 2 different legacy strings, and it’s not a proper plural form. 570 Notice how the first string is associated to the :js:`1` case, not the :js:`one` 571 category used in plural forms. For these reasons, it’s not possible to use 572 :python:`PLURALS`, the Transform needs to be crafted recreating the AST. 573 574 575 .. code-block:: python 576 577 578 FTL.Message( 579 id=FTL.Identifier("use-current-pages"), 580 attributes=[ 581 FTL.Attribute( 582 id=FTL.Identifier("label"), 583 value=FTL.Pattern( 584 elements=[ 585 FTL.Placeable( 586 expression=FTL.SelectExpression( 587 selector=VARIABLE_REFERENCE("tabCount"), 588 variants=[ 589 FTL.Variant( 590 key=FTL.NumberLiteral("1"), 591 default=False, 592 value=COPY( 593 "browser/chrome/browser/preferences/main.dtd", 594 "useCurrentPage.label", 595 ) 596 ), 597 FTL.Variant( 598 key=FTL.Identifier("other"), 599 default=True, 600 value=COPY( 601 "browser/chrome/browser/preferences/main.dtd", 602 "useMultiple.label", 603 ) 604 ) 605 ] 606 ) 607 ) 608 ] 609 ) 610 ), 611 FTL.Attribute( 612 id=FTL.Identifier("accesskey"), 613 value=COPY( 614 "browser/chrome/browser/preferences/main.dtd", 615 "useCurrentPage.accesskey", 616 ) 617 ), 618 ], 619 ), 620 621 622 This Transform uses several concepts already described in this document. Notable 623 is the :python:`SelectExpression` inside a :python:`Placeable`, with an array 624 of :python:`Variant` objects. Exactly one of those variants needs to have 625 ``default=True``. 626 627 This example can still use :py:func:`transforms_from()``, since existing strings 628 are copied without interpolation. 629 630 .. code-block:: python 631 632 transforms_from( 633 """ 634 use-current-pages = 635 .label = 636 { $tabCount -> 637 [1] { COPY(main_dtd, "useCurrentPage.label") } 638 *[other] { COPY(main_dtd, "useMultiple.label") } 639 } 640 .accesskey = { COPY(main_dtd, "useCurrentPage.accesskey") } 641 """, main_dtd="browser/chrome/browser/preferences/main.dtd" 642 )