tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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  )