summaryrefslogtreecommitdiffstats
path: root/third_party/rust/fluent
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/rust/fluent
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--third_party/rust/fluent-bundle/.cargo-checksum.json1
-rw-r--r--third_party/rust/fluent-bundle/Cargo.toml78
-rw-r--r--third_party/rust/fluent-bundle/LICENSE-APACHE201
-rw-r--r--third_party/rust/fluent-bundle/LICENSE-MIT19
-rw-r--r--third_party/rust/fluent-bundle/README.md111
-rw-r--r--third_party/rust/fluent-bundle/benches/resolver.rs168
-rw-r--r--third_party/rust/fluent-bundle/benches/resolver_iai.rs79
-rw-r--r--third_party/rust/fluent-bundle/examples/README.md6
-rw-r--r--third_party/rust/fluent-bundle/src/args.rs120
-rw-r--r--third_party/rust/fluent-bundle/src/bundle.rs615
-rw-r--r--third_party/rust/fluent-bundle/src/concurrent.rs59
-rw-r--r--third_party/rust/fluent-bundle/src/entry.rs62
-rw-r--r--third_party/rust/fluent-bundle/src/errors.rs86
-rw-r--r--third_party/rust/fluent-bundle/src/lib.rs127
-rw-r--r--third_party/rust/fluent-bundle/src/memoizer.rs18
-rw-r--r--third_party/rust/fluent-bundle/src/message.rs274
-rw-r--r--third_party/rust/fluent-bundle/src/resolver/errors.rs96
-rw-r--r--third_party/rust/fluent-bundle/src/resolver/expression.rs66
-rw-r--r--third_party/rust/fluent-bundle/src/resolver/inline_expression.rs181
-rw-r--r--third_party/rust/fluent-bundle/src/resolver/mod.rs42
-rw-r--r--third_party/rust/fluent-bundle/src/resolver/pattern.rs108
-rw-r--r--third_party/rust/fluent-bundle/src/resolver/scope.rs141
-rw-r--r--third_party/rust/fluent-bundle/src/resource.rs171
-rw-r--r--third_party/rust/fluent-bundle/src/types/mod.rs202
-rw-r--r--third_party/rust/fluent-bundle/src/types/number.rs252
-rw-r--r--third_party/rust/fluent-bundle/src/types/plural.rs22
-rw-r--r--third_party/rust/fluent-fallback/.cargo-checksum.json1
-rw-r--r--third_party/rust/fluent-fallback/CHANGELOG.md68
-rw-r--r--third_party/rust/fluent-fallback/Cargo.lock415
-rw-r--r--third_party/rust/fluent-fallback/Cargo.toml74
-rw-r--r--third_party/rust/fluent-fallback/LICENSE-APACHE201
-rw-r--r--third_party/rust/fluent-fallback/LICENSE-MIT19
-rw-r--r--third_party/rust/fluent-fallback/README.md102
-rw-r--r--third_party/rust/fluent-fallback/examples/resources/en-US/simple.ftl7
-rw-r--r--third_party/rust/fluent-fallback/examples/resources/pl/simple.ftl8
-rw-r--r--third_party/rust/fluent-fallback/examples/simple-fallback.rs237
-rw-r--r--third_party/rust/fluent-fallback/src/bundles.rs426
-rw-r--r--third_party/rust/fluent-fallback/src/cache.rs253
-rw-r--r--third_party/rust/fluent-fallback/src/env.rs84
-rw-r--r--third_party/rust/fluent-fallback/src/errors.rs71
-rw-r--r--third_party/rust/fluent-fallback/src/generator.rs41
-rw-r--r--third_party/rust/fluent-fallback/src/lib.rs118
-rw-r--r--third_party/rust/fluent-fallback/src/localization.rs137
-rw-r--r--third_party/rust/fluent-fallback/src/pin_cell/README.md2
-rw-r--r--third_party/rust/fluent-fallback/src/pin_cell/mod.rs97
-rw-r--r--third_party/rust/fluent-fallback/src/pin_cell/pin_mut.rs50
-rw-r--r--third_party/rust/fluent-fallback/src/pin_cell/pin_ref.rs47
-rw-r--r--third_party/rust/fluent-fallback/src/types.rs141
-rw-r--r--third_party/rust/fluent-fallback/tests/localization_test.rs518
-rw-r--r--third_party/rust/fluent-fallback/tests/resources/en-US/test.ftl4
-rw-r--r--third_party/rust/fluent-fallback/tests/resources/en-US/test2.ftl10
-rw-r--r--third_party/rust/fluent-fallback/tests/resources/pl/test.ftl4
-rw-r--r--third_party/rust/fluent-fallback/tests/resources/pl/test2.ftl9
-rw-r--r--third_party/rust/fluent-langneg/.cargo-checksum.json1
-rw-r--r--third_party/rust/fluent-langneg/Cargo.toml61
-rw-r--r--third_party/rust/fluent-langneg/README.md113
-rw-r--r--third_party/rust/fluent-langneg/benches/negotiate.rs40
-rw-r--r--third_party/rust/fluent-langneg/src/accepted_languages.rs41
-rw-r--r--third_party/rust/fluent-langneg/src/lib.rs49
-rw-r--r--third_party/rust/fluent-langneg/src/negotiate/likely_subtags.rs39
-rw-r--r--third_party/rust/fluent-langneg/src/negotiate/mod.rs233
-rw-r--r--third_party/rust/fluent-pseudo/.cargo-checksum.json1
-rw-r--r--third_party/rust/fluent-pseudo/Cargo.toml27
-rw-r--r--third_party/rust/fluent-pseudo/LICENSE-APACHE201
-rw-r--r--third_party/rust/fluent-pseudo/LICENSE-MIT19
-rw-r--r--third_party/rust/fluent-pseudo/README.md48
-rw-r--r--third_party/rust/fluent-pseudo/src/lib.rs135
-rw-r--r--third_party/rust/fluent-syntax/.cargo-checksum.json1
-rw-r--r--third_party/rust/fluent-syntax/Cargo.lock672
-rw-r--r--third_party/rust/fluent-syntax/Cargo.toml78
-rw-r--r--third_party/rust/fluent-syntax/LICENSE-APACHE201
-rw-r--r--third_party/rust/fluent-syntax/LICENSE-MIT19
-rw-r--r--third_party/rust/fluent-syntax/README.md63
-rw-r--r--third_party/rust/fluent-syntax/benches/contexts/README.md4
-rw-r--r--third_party/rust/fluent-syntax/benches/parser.rs141
-rw-r--r--third_party/rust/fluent-syntax/benches/parser_iai.rs15
-rw-r--r--third_party/rust/fluent-syntax/src/ast/helper.rs25
-rw-r--r--third_party/rust/fluent-syntax/src/ast/mod.rs1446
-rw-r--r--third_party/rust/fluent-syntax/src/bin/parser.rs42
-rw-r--r--third_party/rust/fluent-syntax/src/bin/update_fixtures.rs44
-rw-r--r--third_party/rust/fluent-syntax/src/lib.rs51
-rw-r--r--third_party/rust/fluent-syntax/src/parser/comment.rs89
-rw-r--r--third_party/rust/fluent-syntax/src/parser/core.rs307
-rw-r--r--third_party/rust/fluent-syntax/src/parser/errors.rs169
-rw-r--r--third_party/rust/fluent-syntax/src/parser/expression.rs224
-rw-r--r--third_party/rust/fluent-syntax/src/parser/helper.rs169
-rw-r--r--third_party/rust/fluent-syntax/src/parser/macros.rs11
-rw-r--r--third_party/rust/fluent-syntax/src/parser/mod.rs278
-rw-r--r--third_party/rust/fluent-syntax/src/parser/pattern.rs207
-rw-r--r--third_party/rust/fluent-syntax/src/parser/runtime.rs61
-rw-r--r--third_party/rust/fluent-syntax/src/parser/slice.rs25
-rw-r--r--third_party/rust/fluent-syntax/src/unicode.rs159
-rw-r--r--third_party/rust/fluent-testing/.cargo-checksum.json1
-rw-r--r--third_party/rust/fluent-testing/Cargo.toml67
-rw-r--r--third_party/rust/fluent-testing/LICENSE-APACHE201
-rw-r--r--third_party/rust/fluent-testing/LICENSE-MIT19
-rw-r--r--third_party/rust/fluent-testing/README.md74
-rw-r--r--third_party/rust/fluent-testing/resources/README.md8
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/branding/brand.ftl26
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/aboutDialog.ftl61
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/allTabsMenu.ftl24
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/appmenu.ftl33
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/branding/brandings.ftl25
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/branding/sync-brand.ftl13
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/browser.ftl593
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/browserContext.ftl393
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/browserSets.ftl215
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/downloads.ftl163
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/menubar.ftl278
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/places.ftl77
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/addEngine.ftl22
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/blocklists.ftl33
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/clearSiteData.ftl56
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/colors.ftl48
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/connection.ftl106
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/fonts.ftl116
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/languages.ftl73
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/permissions.ftl166
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/preferences.ftl1389
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/selectBookmark.ftl9
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/siteDataSettings.ftl63
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/protectionsPanel.ftl107
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/sanitize.ftl110
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/browser/sidebarMenu.ftl15
-rw-r--r--third_party/rust/fluent-testing/resources/browser/en-US/preview/interventions.ftl40
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/branding/brand.ftl37
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/aboutDialog.ftl60
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/allTabsMenu.ftl26
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/appmenu.ftl34
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/branding/brandings.ftl31
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/branding/sync-brand.ftl107
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/browser.ftl574
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/browserContext.ftl319
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/browserSets.ftl180
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/downloads.ftl166
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/menubar.ftl274
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/places.ftl68
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/addEngine.ftl22
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/blocklists.ftl33
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/clearSiteData.ftl53
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/colors.ftl37
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/connection.ftl107
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/fonts.ftl157
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/languages.ftl73
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/permissions.ftl157
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/preferences.ftl1166
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/selectBookmark.ftl9
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/siteDataSettings.ftl57
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/protectionsPanel.ftl108
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/sanitize.ftl110
-rw-r--r--third_party/rust/fluent-testing/resources/browser/pl/browser/sidebarMenu.ftl15
-rw-r--r--third_party/rust/fluent-testing/resources/empty-resource/en-US/empty/empty-all.ftl0
-rw-r--r--third_party/rust/fluent-testing/resources/empty-resource/en-US/empty/empty-one.ftl0
-rw-r--r--third_party/rust/fluent-testing/resources/empty-resource/pl/empty/empty-all.ftl0
-rw-r--r--third_party/rust/fluent-testing/resources/empty-resource/pl/empty/empty-one.ftl1
-rw-r--r--third_party/rust/fluent-testing/resources/missing-resource/pl/missing/missing-one.ftl1
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/en-US/security/certificates/certManager.ftl225
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/en-US/security/certificates/deviceManager.ftl130
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/featuregates/features.ftl124
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/global/textActions.ftl49
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/printing/printUI.ftl120
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/updates/history.ftl27
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/pl/security/certificates/certManager.ftl251
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/pl/security/certificates/deviceManager.ftl135
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/featuregates/features.ftl100
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/global/textActions.ftl49
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/printing/printUI.ftl97
-rw-r--r--third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/updates/history.ftl27
-rw-r--r--third_party/rust/fluent-testing/src/fs.rs41
-rw-r--r--third_party/rust/fluent-testing/src/lib.rs5
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/browser.rs389
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/empty_resource_all_locales.rs25
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/empty_resource_one_locale.rs25
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/missing_optional_all_locales.rs36
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/missing_optional_one_locale.rs30
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/missing_required_all_locales.rs38
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/missing_required_one_locale.rs28
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/mod.rs39
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/preferences.rs630
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/simple.rs16
-rw-r--r--third_party/rust/fluent-testing/src/scenarios/structs.rs291
-rw-r--r--third_party/rust/fluent/.cargo-checksum.json1
-rw-r--r--third_party/rust/fluent/Cargo.toml34
-rw-r--r--third_party/rust/fluent/LICENSE-APACHE201
-rw-r--r--third_party/rust/fluent/LICENSE-MIT19
-rw-r--r--third_party/rust/fluent/README.md120
-rw-r--r--third_party/rust/fluent/src/lib.rs108
187 files changed, 23974 insertions, 0 deletions
diff --git a/third_party/rust/fluent-bundle/.cargo-checksum.json b/third_party/rust/fluent-bundle/.cargo-checksum.json
new file mode 100644
index 0000000000..5ad5a9d239
--- /dev/null
+++ b/third_party/rust/fluent-bundle/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"87a01e2e130c153cac13b916dba613ff4d9dde0795ebc607932d9ea9c960cf77","LICENSE-APACHE":"5db2b182453ff32ed40f7da63589c9667a3f8bd8b16b1471b152caae56f77e45","LICENSE-MIT":"49c0b000c03731d9e3970dc059ad4ca345d773681f4a612b0024435b663e0220","README.md":"444c8934a3bbec88cbf5e41d7b5fcb2d1f9c7e2c69a906f6df5fe18171942157","benches/resolver.rs":"bd46c8b710ac1898a0e69324a7ecf9aa38d577337bc5855a07ca0ad1043603a1","benches/resolver_iai.rs":"e9e940f4c09908069d474d379a0230dfc6fa44325300d72975e8f1d9ef64f6f1","examples/README.md":"99a51f7d388d2da3c5291e68de5264feaf642ba9a22f6f882c3b940c1b6429b2","src/args.rs":"51346e9ec84f2eeb4462e0e993b1bbb307585a2a40e41f6d0d745889bca56a7d","src/bundle.rs":"3da63b685acf559ee80fa489885da126f7c68405026ac065a07e559e2186a77f","src/concurrent.rs":"be77275513918809b98c554b26a65c6a9cf2a7bf52db3bbaf21ebdd34d94c651","src/entry.rs":"e1507b0e4c3e6d0d2efc5d622f4156a5156b9eeb40d9c5353cb7fdc236c38189","src/errors.rs":"a357f3a09335d31e362aa99a8d82eab4e238fdec8498141990f61ede58f4dabb","src/lib.rs":"58a0c929322f83aac41280da035b50adbcdb05d8a8376359d58c177cd9755eab","src/memoizer.rs":"922084f71f02d0532056db9b41cec4c1434001fe60215ee6f6ac8e3fd2518f12","src/message.rs":"4a3c95d3ecd016aeaa5da07e99d78de62f13aac8aa447818aabd0f63f2d143c4","src/resolver/errors.rs":"beaf41fabbfd11211cb2c3db6ca0ba26bccf75817bed05a92b980393edfb3f9f","src/resolver/expression.rs":"f18413de1a6b3ba43c062e24d58a60d63f4dad66bcec67ed55756ff5014f9347","src/resolver/inline_expression.rs":"089ad6745d0790478ea698fd530f2236c550889f9be75e245ed94bba4b883884","src/resolver/mod.rs":"d1b15ce110ea49876909412c12c4c1841052bb80f4838a934dfccd6a5264855c","src/resolver/pattern.rs":"64162a7e2ad0df82d463d14ac6a472005bba4cef4a7e73fe2a9529e811124a85","src/resolver/scope.rs":"816f51146c38affea54c6e0911e3522f998485829e619cca5f72cec05180de59","src/resource.rs":"cd56a14c01da2a689a408f0e5edd789e8557ae9327fa79788bf4ddb9c83431c7","src/types/mod.rs":"1cd65301fb32233fd241a79c6fa873edcb40a271330601360f72e2c452900509","src/types/number.rs":"2d3b403e5f545e2f4a3a16aec0bb019a4cc8e5ac0ab2db5642ba8039fbe203a8","src/types/plural.rs":"f28834e71d6970d5eb48089132f5242433b1e62b90765d85e3c76f805eecc92e"},"package":"e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd"} \ No newline at end of file
diff --git a/third_party/rust/fluent-bundle/Cargo.toml b/third_party/rust/fluent-bundle/Cargo.toml
new file mode 100644
index 0000000000..6d36ec3acd
--- /dev/null
+++ b/third_party/rust/fluent-bundle/Cargo.toml
@@ -0,0 +1,78 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+name = "fluent-bundle"
+version = "0.15.2"
+authors = ["Zibi Braniecki <gandalf@mozilla.com>", "Staś Małolepszy <stas@mozilla.com>"]
+include = ["src/**/*", "benches/*.rs", "Cargo.toml", "README.md", "LICENSE-APACHE", "LICENSE-MIT"]
+description = "A localization system designed to unleash the entire expressive power of\nnatural language translations.\n"
+homepage = "http://www.projectfluent.org"
+readme = "README.md"
+keywords = ["localization", "l10n", "i18n", "intl", "internationalization"]
+categories = ["localization", "internationalization"]
+license = "Apache-2.0/MIT"
+repository = "https://github.com/projectfluent/fluent-rs"
+
+[[bench]]
+name = "resolver"
+harness = false
+
+[[bench]]
+name = "resolver_iai"
+harness = false
+[dependencies.fluent-langneg]
+version = "0.13"
+
+[dependencies.fluent-syntax]
+version = "0.11"
+
+[dependencies.intl-memoizer]
+version = "0.5"
+
+[dependencies.intl_pluralrules]
+version = "7.0.1"
+
+[dependencies.rustc-hash]
+version = "1"
+
+[dependencies.self_cell]
+version = "0.10"
+
+[dependencies.smallvec]
+version = "1"
+
+[dependencies.unic-langid]
+version = "0.9"
+[dev-dependencies.criterion]
+version = "0.3"
+
+[dev-dependencies.iai]
+version = "0.1"
+
+[dev-dependencies.rand]
+version = "0.8"
+
+[dev-dependencies.serde]
+version = "1.0"
+features = ["derive"]
+
+[dev-dependencies.serde_yaml]
+version = "0.8"
+
+[dev-dependencies.unic-langid]
+version = "0.9"
+features = ["macros"]
+
+[features]
+all-benchmarks = []
+default = []
diff --git a/third_party/rust/fluent-bundle/LICENSE-APACHE b/third_party/rust/fluent-bundle/LICENSE-APACHE
new file mode 100644
index 0000000000..35582f166b
--- /dev/null
+++ b/third_party/rust/fluent-bundle/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017 Mozilla
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/third_party/rust/fluent-bundle/LICENSE-MIT b/third_party/rust/fluent-bundle/LICENSE-MIT
new file mode 100644
index 0000000000..5655fa311c
--- /dev/null
+++ b/third_party/rust/fluent-bundle/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright 2017 Mozilla
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/rust/fluent-bundle/README.md b/third_party/rust/fluent-bundle/README.md
new file mode 100644
index 0000000000..c8e1b1124a
--- /dev/null
+++ b/third_party/rust/fluent-bundle/README.md
@@ -0,0 +1,111 @@
+# Fluent
+
+`fluent-rs` is a Rust implementation of [Project Fluent][], a localization
+framework designed to unleash the entire expressive power of natural language
+translations.
+
+[![crates.io](https://img.shields.io/crates/v/fluent-bundle.svg)](https://crates.io/crates/fluent-bundle)
+[![Build and test](https://github.com/projectfluent/fluent-rs/workflows/Build%20and%20test/badge.svg)](https://github.com/projectfluent/fluent-rs/actions?query=branch%3Amaster+workflow%3A%22Build+and+test%22)
+[![Coverage Status](https://coveralls.io/repos/github/projectfluent/fluent-rs/badge.svg?branch=master)](https://coveralls.io/github/projectfluent/fluent-rs?branch=master)
+
+Project Fluent keeps simple things simple and makes complex things possible.
+The syntax used for describing translations is easy to read and understand. At
+the same time it allows, when necessary, to represent complex concepts from
+natural languages like gender, plurals, conjugations, and others.
+
+[Documentation][]
+
+[Project Fluent]: http://projectfluent.org
+[Documentation]: https://docs.rs/fluent/
+
+Usage
+-----
+
+```rust
+use fluent_bundle::{FluentBundle, FluentResource};
+use unic_langid::langid;
+
+fn main() {
+ let ftl_string = "hello-world = Hello, world!".to_owned();
+ let res = FluentResource::try_new(ftl_string)
+ .expect("Could not parse an FTL string.");
+
+ let langid_en = langid!("en");
+ let mut bundle = FluentBundle::new(vec![langid_en]);
+
+ bundle.add_resource(&res)
+ .expect("Failed to add FTL resources to the bundle.");
+
+ let msg = bundle.get_message("hello-world")
+ .expect("Failed to retrieve a message.");
+ let val = msg.value.expect("Message has no value.");
+
+ let mut errors = vec![];
+ let value = bundle.format_pattern(val, None, &mut errors);
+
+ assert_eq!(&value, "Hello, world!");
+}
+```
+
+
+Status
+------
+
+The implementation is in its early stages and supports only some of the Project
+Fluent's spec. Consult the [list of milestones][] for more information about
+release planning and scope.
+
+[list of milestones]: https://github.com/projectfluent/fluent-rs/milestones
+
+
+Local Development
+-----------------
+
+ cargo build
+ cargo test
+ cargo bench
+ cargo run --example simple-app
+
+When submitting a PR please use [`cargo fmt`][] (nightly).
+
+[`cargo fmt`]: https://github.com/rust-lang-nursery/rustfmt
+
+
+Learn the FTL syntax
+--------------------
+
+FTL is a localization file format used for describing translation resources.
+FTL stands for _Fluent Translation List_.
+
+FTL is designed to be simple to read, but at the same time allows to represent
+complex concepts from natural languages like gender, plurals, conjugations, and
+others.
+
+ hello-user = Hello, { $username }!
+
+[Read the Fluent Syntax Guide][] in order to learn more about the syntax. If
+you're a tool author you may be interested in the formal [EBNF grammar][].
+
+[Read the Fluent Syntax Guide]: http://projectfluent.org/fluent/guide/
+[EBNF grammar]: https://github.com/projectfluent/fluent/tree/master/spec
+
+
+Get Involved
+------------
+
+`fluent-rs` is open-source, licensed under the Apache License, Version 2.0. We
+encourage everyone to take a look at our code and we'll listen to your
+feedback.
+
+
+Discuss
+-------
+
+We'd love to hear your thoughts on Project Fluent! Whether you're a localizer
+looking for a better way to express yourself in your language, or a developer
+trying to make your app localizable and multilingual, or a hacker looking for
+a project to contribute to, please do get in touch on the mailing list and the
+IRC channel.
+
+ - Discourse: https://discourse.mozilla.org/c/fluent
+ - IRC channel: [irc://irc.mozilla.org/l20n](irc://irc.mozilla.org/l20n)
diff --git a/third_party/rust/fluent-bundle/benches/resolver.rs b/third_party/rust/fluent-bundle/benches/resolver.rs
new file mode 100644
index 0000000000..2116d1b91e
--- /dev/null
+++ b/third_party/rust/fluent-bundle/benches/resolver.rs
@@ -0,0 +1,168 @@
+use criterion::criterion_group;
+use criterion::criterion_main;
+use criterion::BenchmarkId;
+use criterion::Criterion;
+use std::collections::HashMap;
+use std::fs::File;
+use std::io;
+use std::io::Read;
+use std::rc::Rc;
+
+use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
+use fluent_syntax::ast;
+use unic_langid::langid;
+
+fn read_file(path: &str) -> Result<String, io::Error> {
+ let mut f = File::open(path)?;
+ let mut s = String::new();
+ f.read_to_string(&mut s)?;
+ Ok(s)
+}
+
+fn get_strings(tests: &[&'static str]) -> HashMap<&'static str, String> {
+ let mut ftl_strings = HashMap::new();
+ for test in tests {
+ let path = format!("./benches/{}.ftl", test);
+ ftl_strings.insert(*test, read_file(&path).expect("Couldn't load file"));
+ }
+ return ftl_strings;
+}
+
+fn get_ids(res: &FluentResource) -> Vec<String> {
+ res.entries()
+ .filter_map(|entry| match entry {
+ ast::Entry::Message(ast::Message { id, .. }) => Some(id.name.to_owned()),
+ _ => None,
+ })
+ .collect()
+}
+
+fn get_args(name: &str) -> Option<FluentArgs> {
+ match name {
+ "preferences" => {
+ let mut prefs_args = FluentArgs::new();
+ prefs_args.set("name", FluentValue::from("John"));
+ prefs_args.set("tabCount", FluentValue::from(5));
+ prefs_args.set("count", FluentValue::from(3));
+ prefs_args.set("version", FluentValue::from("65.0"));
+ prefs_args.set("path", FluentValue::from("/tmp"));
+ prefs_args.set("num", FluentValue::from(4));
+ prefs_args.set("email", FluentValue::from("john@doe.com"));
+ prefs_args.set("value", FluentValue::from(4.5));
+ prefs_args.set("unit", FluentValue::from("mb"));
+ prefs_args.set("service-name", FluentValue::from("Mozilla Disk"));
+ Some(prefs_args)
+ }
+ _ => None,
+ }
+}
+
+fn add_functions<R>(name: &'static str, bundle: &mut FluentBundle<R>) {
+ match name {
+ "preferences" => {
+ bundle
+ .add_function("PLATFORM", |_args, _named_args| {
+ return "linux".into();
+ })
+ .expect("Failed to add a function to the bundle.");
+ }
+ _ => {}
+ }
+}
+
+fn get_bundle(name: &'static str, source: &str) -> (FluentBundle<FluentResource>, Vec<String>) {
+ let res = FluentResource::try_new(source.to_owned()).expect("Couldn't parse an FTL source");
+ let ids = get_ids(&res);
+ let lids = vec![langid!("en")];
+ let mut bundle = FluentBundle::new(lids);
+ bundle
+ .add_resource(res)
+ .expect("Couldn't add FluentResource to the FluentBundle");
+ add_functions(name, &mut bundle);
+ (bundle, ids)
+}
+
+fn resolver_bench(c: &mut Criterion) {
+ let tests = &[
+ #[cfg(feature = "all-benchmarks")]
+ "simple",
+ "preferences",
+ #[cfg(feature = "all-benchmarks")]
+ "menubar",
+ #[cfg(feature = "all-benchmarks")]
+ "unescape",
+ ];
+ let ftl_strings = get_strings(tests);
+
+ let mut group = c.benchmark_group("construct");
+ for name in tests {
+ let source = ftl_strings.get(name).expect("Failed to find the source.");
+ group.bench_with_input(BenchmarkId::from_parameter(name), &source, |b, source| {
+ let res = Rc::new(
+ FluentResource::try_new(source.to_string()).expect("Couldn't parse an FTL source"),
+ );
+ b.iter(|| {
+ let lids = vec![langid!("en")];
+ let mut bundle = FluentBundle::new(lids);
+ bundle
+ .add_resource(res.clone())
+ .expect("Couldn't add FluentResource to the FluentBundle");
+ add_functions(name, &mut bundle);
+ })
+ });
+ }
+ group.finish();
+
+ let mut group = c.benchmark_group("resolve");
+ for name in tests {
+ let source = ftl_strings.get(name).expect("Failed to find the source.");
+ group.bench_with_input(BenchmarkId::from_parameter(name), &source, |b, source| {
+ let (bundle, ids) = get_bundle(name, source);
+ let args = get_args(name);
+ b.iter(|| {
+ let mut s = String::new();
+ for id in &ids {
+ let msg = bundle.get_message(id).expect("Message found");
+ let mut errors = vec![];
+ if let Some(value) = msg.value() {
+ let _ = bundle.write_pattern(&mut s, value, args.as_ref(), &mut errors);
+ s.clear();
+ }
+ for attr in msg.attributes() {
+ let _ =
+ bundle.write_pattern(&mut s, attr.value(), args.as_ref(), &mut errors);
+ s.clear();
+ }
+ assert!(errors.len() == 0, "Resolver errors: {:#?}", errors);
+ }
+ })
+ });
+ }
+ group.finish();
+
+ let mut group = c.benchmark_group("resolve_to_str");
+ for name in tests {
+ let source = ftl_strings.get(name).expect("Failed to find the source.");
+ group.bench_with_input(BenchmarkId::from_parameter(name), &source, |b, source| {
+ let (bundle, ids) = get_bundle(name, source);
+ let args = get_args(name);
+ b.iter(|| {
+ for id in &ids {
+ let msg = bundle.get_message(id).expect("Message found");
+ let mut errors = vec![];
+ if let Some(value) = msg.value() {
+ let _ = bundle.format_pattern(value, args.as_ref(), &mut errors);
+ }
+ for attr in msg.attributes() {
+ let _ = bundle.format_pattern(attr.value(), args.as_ref(), &mut errors);
+ }
+ assert!(errors.len() == 0, "Resolver errors: {:#?}", errors);
+ }
+ })
+ });
+ }
+ group.finish();
+}
+
+criterion_group!(benches, resolver_bench);
+criterion_main!(benches);
diff --git a/third_party/rust/fluent-bundle/benches/resolver_iai.rs b/third_party/rust/fluent-bundle/benches/resolver_iai.rs
new file mode 100644
index 0000000000..10212f6f39
--- /dev/null
+++ b/third_party/rust/fluent-bundle/benches/resolver_iai.rs
@@ -0,0 +1,79 @@
+use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
+use fluent_syntax::ast;
+use unic_langid::{langid, LanguageIdentifier};
+
+const LANG_EN: LanguageIdentifier = langid!("en");
+
+fn add_functions<R>(name: &'static str, bundle: &mut FluentBundle<R>) {
+ match name {
+ "preferences" => {
+ bundle
+ .add_function("PLATFORM", |_args, _named_args| {
+ return "linux".into();
+ })
+ .expect("Failed to add a function to the bundle.");
+ }
+ _ => {}
+ }
+}
+
+fn get_args(name: &str) -> Option<FluentArgs> {
+ match name {
+ "preferences" => {
+ let mut prefs_args = FluentArgs::new();
+ prefs_args.set("name", FluentValue::from("John"));
+ prefs_args.set("tabCount", FluentValue::from(5));
+ prefs_args.set("count", FluentValue::from(3));
+ prefs_args.set("version", FluentValue::from("65.0"));
+ prefs_args.set("path", FluentValue::from("/tmp"));
+ prefs_args.set("num", FluentValue::from(4));
+ prefs_args.set("email", FluentValue::from("john@doe.com"));
+ prefs_args.set("value", FluentValue::from(4.5));
+ prefs_args.set("unit", FluentValue::from("mb"));
+ prefs_args.set("service-name", FluentValue::from("Mozilla Disk"));
+ Some(prefs_args)
+ }
+ _ => None,
+ }
+}
+
+fn get_ids(res: &FluentResource) -> Vec<String> {
+ res.entries()
+ .filter_map(|entry| match entry {
+ ast::Entry::Message(ast::Message { id, .. }) => Some(id.name.to_owned()),
+ _ => None,
+ })
+ .collect()
+}
+
+fn iai_resolve_preferences() {
+ let files = &[include_str!("preferences.ftl")];
+ for source in files {
+ let res = FluentResource::try_new(source.to_string()).expect("failed to parse FTL.");
+ let ids = get_ids(&res);
+ let mut bundle = FluentBundle::new(vec![LANG_EN]);
+ bundle.add_resource(res).expect("Failed to add a resource.");
+ add_functions("preferences", &mut bundle);
+ let args = get_args("preferences");
+ let mut s = String::new();
+ for id in &ids {
+ let msg = bundle.get_message(id).expect("Message found");
+ let mut errors = vec![];
+ if let Some(value) = msg.value() {
+ bundle
+ .write_pattern(&mut s, value, args.as_ref(), &mut errors)
+ .expect("Failed to write a pattern.");
+ s.clear();
+ }
+ for attr in msg.attributes() {
+ bundle
+ .write_pattern(&mut s, attr.value(), args.as_ref(), &mut errors)
+ .expect("Failed to write a pattern.");
+ s.clear();
+ }
+ assert!(errors.len() == 0, "Resolver errors: {:#?}", errors);
+ }
+ }
+}
+
+iai::main!(iai_resolve_preferences);
diff --git a/third_party/rust/fluent-bundle/examples/README.md b/third_party/rust/fluent-bundle/examples/README.md
new file mode 100644
index 0000000000..2411aeb903
--- /dev/null
+++ b/third_party/rust/fluent-bundle/examples/README.md
@@ -0,0 +1,6 @@
+This directory contains a set of examples
+of how to use Fluent.
+
+Start with the `simple-app.rs` which is a very
+trivial example of a command line application
+with localization handled by Fluent.
diff --git a/third_party/rust/fluent-bundle/src/args.rs b/third_party/rust/fluent-bundle/src/args.rs
new file mode 100644
index 0000000000..b2d17a84b6
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/args.rs
@@ -0,0 +1,120 @@
+use std::borrow::Cow;
+use std::iter::FromIterator;
+
+use crate::types::FluentValue;
+
+/// A map of arguments passed from the code to
+/// the localization to be used for message
+/// formatting.
+///
+/// # Example
+///
+/// ```
+/// use fluent_bundle::{FluentArgs, FluentBundle, FluentResource};
+///
+/// let mut args = FluentArgs::new();
+/// args.set("user", "John");
+/// args.set("emailCount", 5);
+///
+/// let res = FluentResource::try_new(r#"
+///
+/// msg-key = Hello, { $user }. You have { $emailCount } messages.
+///
+/// "#.to_string())
+/// .expect("Failed to parse FTL.");
+///
+/// let mut bundle = FluentBundle::default();
+///
+/// // For this example, we'll turn on BiDi support.
+/// // Please, be careful when doing it, it's a risky move.
+/// bundle.set_use_isolating(false);
+///
+/// bundle.add_resource(res)
+/// .expect("Failed to add a resource.");
+///
+/// let mut err = vec![];
+///
+/// let msg = bundle.get_message("msg-key")
+/// .expect("Failed to retrieve a message.");
+/// let value = msg.value()
+/// .expect("Failed to retrieve a value.");
+///
+/// assert_eq!(
+/// bundle.format_pattern(value, Some(&args), &mut err),
+/// "Hello, John. You have 5 messages."
+/// );
+/// ```
+#[derive(Debug, Default)]
+pub struct FluentArgs<'args>(Vec<(Cow<'args, str>, FluentValue<'args>)>);
+
+impl<'args> FluentArgs<'args> {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self(Vec::with_capacity(capacity))
+ }
+
+ pub fn get<K>(&self, key: K) -> Option<&FluentValue<'args>>
+ where
+ K: Into<Cow<'args, str>>,
+ {
+ let key = key.into();
+ if let Ok(idx) = self.0.binary_search_by_key(&&key, |(k, _)| k) {
+ Some(&self.0[idx].1)
+ } else {
+ None
+ }
+ }
+
+ pub fn set<K, V>(&mut self, key: K, value: V)
+ where
+ K: Into<Cow<'args, str>>,
+ V: Into<FluentValue<'args>>,
+ {
+ let key = key.into();
+ let idx = match self.0.binary_search_by_key(&&key, |(k, _)| k) {
+ Ok(idx) => idx,
+ Err(idx) => idx,
+ };
+ self.0.insert(idx, (key, value.into()));
+ }
+
+ pub fn iter(&self) -> impl Iterator<Item = (&str, &FluentValue)> {
+ self.0.iter().map(|(k, v)| (k.as_ref(), v))
+ }
+}
+
+impl<'args, K, V> FromIterator<(K, V)> for FluentArgs<'args>
+where
+ K: Into<Cow<'args, str>>,
+ V: Into<FluentValue<'args>>,
+{
+ fn from_iter<I>(iter: I) -> Self
+ where
+ I: IntoIterator<Item = (K, V)>,
+ {
+ let iter = iter.into_iter();
+ let mut args = if let Some(size) = iter.size_hint().1 {
+ FluentArgs::with_capacity(size)
+ } else {
+ FluentArgs::new()
+ };
+
+ for (k, v) in iter {
+ args.set(k, v);
+ }
+
+ args
+ }
+}
+
+impl<'args> IntoIterator for FluentArgs<'args> {
+ type Item = (Cow<'args, str>, FluentValue<'args>);
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.into_iter()
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/bundle.rs b/third_party/rust/fluent-bundle/src/bundle.rs
new file mode 100644
index 0000000000..3d085cfee5
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/bundle.rs
@@ -0,0 +1,615 @@
+//! `FluentBundle` is a collection of localization messages in Fluent.
+//!
+//! It stores a list of messages in a single locale which can reference one another, use the same
+//! internationalization formatters, functions, scopeironmental variables and are expected to be used
+//! together.
+
+use rustc_hash::FxHashMap;
+use std::borrow::Borrow;
+use std::borrow::Cow;
+use std::collections::hash_map::Entry as HashEntry;
+use std::default::Default;
+use std::fmt;
+
+use fluent_syntax::ast;
+use intl_memoizer::IntlLangMemoizer;
+use unic_langid::LanguageIdentifier;
+
+use crate::args::FluentArgs;
+use crate::entry::Entry;
+use crate::entry::GetEntry;
+use crate::errors::{EntryKind, FluentError};
+use crate::memoizer::MemoizerKind;
+use crate::message::FluentMessage;
+use crate::resolver::{ResolveValue, Scope, WriteValue};
+use crate::resource::FluentResource;
+use crate::types::FluentValue;
+
+/// A collection of localization messages for a single locale, which are meant
+/// to be used together in a single view, widget or any other UI abstraction.
+///
+/// # Examples
+///
+/// ```
+/// use fluent_bundle::{FluentBundle, FluentResource, FluentValue, FluentArgs};
+/// use unic_langid::langid;
+///
+/// // 1. Create a FluentResource
+///
+/// let ftl_string = String::from("intro = Welcome, { $name }.");
+/// let resource = FluentResource::try_new(ftl_string)
+/// .expect("Could not parse an FTL string.");
+///
+///
+/// // 2. Create a FluentBundle
+///
+/// let langid_en = langid!("en-US");
+/// let mut bundle = FluentBundle::new(vec![langid_en]);
+///
+///
+/// // 3. Add the resource to the bundle
+///
+/// bundle.add_resource(&resource)
+/// .expect("Failed to add FTL resources to the bundle.");
+///
+///
+/// // 4. Retrieve a FluentMessage from the bundle
+///
+/// let msg = bundle.get_message("intro")
+/// .expect("Message doesn't exist.");
+///
+/// let mut args = FluentArgs::new();
+/// args.set("name", "Rustacean");
+///
+///
+/// // 5. Format the value of the message
+///
+/// let mut errors = vec![];
+///
+/// let pattern = msg.value()
+/// .expect("Message has no value.");
+///
+/// assert_eq!(
+/// bundle.format_pattern(&pattern, Some(&args), &mut errors),
+/// // The placeholder is wrapper in Unicode Directionality Marks
+/// // to indicate that the placeholder may be of different direction
+/// // than surrounding string.
+/// "Welcome, \u{2068}Rustacean\u{2069}."
+/// );
+///
+/// ```
+///
+/// # `FluentBundle` Life Cycle
+///
+/// ## Create a bundle
+///
+/// To create a bundle, call [`FluentBundle::new`] with a locale list that represents the best
+/// possible fallback chain for a given locale. The simplest case is a one-locale list.
+///
+/// Fluent uses [`LanguageIdentifier`] which can be created using `langid!` macro.
+///
+/// ## Add Resources
+///
+/// Next, call [`add_resource`](FluentBundle::add_resource) one or more times, supplying translations in the FTL syntax.
+///
+/// Since [`FluentBundle`] is generic over anything that can borrow a [`FluentResource`],
+/// one can use [`FluentBundle`] to own its resources, store references to them,
+/// or even [`Rc<FluentResource>`](std::rc::Rc) or [`Arc<FluentResource>`](std::sync::Arc).
+///
+/// The [`FluentBundle`] instance is now ready to be used for localization.
+///
+/// ## Format
+///
+/// To format a translation, call [`get_message`](FluentBundle::get_message) to retrieve a [`FluentMessage`],
+/// and then call [`format_pattern`](FluentBundle::format_pattern) on the message value or attribute in order to
+/// retrieve the translated string.
+///
+/// The result of [`format_pattern`](FluentBundle::format_pattern) is an
+/// [`Cow<str>`](std::borrow::Cow). It is
+/// recommended to treat the result as opaque from the perspective of the program and use it only
+/// to display localized messages. Do not examine it or alter in any way before displaying. This
+/// is a general good practice as far as all internationalization operations are concerned.
+///
+/// If errors were encountered during formatting, they will be
+/// accumulated in the [`Vec<FluentError>`](FluentError) passed as the third argument.
+///
+/// While they are not fatal, they usually indicate problems with the translation,
+/// and should be logged or reported in a way that allows the developer to notice
+/// and fix them.
+///
+///
+/// # Locale Fallback Chain
+///
+/// [`FluentBundle`] stores messages in a single locale, but keeps a locale fallback chain for the
+/// purpose of language negotiation with i18n formatters. For instance, if date and time formatting
+/// are not available in the first locale, [`FluentBundle`] will use its `locales` fallback chain
+/// to negotiate a sensible fallback for date and time formatting.
+///
+/// # Concurrency
+///
+/// As you may have noticed, [`fluent_bundle::FluentBundle`](crate::FluentBundle) is a specialization of [`fluent_bundle::bundle::FluentBundle`](crate::bundle::FluentBundle)
+/// which works with an [`IntlLangMemoizer`] over [`RefCell`](std::cell::RefCell).
+/// In scenarios where the memoizer must work concurrently, there's an implementation of
+/// [`IntlLangMemoizer`](intl_memoizer::concurrent::IntlLangMemoizer) that uses [`Mutex`](std::sync::Mutex) and there's [`FluentBundle::new_concurrent`] which works with that.
+pub struct FluentBundle<R, M> {
+ pub locales: Vec<LanguageIdentifier>,
+ pub(crate) resources: Vec<R>,
+ pub(crate) entries: FxHashMap<String, Entry>,
+ pub(crate) intls: M,
+ pub(crate) use_isolating: bool,
+ pub(crate) transform: Option<fn(&str) -> Cow<str>>,
+ pub(crate) formatter: Option<fn(&FluentValue, &M) -> Option<String>>,
+}
+
+impl<R, M> FluentBundle<R, M> {
+ /// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
+ ///
+ /// If any entry in the resource uses the same identifier as an already
+ /// existing key in the bundle, the new entry will be ignored and a
+ /// `FluentError::Overriding` will be added to the result.
+ ///
+ /// The method can take any type that can be borrowed to `FluentResource`:
+ /// - FluentResource
+ /// - &FluentResource
+ /// - Rc<FluentResource>
+ /// - Arc<FluentResurce>
+ ///
+ /// This allows the user to introduce custom resource management and share
+ /// resources between instances of `FluentBundle`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use fluent_bundle::{FluentBundle, FluentResource};
+ /// use unic_langid::langid;
+ ///
+ /// let ftl_string = String::from("
+ /// hello = Hi!
+ /// goodbye = Bye!
+ /// ");
+ /// let resource = FluentResource::try_new(ftl_string)
+ /// .expect("Could not parse an FTL string.");
+ /// let langid_en = langid!("en-US");
+ /// let mut bundle = FluentBundle::new(vec![langid_en]);
+ /// bundle.add_resource(resource)
+ /// .expect("Failed to add FTL resources to the bundle.");
+ /// assert_eq!(true, bundle.has_message("hello"));
+ /// ```
+ ///
+ /// # Whitespace
+ ///
+ /// Message ids must have no leading whitespace. Message values that span
+ /// multiple lines must have leading whitespace on all but the first line. These
+ /// are standard FTL syntax rules that may prove a bit troublesome in source
+ /// code formatting. The [`indoc!`] crate can help with stripping extra indentation
+ /// if you wish to indent your entire message.
+ ///
+ /// [FTL syntax]: https://projectfluent.org/fluent/guide/
+ /// [`indoc!`]: https://github.com/dtolnay/indoc
+ /// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
+ pub fn add_resource(&mut self, r: R) -> Result<(), Vec<FluentError>>
+ where
+ R: Borrow<FluentResource>,
+ {
+ let mut errors = vec![];
+
+ let res = r.borrow();
+ let res_pos = self.resources.len();
+
+ for (entry_pos, entry) in res.entries().enumerate() {
+ let (id, entry) = match entry {
+ ast::Entry::Message(ast::Message { ref id, .. }) => {
+ (id.name, Entry::Message((res_pos, entry_pos)))
+ }
+ ast::Entry::Term(ast::Term { ref id, .. }) => {
+ (id.name, Entry::Term((res_pos, entry_pos)))
+ }
+ _ => continue,
+ };
+
+ match self.entries.entry(id.to_string()) {
+ HashEntry::Vacant(empty) => {
+ empty.insert(entry);
+ }
+ HashEntry::Occupied(_) => {
+ let kind = match entry {
+ Entry::Message(..) => EntryKind::Message,
+ Entry::Term(..) => EntryKind::Term,
+ _ => unreachable!(),
+ };
+ errors.push(FluentError::Overriding {
+ kind,
+ id: id.to_string(),
+ });
+ }
+ }
+ }
+ self.resources.push(r);
+
+ if errors.is_empty() {
+ Ok(())
+ } else {
+ Err(errors)
+ }
+ }
+
+ /// Adds a resource to the bundle, returning an empty [`Result<T>`] on success.
+ ///
+ /// If any entry in the resource uses the same identifier as an already
+ /// existing key in the bundle, the entry will override the previous one.
+ ///
+ /// The method can take any type that can be borrowed as FluentResource:
+ /// - FluentResource
+ /// - &FluentResource
+ /// - Rc<FluentResource>
+ /// - Arc<FluentResurce>
+ ///
+ /// This allows the user to introduce custom resource management and share
+ /// resources between instances of `FluentBundle`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use fluent_bundle::{FluentBundle, FluentResource};
+ /// use unic_langid::langid;
+ ///
+ /// let ftl_string = String::from("
+ /// hello = Hi!
+ /// goodbye = Bye!
+ /// ");
+ /// let resource = FluentResource::try_new(ftl_string)
+ /// .expect("Could not parse an FTL string.");
+ ///
+ /// let ftl_string = String::from("
+ /// hello = Another Hi!
+ /// ");
+ /// let resource2 = FluentResource::try_new(ftl_string)
+ /// .expect("Could not parse an FTL string.");
+ ///
+ /// let langid_en = langid!("en-US");
+ ///
+ /// let mut bundle = FluentBundle::new(vec![langid_en]);
+ /// bundle.add_resource(resource)
+ /// .expect("Failed to add FTL resources to the bundle.");
+ ///
+ /// bundle.add_resource_overriding(resource2);
+ ///
+ /// let mut errors = vec![];
+ /// let msg = bundle.get_message("hello")
+ /// .expect("Failed to retrieve the message");
+ /// let value = msg.value().expect("Failed to retrieve the value of the message");
+ /// assert_eq!(bundle.format_pattern(value, None, &mut errors), "Another Hi!");
+ /// ```
+ ///
+ /// # Whitespace
+ ///
+ /// Message ids must have no leading whitespace. Message values that span
+ /// multiple lines must have leading whitespace on all but the first line. These
+ /// are standard FTL syntax rules that may prove a bit troublesome in source
+ /// code formatting. The [`indoc!`] crate can help with stripping extra indentation
+ /// if you wish to indent your entire message.
+ ///
+ /// [FTL syntax]: https://projectfluent.org/fluent/guide/
+ /// [`indoc!`]: https://github.com/dtolnay/indoc
+ /// [`Result<T>`]: https://doc.rust-lang.org/std/result/enum.Result.html
+ pub fn add_resource_overriding(&mut self, r: R)
+ where
+ R: Borrow<FluentResource>,
+ {
+ let res = r.borrow();
+ let res_pos = self.resources.len();
+
+ for (entry_pos, entry) in res.entries().enumerate() {
+ let (id, entry) = match entry {
+ ast::Entry::Message(ast::Message { ref id, .. }) => {
+ (id.name, Entry::Message((res_pos, entry_pos)))
+ }
+ ast::Entry::Term(ast::Term { ref id, .. }) => {
+ (id.name, Entry::Term((res_pos, entry_pos)))
+ }
+ _ => continue,
+ };
+
+ self.entries.insert(id.to_string(), entry);
+ }
+ self.resources.push(r);
+ }
+
+ /// When formatting patterns, `FluentBundle` inserts
+ /// Unicode Directionality Isolation Marks to indicate
+ /// that the direction of a placeable may differ from
+ /// the surrounding message.
+ ///
+ /// This is important for cases such as when a
+ /// right-to-left user name is presented in the
+ /// left-to-right message.
+ ///
+ /// In some cases, such as testing, the user may want
+ /// to disable the isolating.
+ pub fn set_use_isolating(&mut self, value: bool) {
+ self.use_isolating = value;
+ }
+
+ /// This method allows to specify a function that will
+ /// be called on all textual fragments of the pattern
+ /// during formatting.
+ ///
+ /// This is currently primarly used for pseudolocalization,
+ /// and `fluent-pseudo` crate provides a function
+ /// that can be passed here.
+ pub fn set_transform(&mut self, func: Option<fn(&str) -> Cow<str>>) {
+ self.transform = func;
+ }
+
+ /// This method allows to specify a function that will
+ /// be called before any `FluentValue` is formatted
+ /// allowing overrides.
+ ///
+ /// It's particularly useful for plugging in an external
+ /// formatter for `FluentValue::Number`.
+ pub fn set_formatter(&mut self, func: Option<fn(&FluentValue, &M) -> Option<String>>) {
+ self.formatter = func;
+ }
+
+ /// Returns true if this bundle contains a message with the given id.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use fluent_bundle::{FluentBundle, FluentResource};
+ /// use unic_langid::langid;
+ ///
+ /// let ftl_string = String::from("hello = Hi!");
+ /// let resource = FluentResource::try_new(ftl_string)
+ /// .expect("Failed to parse an FTL string.");
+ /// let langid_en = langid!("en-US");
+ /// let mut bundle = FluentBundle::new(vec![langid_en]);
+ /// bundle.add_resource(&resource)
+ /// .expect("Failed to add FTL resources to the bundle.");
+ /// assert_eq!(true, bundle.has_message("hello"));
+ ///
+ /// ```
+ pub fn has_message(&self, id: &str) -> bool
+ where
+ R: Borrow<FluentResource>,
+ {
+ self.get_entry_message(id).is_some()
+ }
+
+ /// Retrieves a `FluentMessage` from a bundle.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use fluent_bundle::{FluentBundle, FluentResource};
+ /// use unic_langid::langid;
+ ///
+ /// let ftl_string = String::from("hello-world = Hello World!");
+ /// let resource = FluentResource::try_new(ftl_string)
+ /// .expect("Failed to parse an FTL string.");
+ ///
+ /// let langid_en = langid!("en-US");
+ /// let mut bundle = FluentBundle::new(vec![langid_en]);
+ ///
+ /// bundle.add_resource(&resource)
+ /// .expect("Failed to add FTL resources to the bundle.");
+ ///
+ /// let msg = bundle.get_message("hello-world");
+ /// assert_eq!(msg.is_some(), true);
+ /// ```
+ pub fn get_message<'l>(&'l self, id: &str) -> Option<FluentMessage<'l>>
+ where
+ R: Borrow<FluentResource>,
+ {
+ self.get_entry_message(id).map(Into::into)
+ }
+
+ /// Writes a formatted pattern which comes from a `FluentMessage`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_bundle::{FluentBundle, FluentResource};
+ /// use unic_langid::langid;
+ ///
+ /// let ftl_string = String::from("hello-world = Hello World!");
+ /// let resource = FluentResource::try_new(ftl_string)
+ /// .expect("Failed to parse an FTL string.");
+ ///
+ /// let langid_en = langid!("en-US");
+ /// let mut bundle = FluentBundle::new(vec![langid_en]);
+ ///
+ /// bundle.add_resource(&resource)
+ /// .expect("Failed to add FTL resources to the bundle.");
+ ///
+ /// let msg = bundle.get_message("hello-world")
+ /// .expect("Failed to retrieve a FluentMessage.");
+ ///
+ /// let pattern = msg.value()
+ /// .expect("Missing Value.");
+ /// let mut errors = vec![];
+ ///
+ /// let mut s = String::new();
+ /// bundle.write_pattern(&mut s, &pattern, None, &mut errors)
+ /// .expect("Failed to write.");
+ ///
+ /// assert_eq!(s, "Hello World!");
+ /// ```
+ pub fn write_pattern<'bundle, W>(
+ &'bundle self,
+ w: &mut W,
+ pattern: &'bundle ast::Pattern<&str>,
+ args: Option<&'bundle FluentArgs>,
+ errors: &mut Vec<FluentError>,
+ ) -> fmt::Result
+ where
+ R: Borrow<FluentResource>,
+ W: fmt::Write,
+ M: MemoizerKind,
+ {
+ let mut scope = Scope::new(self, args, Some(errors));
+ pattern.write(w, &mut scope)
+ }
+
+ /// Formats a pattern which comes from a `FluentMessage`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_bundle::{FluentBundle, FluentResource};
+ /// use unic_langid::langid;
+ ///
+ /// let ftl_string = String::from("hello-world = Hello World!");
+ /// let resource = FluentResource::try_new(ftl_string)
+ /// .expect("Failed to parse an FTL string.");
+ ///
+ /// let langid_en = langid!("en-US");
+ /// let mut bundle = FluentBundle::new(vec![langid_en]);
+ ///
+ /// bundle.add_resource(&resource)
+ /// .expect("Failed to add FTL resources to the bundle.");
+ ///
+ /// let msg = bundle.get_message("hello-world")
+ /// .expect("Failed to retrieve a FluentMessage.");
+ ///
+ /// let pattern = msg.value()
+ /// .expect("Missing Value.");
+ /// let mut errors = vec![];
+ ///
+ /// let result = bundle.format_pattern(&pattern, None, &mut errors);
+ ///
+ /// assert_eq!(result, "Hello World!");
+ /// ```
+ pub fn format_pattern<'bundle>(
+ &'bundle self,
+ pattern: &'bundle ast::Pattern<&str>,
+ args: Option<&'bundle FluentArgs>,
+ errors: &mut Vec<FluentError>,
+ ) -> Cow<'bundle, str>
+ where
+ R: Borrow<FluentResource>,
+ M: MemoizerKind,
+ {
+ let mut scope = Scope::new(self, args, Some(errors));
+ let value = pattern.resolve(&mut scope);
+ value.as_string(&scope)
+ }
+
+ /// Makes the provided rust function available to messages with the name `id`. See
+ /// the [FTL syntax guide] to learn how these are used in messages.
+ ///
+ /// FTL functions accept both positional and named args. The rust function you
+ /// provide therefore has two parameters: a slice of values for the positional
+ /// args, and a `FluentArgs` for named args.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
+ /// use unic_langid::langid;
+ ///
+ /// let ftl_string = String::from("length = { STRLEN(\"12345\") }");
+ /// let resource = FluentResource::try_new(ftl_string)
+ /// .expect("Could not parse an FTL string.");
+ /// let langid_en = langid!("en-US");
+ /// let mut bundle = FluentBundle::new(vec![langid_en]);
+ /// bundle.add_resource(&resource)
+ /// .expect("Failed to add FTL resources to the bundle.");
+ ///
+ /// // Register a fn that maps from string to string length
+ /// bundle.add_function("STRLEN", |positional, _named| match positional {
+ /// [FluentValue::String(str)] => str.len().into(),
+ /// _ => FluentValue::Error,
+ /// }).expect("Failed to add a function to the bundle.");
+ ///
+ /// let msg = bundle.get_message("length").expect("Message doesn't exist.");
+ /// let mut errors = vec![];
+ /// let pattern = msg.value().expect("Message has no value.");
+ /// let value = bundle.format_pattern(&pattern, None, &mut errors);
+ /// assert_eq!(&value, "5");
+ /// ```
+ ///
+ /// [FTL syntax guide]: https://projectfluent.org/fluent/guide/functions.html
+ pub fn add_function<F>(&mut self, id: &str, func: F) -> Result<(), FluentError>
+ where
+ F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static,
+ {
+ match self.entries.entry(id.to_owned()) {
+ HashEntry::Vacant(entry) => {
+ entry.insert(Entry::Function(Box::new(func)));
+ Ok(())
+ }
+ HashEntry::Occupied(_) => Err(FluentError::Overriding {
+ kind: EntryKind::Function,
+ id: id.to_owned(),
+ }),
+ }
+ }
+}
+
+impl<R> Default for FluentBundle<R, IntlLangMemoizer> {
+ fn default() -> Self {
+ Self::new(vec![LanguageIdentifier::default()])
+ }
+}
+
+impl<R> FluentBundle<R, IntlLangMemoizer> {
+ /// Constructs a FluentBundle. The first element in `locales` should be the
+ /// language this bundle represents, and will be used to determine the
+ /// correct plural rules for this bundle. You can optionally provide extra
+ /// languages in the list; they will be used as fallback date and time
+ /// formatters if a formatter for the primary language is unavailable.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use fluent_bundle::FluentBundle;
+ /// use fluent_bundle::FluentResource;
+ /// use unic_langid::langid;
+ ///
+ /// let langid_en = langid!("en-US");
+ /// let mut bundle: FluentBundle<FluentResource> = FluentBundle::new(vec![langid_en]);
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// This will panic if no formatters can be found for the locales.
+ pub fn new(locales: Vec<LanguageIdentifier>) -> Self {
+ let first_locale = locales.get(0).cloned().unwrap_or_default();
+ Self {
+ locales,
+ resources: vec![],
+ entries: FxHashMap::default(),
+ intls: IntlLangMemoizer::new(first_locale),
+ use_isolating: true,
+ transform: None,
+ formatter: None,
+ }
+ }
+}
+
+impl crate::memoizer::MemoizerKind for IntlLangMemoizer {
+ fn new(lang: LanguageIdentifier) -> Self
+ where
+ Self: Sized,
+ {
+ Self::new(lang)
+ }
+
+ fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
+ where
+ Self: Sized,
+ I: intl_memoizer::Memoizable + Send + Sync + 'static,
+ I::Args: Send + Sync + 'static,
+ U: FnOnce(&I) -> R,
+ {
+ self.with_try_get(args, cb)
+ }
+
+ fn stringify_value(
+ &self,
+ value: &dyn crate::types::FluentType,
+ ) -> std::borrow::Cow<'static, str> {
+ value.as_string(self)
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/concurrent.rs b/third_party/rust/fluent-bundle/src/concurrent.rs
new file mode 100644
index 0000000000..b87225efee
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/concurrent.rs
@@ -0,0 +1,59 @@
+use intl_memoizer::{concurrent::IntlLangMemoizer, Memoizable};
+use rustc_hash::FxHashMap;
+use unic_langid::LanguageIdentifier;
+
+use crate::bundle::FluentBundle;
+use crate::memoizer::MemoizerKind;
+use crate::types::FluentType;
+
+impl<R> FluentBundle<R, IntlLangMemoizer> {
+ /// A constructor analogous to [`FluentBundle::new`] but operating
+ /// on a concurrent version of [`IntlLangMemoizer`] over [`Mutex`](std::sync::Mutex).
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_bundle::bundle::FluentBundle;
+ /// use fluent_bundle::FluentResource;
+ /// use unic_langid::langid;
+ ///
+ /// let langid_en = langid!("en-US");
+ /// let mut bundle: FluentBundle<FluentResource, _> =
+ /// FluentBundle::new_concurrent(vec![langid_en]);
+ /// ```
+ pub fn new_concurrent(locales: Vec<LanguageIdentifier>) -> Self {
+ let first_locale = locales.get(0).cloned().unwrap_or_default();
+ Self {
+ locales,
+ resources: vec![],
+ entries: FxHashMap::default(),
+ intls: IntlLangMemoizer::new(first_locale),
+ use_isolating: true,
+ transform: None,
+ formatter: None,
+ }
+ }
+}
+
+impl MemoizerKind for IntlLangMemoizer {
+ fn new(lang: LanguageIdentifier) -> Self
+ where
+ Self: Sized,
+ {
+ Self::new(lang)
+ }
+
+ fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
+ where
+ Self: Sized,
+ I: Memoizable + Send + Sync + 'static,
+ I::Args: Send + Sync + 'static,
+ U: FnOnce(&I) -> R,
+ {
+ self.with_try_get(args, cb)
+ }
+
+ fn stringify_value(&self, value: &dyn FluentType) -> std::borrow::Cow<'static, str> {
+ value.as_string_threadsafe(self)
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/entry.rs b/third_party/rust/fluent-bundle/src/entry.rs
new file mode 100644
index 0000000000..1ac8ecf01b
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/entry.rs
@@ -0,0 +1,62 @@
+//! `Entry` is used to store Messages, Terms and Functions in `FluentBundle` instances.
+
+use std::borrow::Borrow;
+
+use fluent_syntax::ast;
+
+use crate::args::FluentArgs;
+use crate::bundle::FluentBundle;
+use crate::resource::FluentResource;
+use crate::types::FluentValue;
+
+pub type FluentFunction =
+ Box<dyn for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Send + Sync>;
+
+pub enum Entry {
+ Message((usize, usize)),
+ Term((usize, usize)),
+ Function(FluentFunction),
+}
+
+pub trait GetEntry {
+ fn get_entry_message(&self, id: &str) -> Option<&ast::Message<&str>>;
+ fn get_entry_term(&self, id: &str) -> Option<&ast::Term<&str>>;
+ fn get_entry_function(&self, id: &str) -> Option<&FluentFunction>;
+}
+
+impl<'bundle, R: Borrow<FluentResource>, M> GetEntry for FluentBundle<R, M> {
+ fn get_entry_message(&self, id: &str) -> Option<&ast::Message<&str>> {
+ self.entries.get(id).and_then(|ref entry| match entry {
+ Entry::Message(pos) => {
+ let res = self.resources.get(pos.0)?.borrow();
+ if let ast::Entry::Message(ref msg) = res.get_entry(pos.1)? {
+ Some(msg)
+ } else {
+ None
+ }
+ }
+ _ => None,
+ })
+ }
+
+ fn get_entry_term(&self, id: &str) -> Option<&ast::Term<&str>> {
+ self.entries.get(id).and_then(|ref entry| match entry {
+ Entry::Term(pos) => {
+ let res = self.resources.get(pos.0)?.borrow();
+ if let ast::Entry::Term(ref msg) = res.get_entry(pos.1)? {
+ Some(msg)
+ } else {
+ None
+ }
+ }
+ _ => None,
+ })
+ }
+
+ fn get_entry_function(&self, id: &str) -> Option<&FluentFunction> {
+ self.entries.get(id).and_then(|ref entry| match entry {
+ Entry::Function(function) => Some(function),
+ _ => None,
+ })
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/errors.rs b/third_party/rust/fluent-bundle/src/errors.rs
new file mode 100644
index 0000000000..ec4a02c4b4
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/errors.rs
@@ -0,0 +1,86 @@
+use crate::resolver::ResolverError;
+use fluent_syntax::parser::ParserError;
+use std::error::Error;
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum EntryKind {
+ Message,
+ Term,
+ Function,
+}
+
+impl std::fmt::Display for EntryKind {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Message => f.write_str("message"),
+ Self::Term => f.write_str("term"),
+ Self::Function => f.write_str("function"),
+ }
+ }
+}
+
+/// Core error type for Fluent runtime system.
+///
+/// It contains three main types of errors that may come up
+/// during runtime use of the fluent-bundle crate.
+#[derive(Debug, PartialEq, Clone)]
+pub enum FluentError {
+ /// An error which occurs when
+ /// [`FluentBundle::add_resource`](crate::bundle::FluentBundle::add_resource)
+ /// adds entries that are already registered in a given [`FluentBundle`](crate::FluentBundle).
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_bundle::{FluentBundle, FluentResource};
+ /// use unic_langid::langid;
+ ///
+ /// let ftl_string = String::from("intro = Welcome, { $name }.");
+ /// let res1 = FluentResource::try_new(ftl_string)
+ /// .expect("Could not parse an FTL string.");
+ ///
+ /// let ftl_string = String::from("intro = Hi, { $name }.");
+ /// let res2 = FluentResource::try_new(ftl_string)
+ /// .expect("Could not parse an FTL string.");
+ ///
+ /// let langid_en = langid!("en-US");
+ /// let mut bundle = FluentBundle::new(vec![langid_en]);
+ ///
+ /// bundle.add_resource(&res1)
+ /// .expect("Failed to add FTL resources to the bundle.");
+ ///
+ /// assert!(bundle.add_resource(&res2).is_err());
+ /// ```
+ Overriding {
+ kind: EntryKind,
+ id: String,
+ },
+ ParserError(ParserError),
+ ResolverError(ResolverError),
+}
+
+impl std::fmt::Display for FluentError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Overriding { kind, id } => {
+ write!(f, "Attempt to override an existing {}: \"{}\".", kind, id)
+ }
+ Self::ParserError(err) => write!(f, "Parser error: {}", err),
+ Self::ResolverError(err) => write!(f, "Resolver error: {}", err),
+ }
+ }
+}
+
+impl Error for FluentError {}
+
+impl From<ResolverError> for FluentError {
+ fn from(error: ResolverError) -> Self {
+ Self::ResolverError(error)
+ }
+}
+
+impl From<ParserError> for FluentError {
+ fn from(error: ParserError) -> Self {
+ Self::ParserError(error)
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/lib.rs b/third_party/rust/fluent-bundle/src/lib.rs
new file mode 100644
index 0000000000..faf3e9ba60
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/lib.rs
@@ -0,0 +1,127 @@
+//! Fluent is a modern localization system designed to improve how software is translated.
+//!
+//! `fluent-bundle` is a mid-level component of the [Fluent Localization
+//! System](https://www.projectfluent.org).
+//!
+//! The crate builds on top of the low level [`fluent-syntax`](../fluent-syntax) package, and provides
+//! foundational types and structures required for executing localization at runtime.
+//!
+//! There are four core concepts to understand Fluent runtime:
+//!
+//! * [`FluentMessage`] - A single translation unit
+//! * [`FluentResource`] - A list of [`FluentMessage`] units
+//! * [`FluentBundle`](crate::bundle::FluentBundle) - A collection of [`FluentResource`] lists
+//! * [`FluentArgs`] - A list of elements used to resolve a [`FluentMessage`] value
+//!
+//! ## Example
+//!
+//! ```
+//! use fluent_bundle::{FluentBundle, FluentValue, FluentResource, FluentArgs};
+//! // Used to provide a locale for the bundle.
+//! use unic_langid::langid;
+//!
+//! // 1. Crate a FluentResource
+//!
+//! let ftl_string = r#"
+//!
+//! hello-world = Hello, world!
+//! intro = Welcome, { $name }.
+//!
+//! "#.to_string();
+//!
+//! let res = FluentResource::try_new(ftl_string)
+//! .expect("Failed to parse an FTL string.");
+//!
+//!
+//! // 2. Crate a FluentBundle
+//!
+//! let langid_en = langid!("en-US");
+//! let mut bundle = FluentBundle::new(vec![langid_en]);
+//!
+//!
+//! // 3. Add the resource to the bundle
+//!
+//! bundle
+//! .add_resource(res)
+//! .expect("Failed to add FTL resources to the bundle.");
+//!
+//!
+//! // 4. Retrieve a FluentMessage from the bundle
+//!
+//! let msg = bundle.get_message("hello-world")
+//! .expect("Message doesn't exist.");
+//!
+//!
+//! // 5. Format the value of the simple message
+//!
+//! let mut errors = vec![];
+//!
+//! let pattern = msg.value()
+//! .expect("Message has no value.");
+//!
+//! let value = bundle.format_pattern(&pattern, None, &mut errors);
+//!
+//! assert_eq!(
+//! bundle.format_pattern(&pattern, None, &mut errors),
+//! "Hello, world!"
+//! );
+//!
+//! // 6. Format the value of the message with arguments
+//!
+//! let mut args = FluentArgs::new();
+//! args.set("name", "John");
+//!
+//! let msg = bundle.get_message("intro")
+//! .expect("Message doesn't exist.");
+//!
+//! let pattern = msg.value()
+//! .expect("Message has no value.");
+//!
+//! // The FSI/PDI isolation marks ensure that the direction of
+//! // the text from the variable is not affected by the translation.
+//! assert_eq!(
+//! bundle.format_pattern(&pattern, Some(&args), &mut errors),
+//! "Welcome, \u{2068}John\u{2069}."
+//! );
+//! ```
+//!
+//! # Ergonomics & Higher Level APIs
+//!
+//! Reading the example, you may notice how verbose it feels.
+//! Many core methods are fallible, others accumulate errors, and there
+//! are intermediate structures used in operations.
+//!
+//! This is intentional as it serves as building blocks for variety of different
+//! scenarios allowing implementations to handle errors, cache and
+//! optimize results.
+//!
+//! At the moment it is expected that users will use
+//! the `fluent-bundle` crate directly, while the ecosystem
+//! matures and higher level APIs are being developed.
+mod args;
+pub mod bundle;
+mod concurrent;
+mod entry;
+mod errors;
+#[doc(hidden)]
+pub mod memoizer;
+mod message;
+#[doc(hidden)]
+pub mod resolver;
+mod resource;
+pub mod types;
+
+pub use args::FluentArgs;
+/// Specialized [`FluentBundle`](crate::bundle::FluentBundle) over
+/// non-concurrent [`IntlLangMemoizer`](intl_memoizer::IntlLangMemoizer).
+///
+/// This is the basic variant of the [`FluentBundle`](crate::bundle::FluentBundle).
+///
+/// The concurrent specialization, can be constructed with
+/// [`FluentBundle::new_concurrent`](crate::bundle::FluentBundle::new_concurrent).
+pub type FluentBundle<R> = bundle::FluentBundle<R, intl_memoizer::IntlLangMemoizer>;
+pub use errors::FluentError;
+pub use message::{FluentAttribute, FluentMessage};
+pub use resource::FluentResource;
+#[doc(inline)]
+pub use types::FluentValue;
diff --git a/third_party/rust/fluent-bundle/src/memoizer.rs b/third_party/rust/fluent-bundle/src/memoizer.rs
new file mode 100644
index 0000000000..c738a857b2
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/memoizer.rs
@@ -0,0 +1,18 @@
+use crate::types::FluentType;
+use intl_memoizer::Memoizable;
+use unic_langid::LanguageIdentifier;
+
+pub trait MemoizerKind: 'static {
+ fn new(lang: LanguageIdentifier) -> Self
+ where
+ Self: Sized;
+
+ fn with_try_get_threadsafe<I, R, U>(&self, args: I::Args, cb: U) -> Result<R, I::Error>
+ where
+ Self: Sized,
+ I: Memoizable + Send + Sync + 'static,
+ I::Args: Send + Sync + 'static,
+ U: FnOnce(&I) -> R;
+
+ fn stringify_value(&self, value: &dyn FluentType) -> std::borrow::Cow<'static, str>;
+}
diff --git a/third_party/rust/fluent-bundle/src/message.rs b/third_party/rust/fluent-bundle/src/message.rs
new file mode 100644
index 0000000000..a6cc00d77e
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/message.rs
@@ -0,0 +1,274 @@
+use fluent_syntax::ast;
+
+/// [`FluentAttribute`] is a component of a compound [`FluentMessage`].
+///
+/// It represents a key-value pair providing a translation of a component
+/// of a user interface widget localized by the given message.
+///
+/// # Example
+///
+/// ```
+/// use fluent_bundle::{FluentResource, FluentBundle};
+///
+/// let source = r#"
+///
+/// confirm-modal = Are you sure?
+/// .confirm = Yes
+/// .cancel = No
+/// .tooltip = Closing the window will lose all unsaved data.
+///
+/// "#;
+///
+/// let resource = FluentResource::try_new(source.to_string())
+/// .expect("Failed to parse the resource.");
+///
+/// let mut bundle = FluentBundle::default();
+/// bundle.add_resource(resource)
+/// .expect("Failed to add a resource.");
+///
+/// let msg = bundle.get_message("confirm-modal")
+/// .expect("Failed to retrieve a message.");
+///
+/// let mut err = vec![];
+///
+/// let attributes = msg.attributes().map(|attr| {
+/// bundle.format_pattern(attr.value(), None, &mut err)
+/// }).collect::<Vec<_>>();
+///
+/// assert_eq!(attributes[0], "Yes");
+/// assert_eq!(attributes[1], "No");
+/// assert_eq!(attributes[2], "Closing the window will lose all unsaved data.");
+/// ```
+#[derive(Debug, PartialEq)]
+pub struct FluentAttribute<'m> {
+ node: &'m ast::Attribute<&'m str>,
+}
+
+impl<'m> FluentAttribute<'m> {
+ /// Retrieves an id of an attribute.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use fluent_bundle::{FluentResource, FluentBundle};
+ /// # let source = r#"
+ /// # confirm-modal =
+ /// # .confirm = Yes
+ /// # "#;
+ /// # let resource = FluentResource::try_new(source.to_string())
+ /// # .expect("Failed to parse the resource.");
+ /// # let mut bundle = FluentBundle::default();
+ /// # bundle.add_resource(resource)
+ /// # .expect("Failed to add a resource.");
+ /// let msg = bundle.get_message("confirm-modal")
+ /// .expect("Failed to retrieve a message.");
+ ///
+ /// let attr1 = msg.attributes().next()
+ /// .expect("Failed to retrieve an attribute.");
+ ///
+ /// assert_eq!(attr1.id(), "confirm");
+ /// ```
+ pub fn id(&self) -> &'m str {
+ &self.node.id.name
+ }
+
+ /// Retrieves an value of an attribute.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use fluent_bundle::{FluentResource, FluentBundle};
+ /// # let source = r#"
+ /// # confirm-modal =
+ /// # .confirm = Yes
+ /// # "#;
+ /// # let resource = FluentResource::try_new(source.to_string())
+ /// # .expect("Failed to parse the resource.");
+ /// # let mut bundle = FluentBundle::default();
+ /// # bundle.add_resource(resource)
+ /// # .expect("Failed to add a resource.");
+ /// let msg = bundle.get_message("confirm-modal")
+ /// .expect("Failed to retrieve a message.");
+ ///
+ /// let attr1 = msg.attributes().next()
+ /// .expect("Failed to retrieve an attribute.");
+ ///
+ /// let mut err = vec![];
+ ///
+ /// let value = attr1.value();
+ /// assert_eq!(
+ /// bundle.format_pattern(value, None, &mut err),
+ /// "Yes"
+ /// );
+ /// ```
+ pub fn value(&self) -> &'m ast::Pattern<&'m str> {
+ &self.node.value
+ }
+}
+
+impl<'m> From<&'m ast::Attribute<&'m str>> for FluentAttribute<'m> {
+ fn from(attr: &'m ast::Attribute<&'m str>) -> Self {
+ FluentAttribute { node: attr }
+ }
+}
+
+/// [`FluentMessage`] is a basic translation unit of the Fluent system.
+///
+/// The instance of a message is returned from the
+/// [`FluentBundle::get_message`](crate::bundle::FluentBundle::get_message)
+/// method, for the lifetime of the [`FluentBundle`](crate::bundle::FluentBundle) instance.
+///
+/// # Example
+///
+/// ```
+/// use fluent_bundle::{FluentResource, FluentBundle};
+///
+/// let source = r#"
+///
+/// hello-world = Hello World!
+///
+/// "#;
+///
+/// let resource = FluentResource::try_new(source.to_string())
+/// .expect("Failed to parse the resource.");
+///
+/// let mut bundle = FluentBundle::default();
+/// bundle.add_resource(resource)
+/// .expect("Failed to add a resource.");
+///
+/// let msg = bundle.get_message("hello-world")
+/// .expect("Failed to retrieve a message.");
+///
+/// assert!(msg.value().is_some());
+/// ```
+///
+/// That value can be then passed to
+/// [`FluentBundle::format_pattern`](crate::bundle::FluentBundle::format_pattern) to be formatted
+/// within the context of a given [`FluentBundle`](crate::bundle::FluentBundle) instance.
+///
+/// # Compound Message
+///
+/// A message may contain a `value`, but it can also contain a list of [`FluentAttribute`] elements.
+///
+/// If a message contains attributes, it is called a "compound" message.
+///
+/// In such case, the message contains a list of key-value attributes that represent
+/// different translation values associated with a single translation unit.
+///
+/// This is useful for scenarios where a [`FluentMessage`] is associated with a
+/// complex User Interface widget which has multiple attributes that need to be translated.
+/// ```text
+/// confirm-modal = Are you sure?
+/// .confirm = Yes
+/// .cancel = No
+/// .tooltip = Closing the window will lose all unsaved data.
+/// ```
+#[derive(Debug, PartialEq)]
+pub struct FluentMessage<'m> {
+ node: &'m ast::Message<&'m str>,
+}
+
+impl<'m> FluentMessage<'m> {
+ /// Retrieves an option of a [`ast::Pattern`](fluent_syntax::ast::Pattern).
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use fluent_bundle::{FluentResource, FluentBundle};
+ /// # let source = r#"
+ /// # hello-world = Hello World!
+ /// # "#;
+ /// # let resource = FluentResource::try_new(source.to_string())
+ /// # .expect("Failed to parse the resource.");
+ /// # let mut bundle = FluentBundle::default();
+ /// # bundle.add_resource(resource)
+ /// # .expect("Failed to add a resource.");
+ /// let msg = bundle.get_message("hello-world")
+ /// .expect("Failed to retrieve a message.");
+ ///
+ /// if let Some(value) = msg.value() {
+ /// let mut err = vec![];
+ /// assert_eq!(
+ /// bundle.format_pattern(value, None, &mut err),
+ /// "Hello World!"
+ /// );
+ /// # assert_eq!(err.len(), 0);
+ /// }
+ /// ```
+ pub fn value(&self) -> Option<&'m ast::Pattern<&'m str>> {
+ self.node.value.as_ref()
+ }
+
+ /// An iterator over [`FluentAttribute`] elements.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use fluent_bundle::{FluentResource, FluentBundle};
+ /// # let source = r#"
+ /// # hello-world =
+ /// # .label = This is a label
+ /// # .accesskey = C
+ /// # "#;
+ /// # let resource = FluentResource::try_new(source.to_string())
+ /// # .expect("Failed to parse the resource.");
+ /// # let mut bundle = FluentBundle::default();
+ /// # bundle.add_resource(resource)
+ /// # .expect("Failed to add a resource.");
+ /// let msg = bundle.get_message("hello-world")
+ /// .expect("Failed to retrieve a message.");
+ ///
+ /// let mut err = vec![];
+ ///
+ /// for attr in msg.attributes() {
+ /// let _ = bundle.format_pattern(attr.value(), None, &mut err);
+ /// }
+ /// # assert_eq!(err.len(), 0);
+ /// ```
+ pub fn attributes(&self) -> impl Iterator<Item = FluentAttribute<'m>> {
+ self.node.attributes.iter().map(Into::into)
+ }
+
+ /// Retrieve a single [`FluentAttribute`] element.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use fluent_bundle::{FluentResource, FluentBundle};
+ /// # let source = r#"
+ /// # hello-world =
+ /// # .label = This is a label
+ /// # .accesskey = C
+ /// # "#;
+ /// # let resource = FluentResource::try_new(source.to_string())
+ /// # .expect("Failed to parse the resource.");
+ /// # let mut bundle = FluentBundle::default();
+ /// # bundle.add_resource(resource)
+ /// # .expect("Failed to add a resource.");
+ /// let msg = bundle.get_message("hello-world")
+ /// .expect("Failed to retrieve a message.");
+ ///
+ /// let mut err = vec![];
+ ///
+ /// if let Some(attr) = msg.get_attribute("label") {
+ /// assert_eq!(
+ /// bundle.format_pattern(attr.value(), None, &mut err),
+ /// "This is a label"
+ /// );
+ /// }
+ /// # assert_eq!(err.len(), 0);
+ /// ```
+ pub fn get_attribute(&self, key: &str) -> Option<FluentAttribute<'m>> {
+ self.node
+ .attributes
+ .iter()
+ .find(|attr| attr.id.name == key)
+ .map(Into::into)
+ }
+}
+
+impl<'m> From<&'m ast::Message<&'m str>> for FluentMessage<'m> {
+ fn from(msg: &'m ast::Message<&'m str>) -> Self {
+ FluentMessage { node: msg }
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/resolver/errors.rs b/third_party/rust/fluent-bundle/src/resolver/errors.rs
new file mode 100644
index 0000000000..831d8474a5
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/resolver/errors.rs
@@ -0,0 +1,96 @@
+use fluent_syntax::ast::InlineExpression;
+use std::error::Error;
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReferenceKind {
+ Function {
+ id: String,
+ },
+ Message {
+ id: String,
+ attribute: Option<String>,
+ },
+ Term {
+ id: String,
+ attribute: Option<String>,
+ },
+ Variable {
+ id: String,
+ },
+}
+
+impl<T> From<&InlineExpression<T>> for ReferenceKind
+where
+ T: ToString,
+{
+ fn from(exp: &InlineExpression<T>) -> Self {
+ match exp {
+ InlineExpression::FunctionReference { id, .. } => Self::Function {
+ id: id.name.to_string(),
+ },
+ InlineExpression::MessageReference { id, attribute } => Self::Message {
+ id: id.name.to_string(),
+ attribute: attribute.as_ref().map(|i| i.name.to_string()),
+ },
+ InlineExpression::TermReference { id, attribute, .. } => Self::Term {
+ id: id.name.to_string(),
+ attribute: attribute.as_ref().map(|i| i.name.to_string()),
+ },
+ InlineExpression::VariableReference { id, .. } => Self::Variable {
+ id: id.name.to_string(),
+ },
+ _ => unreachable!(),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResolverError {
+ Reference(ReferenceKind),
+ NoValue(String),
+ MissingDefault,
+ Cyclic,
+ TooManyPlaceables,
+}
+
+impl std::fmt::Display for ResolverError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Reference(exp) => match exp {
+ ReferenceKind::Function { id } => write!(f, "Unknown function: {}()", id),
+ ReferenceKind::Message {
+ id,
+ attribute: None,
+ } => write!(f, "Unknown message: {}", id),
+ ReferenceKind::Message {
+ id,
+ attribute: Some(attribute),
+ } => write!(f, "Unknown attribute: {}.{}", id, attribute),
+ ReferenceKind::Term {
+ id,
+ attribute: None,
+ } => write!(f, "Unknown term: -{}", id),
+ ReferenceKind::Term {
+ id,
+ attribute: Some(attribute),
+ } => write!(f, "Unknown attribute: -{}.{}", id, attribute),
+ ReferenceKind::Variable { id } => write!(f, "Unknown variable: ${}", id),
+ },
+ Self::NoValue(id) => write!(f, "No value: {}", id),
+ Self::MissingDefault => f.write_str("No default"),
+ Self::Cyclic => f.write_str("Cyclical dependency detected"),
+ Self::TooManyPlaceables => f.write_str("Too many placeables"),
+ }
+ }
+}
+
+impl<T> From<&InlineExpression<T>> for ResolverError
+where
+ T: ToString,
+{
+ fn from(exp: &InlineExpression<T>) -> Self {
+ Self::Reference(exp.into())
+ }
+}
+
+impl Error for ResolverError {}
diff --git a/third_party/rust/fluent-bundle/src/resolver/expression.rs b/third_party/rust/fluent-bundle/src/resolver/expression.rs
new file mode 100644
index 0000000000..d0d02decd3
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/resolver/expression.rs
@@ -0,0 +1,66 @@
+use super::scope::Scope;
+use super::WriteValue;
+
+use std::borrow::Borrow;
+use std::fmt;
+
+use fluent_syntax::ast;
+
+use crate::memoizer::MemoizerKind;
+use crate::resolver::{ResolveValue, ResolverError};
+use crate::resource::FluentResource;
+use crate::types::FluentValue;
+
+impl<'p> WriteValue for ast::Expression<&'p str> {
+ fn write<'scope, 'errors, W, R, M>(
+ &'scope self,
+ w: &mut W,
+ scope: &mut Scope<'scope, 'errors, R, M>,
+ ) -> fmt::Result
+ where
+ W: fmt::Write,
+ R: Borrow<FluentResource>,
+ M: MemoizerKind,
+ {
+ match self {
+ Self::Inline(exp) => exp.write(w, scope),
+ Self::Select { selector, variants } => {
+ let selector = selector.resolve(scope);
+ match selector {
+ FluentValue::String(_) | FluentValue::Number(_) => {
+ for variant in variants {
+ let key = match variant.key {
+ ast::VariantKey::Identifier { name } => name.into(),
+ ast::VariantKey::NumberLiteral { value } => {
+ FluentValue::try_number(value)
+ }
+ };
+ if key.matches(&selector, scope) {
+ return variant.value.write(w, scope);
+ }
+ }
+ }
+ _ => {}
+ }
+
+ for variant in variants {
+ if variant.default {
+ return variant.value.write(w, scope);
+ }
+ }
+ scope.add_error(ResolverError::MissingDefault);
+ Ok(())
+ }
+ }
+ }
+
+ fn write_error<W>(&self, w: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match self {
+ Self::Inline(exp) => exp.write_error(w),
+ Self::Select { .. } => unreachable!(),
+ }
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/resolver/inline_expression.rs b/third_party/rust/fluent-bundle/src/resolver/inline_expression.rs
new file mode 100644
index 0000000000..b9e89b6e8e
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/resolver/inline_expression.rs
@@ -0,0 +1,181 @@
+use super::scope::Scope;
+use super::{ResolveValue, ResolverError, WriteValue};
+
+use std::borrow::Borrow;
+use std::fmt;
+
+use fluent_syntax::ast;
+use fluent_syntax::unicode::{unescape_unicode, unescape_unicode_to_string};
+
+use crate::entry::GetEntry;
+use crate::memoizer::MemoizerKind;
+use crate::resource::FluentResource;
+use crate::types::FluentValue;
+
+impl<'p> WriteValue for ast::InlineExpression<&'p str> {
+ fn write<'scope, 'errors, W, R, M>(
+ &'scope self,
+ w: &mut W,
+ scope: &mut Scope<'scope, 'errors, R, M>,
+ ) -> fmt::Result
+ where
+ W: fmt::Write,
+ R: Borrow<FluentResource>,
+ M: MemoizerKind,
+ {
+ match self {
+ Self::StringLiteral { value } => unescape_unicode(w, value),
+ Self::MessageReference { id, attribute } => {
+ if let Some(msg) = scope.bundle.get_entry_message(id.name) {
+ if let Some(attr) = attribute {
+ msg.attributes
+ .iter()
+ .find_map(|a| {
+ if a.id.name == attr.name {
+ Some(scope.track(w, &a.value, self))
+ } else {
+ None
+ }
+ })
+ .unwrap_or_else(|| scope.write_ref_error(w, self))
+ } else {
+ msg.value
+ .as_ref()
+ .map(|value| scope.track(w, value, self))
+ .unwrap_or_else(|| {
+ scope.add_error(ResolverError::NoValue(id.name.to_string()));
+ w.write_char('{')?;
+ self.write_error(w)?;
+ w.write_char('}')
+ })
+ }
+ } else {
+ scope.write_ref_error(w, self)
+ }
+ }
+ Self::NumberLiteral { value } => FluentValue::try_number(*value).write(w, scope),
+ Self::TermReference {
+ id,
+ attribute,
+ arguments,
+ } => {
+ let (_, resolved_named_args) = scope.get_arguments(arguments.as_ref());
+
+ scope.local_args = Some(resolved_named_args);
+ let result = scope
+ .bundle
+ .get_entry_term(id.name)
+ .and_then(|term| {
+ if let Some(attr) = attribute {
+ term.attributes.iter().find_map(|a| {
+ if a.id.name == attr.name {
+ Some(scope.track(w, &a.value, self))
+ } else {
+ None
+ }
+ })
+ } else {
+ Some(scope.track(w, &term.value, self))
+ }
+ })
+ .unwrap_or_else(|| scope.write_ref_error(w, self));
+ scope.local_args = None;
+ result
+ }
+ Self::FunctionReference { id, arguments } => {
+ let (resolved_positional_args, resolved_named_args) =
+ scope.get_arguments(Some(arguments));
+
+ let func = scope.bundle.get_entry_function(id.name);
+
+ if let Some(func) = func {
+ let result = func(resolved_positional_args.as_slice(), &resolved_named_args);
+ if let FluentValue::Error = result {
+ self.write_error(w)
+ } else {
+ w.write_str(&result.as_string(scope))
+ }
+ } else {
+ scope.write_ref_error(w, self)
+ }
+ }
+ Self::VariableReference { id } => {
+ let args = scope.local_args.as_ref().or(scope.args);
+
+ if let Some(arg) = args.and_then(|args| args.get(id.name)) {
+ arg.write(w, scope)
+ } else {
+ if scope.local_args.is_none() {
+ scope.add_error(self.into());
+ }
+ w.write_char('{')?;
+ self.write_error(w)?;
+ w.write_char('}')
+ }
+ }
+ Self::Placeable { expression } => expression.write(w, scope),
+ }
+ }
+
+ fn write_error<W>(&self, w: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match self {
+ Self::MessageReference {
+ id,
+ attribute: Some(attribute),
+ } => write!(w, "{}.{}", id.name, attribute.name),
+ Self::MessageReference {
+ id,
+ attribute: None,
+ } => w.write_str(id.name),
+ Self::TermReference {
+ id,
+ attribute: Some(attribute),
+ ..
+ } => write!(w, "-{}.{}", id.name, attribute.name),
+ Self::TermReference {
+ id,
+ attribute: None,
+ ..
+ } => write!(w, "-{}", id.name),
+ Self::FunctionReference { id, .. } => write!(w, "{}()", id.name),
+ Self::VariableReference { id } => write!(w, "${}", id.name),
+ _ => unreachable!(),
+ }
+ }
+}
+
+impl<'p> ResolveValue for ast::InlineExpression<&'p str> {
+ fn resolve<'source, 'errors, R, M>(
+ &'source self,
+ scope: &mut Scope<'source, 'errors, R, M>,
+ ) -> FluentValue<'source>
+ where
+ R: Borrow<FluentResource>,
+ M: MemoizerKind,
+ {
+ match self {
+ Self::StringLiteral { value } => unescape_unicode_to_string(value).into(),
+ Self::NumberLiteral { value } => FluentValue::try_number(*value),
+ Self::VariableReference { id } => {
+ let args = scope.local_args.as_ref().or(scope.args);
+
+ if let Some(arg) = args.and_then(|args| args.get(id.name)) {
+ arg.clone()
+ } else {
+ if scope.local_args.is_none() {
+ scope.add_error(self.into());
+ }
+ FluentValue::Error
+ }
+ }
+ _ => {
+ let mut result = String::new();
+ self.write(&mut result, scope).expect("Failed to write");
+ result.into()
+ }
+ }
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/resolver/mod.rs b/third_party/rust/fluent-bundle/src/resolver/mod.rs
new file mode 100644
index 0000000000..f137bcc91b
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/resolver/mod.rs
@@ -0,0 +1,42 @@
+pub mod errors;
+mod expression;
+mod inline_expression;
+mod pattern;
+mod scope;
+
+pub use errors::ResolverError;
+pub use scope::Scope;
+
+use std::borrow::Borrow;
+use std::fmt;
+
+use crate::memoizer::MemoizerKind;
+use crate::resource::FluentResource;
+use crate::types::FluentValue;
+
+// Converts an AST node to a `FluentValue`.
+pub(crate) trait ResolveValue {
+ fn resolve<'source, 'errors, R, M>(
+ &'source self,
+ scope: &mut Scope<'source, 'errors, R, M>,
+ ) -> FluentValue<'source>
+ where
+ R: Borrow<FluentResource>,
+ M: MemoizerKind;
+}
+
+pub(crate) trait WriteValue {
+ fn write<'source, 'errors, W, R, M>(
+ &'source self,
+ w: &mut W,
+ scope: &mut Scope<'source, 'errors, R, M>,
+ ) -> fmt::Result
+ where
+ W: fmt::Write,
+ R: Borrow<FluentResource>,
+ M: MemoizerKind;
+
+ fn write_error<W>(&self, _w: &mut W) -> fmt::Result
+ where
+ W: fmt::Write;
+}
diff --git a/third_party/rust/fluent-bundle/src/resolver/pattern.rs b/third_party/rust/fluent-bundle/src/resolver/pattern.rs
new file mode 100644
index 0000000000..4e01d4ca47
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/resolver/pattern.rs
@@ -0,0 +1,108 @@
+use super::scope::Scope;
+use super::{ResolverError, WriteValue};
+
+use std::borrow::Borrow;
+use std::fmt;
+
+use fluent_syntax::ast;
+
+use crate::memoizer::MemoizerKind;
+use crate::resolver::ResolveValue;
+use crate::resource::FluentResource;
+use crate::types::FluentValue;
+
+const MAX_PLACEABLES: u8 = 100;
+
+impl<'p> WriteValue for ast::Pattern<&'p str> {
+ fn write<'scope, 'errors, W, R, M>(
+ &'scope self,
+ w: &mut W,
+ scope: &mut Scope<'scope, 'errors, R, M>,
+ ) -> fmt::Result
+ where
+ W: fmt::Write,
+ R: Borrow<FluentResource>,
+ M: MemoizerKind,
+ {
+ let len = self.elements.len();
+
+ for elem in &self.elements {
+ if scope.dirty {
+ return Ok(());
+ }
+
+ match elem {
+ ast::PatternElement::TextElement { value } => {
+ if let Some(ref transform) = scope.bundle.transform {
+ w.write_str(&transform(value))?;
+ } else {
+ w.write_str(value)?;
+ }
+ }
+ ast::PatternElement::Placeable { ref expression } => {
+ scope.placeables += 1;
+ if scope.placeables > MAX_PLACEABLES {
+ scope.dirty = true;
+ scope.add_error(ResolverError::TooManyPlaceables);
+ return Ok(());
+ }
+
+ let needs_isolation = scope.bundle.use_isolating
+ && len > 1
+ && !matches!(
+ expression,
+ ast::Expression::Inline(ast::InlineExpression::MessageReference { .. },)
+ | ast::Expression::Inline(
+ ast::InlineExpression::TermReference { .. },
+ )
+ | ast::Expression::Inline(
+ ast::InlineExpression::StringLiteral { .. },
+ )
+ );
+ if needs_isolation {
+ w.write_char('\u{2068}')?;
+ }
+ scope.maybe_track(w, self, expression)?;
+ if needs_isolation {
+ w.write_char('\u{2069}')?;
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn write_error<W>(&self, _w: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ unreachable!()
+ }
+}
+
+impl<'p> ResolveValue for ast::Pattern<&'p str> {
+ fn resolve<'source, 'errors, R, M>(
+ &'source self,
+ scope: &mut Scope<'source, 'errors, R, M>,
+ ) -> FluentValue<'source>
+ where
+ R: Borrow<FluentResource>,
+ M: MemoizerKind,
+ {
+ let len = self.elements.len();
+
+ if len == 1 {
+ if let ast::PatternElement::TextElement { value } = self.elements[0] {
+ return scope
+ .bundle
+ .transform
+ .map_or_else(|| value.into(), |transform| transform(value).into());
+ }
+ }
+
+ let mut result = String::new();
+ self.write(&mut result, scope)
+ .expect("Failed to write to a string.");
+ result.into()
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/resolver/scope.rs b/third_party/rust/fluent-bundle/src/resolver/scope.rs
new file mode 100644
index 0000000000..004701137e
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/resolver/scope.rs
@@ -0,0 +1,141 @@
+use crate::bundle::FluentBundle;
+use crate::memoizer::MemoizerKind;
+use crate::resolver::{ResolveValue, ResolverError, WriteValue};
+use crate::types::FluentValue;
+use crate::{FluentArgs, FluentError, FluentResource};
+use fluent_syntax::ast;
+use std::borrow::Borrow;
+use std::fmt;
+
+/// State for a single `ResolveValue::to_value` call.
+pub struct Scope<'scope, 'errors, R, M> {
+ /// The current `FluentBundle` instance.
+ pub bundle: &'scope FluentBundle<R, M>,
+ /// The current arguments passed by the developer.
+ pub(super) args: Option<&'scope FluentArgs<'scope>>,
+ /// Local args
+ pub(super) local_args: Option<FluentArgs<'scope>>,
+ /// The running count of resolved placeables. Used to detect the Billion
+ /// Laughs and Quadratic Blowup attacks.
+ pub(super) placeables: u8,
+ /// Tracks hashes to prevent infinite recursion.
+ travelled: smallvec::SmallVec<[&'scope ast::Pattern<&'scope str>; 2]>,
+ /// Track errors accumulated during resolving.
+ pub errors: Option<&'errors mut Vec<FluentError>>,
+ /// Makes the resolver bail.
+ pub dirty: bool,
+}
+
+impl<'scope, 'errors, R, M> Scope<'scope, 'errors, R, M> {
+ pub fn new(
+ bundle: &'scope FluentBundle<R, M>,
+ args: Option<&'scope FluentArgs>,
+ errors: Option<&'errors mut Vec<FluentError>>,
+ ) -> Self {
+ Scope {
+ bundle,
+ args,
+ local_args: None,
+ placeables: 0,
+ travelled: Default::default(),
+ errors,
+ dirty: false,
+ }
+ }
+
+ pub fn add_error(&mut self, error: ResolverError) {
+ if let Some(errors) = self.errors.as_mut() {
+ errors.push(error.into());
+ }
+ }
+
+ // This method allows us to lazily add Pattern on the stack,
+ // only if the Pattern::resolve has been called on an empty stack.
+ //
+ // This is the case when pattern is called from Bundle and it
+ // allows us to fast-path simple resolutions, and only use the stack
+ // for placeables.
+ pub fn maybe_track<W>(
+ &mut self,
+ w: &mut W,
+ pattern: &'scope ast::Pattern<&str>,
+ exp: &'scope ast::Expression<&str>,
+ ) -> fmt::Result
+ where
+ R: Borrow<FluentResource>,
+ W: fmt::Write,
+ M: MemoizerKind,
+ {
+ if self.travelled.is_empty() {
+ self.travelled.push(pattern);
+ }
+ exp.write(w, self)?;
+ if self.dirty {
+ w.write_char('{')?;
+ exp.write_error(w)?;
+ w.write_char('}')
+ } else {
+ Ok(())
+ }
+ }
+
+ pub fn track<W>(
+ &mut self,
+ w: &mut W,
+ pattern: &'scope ast::Pattern<&str>,
+ exp: &ast::InlineExpression<&str>,
+ ) -> fmt::Result
+ where
+ R: Borrow<FluentResource>,
+ W: fmt::Write,
+ M: MemoizerKind,
+ {
+ if self.travelled.contains(&pattern) {
+ self.add_error(ResolverError::Cyclic);
+ w.write_char('{')?;
+ exp.write_error(w)?;
+ w.write_char('}')
+ } else {
+ self.travelled.push(pattern);
+ let result = pattern.write(w, self);
+ self.travelled.pop();
+ result
+ }
+ }
+
+ pub fn write_ref_error<W>(
+ &mut self,
+ w: &mut W,
+ exp: &ast::InlineExpression<&str>,
+ ) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.add_error(exp.into());
+ w.write_char('{')?;
+ exp.write_error(w)?;
+ w.write_char('}')
+ }
+
+ pub fn get_arguments(
+ &mut self,
+ arguments: Option<&'scope ast::CallArguments<&'scope str>>,
+ ) -> (Vec<FluentValue<'scope>>, FluentArgs<'scope>)
+ where
+ R: Borrow<FluentResource>,
+ M: MemoizerKind,
+ {
+ if let Some(ast::CallArguments { positional, named }) = arguments {
+ let positional = positional.iter().map(|expr| expr.resolve(self)).collect();
+
+ let named = named
+ .iter()
+ .map(|arg| (arg.name.name, arg.value.resolve(self)))
+ .collect();
+
+ (positional, named)
+ } else {
+ (Vec::new(), FluentArgs::new())
+ }
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/resource.rs b/third_party/rust/fluent-bundle/src/resource.rs
new file mode 100644
index 0000000000..0c39c838f4
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/resource.rs
@@ -0,0 +1,171 @@
+use fluent_syntax::ast;
+use fluent_syntax::parser::{parse_runtime, ParserError};
+
+use self_cell::self_cell;
+
+type Resource<'s> = ast::Resource<&'s str>;
+
+self_cell!(
+ pub struct InnerFluentResource {
+ owner: String,
+
+ #[covariant]
+ dependent: Resource,
+ }
+
+ impl {Debug}
+);
+
+/// A resource containing a list of localization messages.
+///
+/// [`FluentResource`] wraps an [`Abstract Syntax Tree`](../fluent_syntax/ast/index.html) produced by the
+/// [`parser`](../fluent_syntax/parser/index.html) and provides an access to a list
+/// of its entries.
+///
+/// A good mental model for a resource is a single FTL file, but in the future
+/// there's nothing preventing a resource from being stored in a data base,
+/// pre-parsed format or in some other structured form.
+///
+/// # Example
+///
+/// ```
+/// use fluent_bundle::FluentResource;
+///
+/// let source = r#"
+///
+/// hello-world = Hello World!
+///
+/// "#;
+///
+/// let resource = FluentResource::try_new(source.to_string())
+/// .expect("Errors encountered while parsing a resource.");
+///
+/// assert_eq!(resource.entries().count(), 1);
+/// ```
+///
+/// # Ownership
+///
+/// A resource owns the source string and the AST contains references
+/// to the slices of the source.
+#[derive(Debug)]
+pub struct FluentResource(InnerFluentResource);
+
+impl FluentResource {
+ /// A fallible constructor of a new [`FluentResource`].
+ ///
+ /// It takes an encoded `Fluent Translation List` string, parses
+ /// it and stores both, the input string and the AST view of it,
+ /// for runtime use.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_bundle::FluentResource;
+ ///
+ /// let source = r#"
+ ///
+ /// hello-world = Hello, { $user }!
+ ///
+ /// "#;
+ ///
+ /// let resource = FluentResource::try_new(source.to_string());
+ ///
+ /// assert!(resource.is_ok());
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// The method will return the resource irrelevant of parse errors
+ /// encountered during parsing of the source, but in case of errors,
+ /// the `Err` variant will contain both the structure and a vector
+ /// of errors.
+ pub fn try_new(source: String) -> Result<Self, (Self, Vec<ParserError>)> {
+ let mut errors = None;
+
+ let res = InnerFluentResource::new(source, |source| match parse_runtime(source.as_str()) {
+ Ok(ast) => ast,
+ Err((ast, err)) => {
+ errors = Some(err);
+ ast
+ }
+ });
+
+ match errors {
+ None => Ok(Self(res)),
+ Some(err) => Err((Self(res), err)),
+ }
+ }
+
+ /// Returns a reference to the source string that was used
+ /// to construct the [`FluentResource`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_bundle::FluentResource;
+ ///
+ /// let source = "hello-world = Hello, { $user }!";
+ ///
+ /// let resource = FluentResource::try_new(source.to_string())
+ /// .expect("Failed to parse FTL.");
+ ///
+ /// assert_eq!(
+ /// resource.source(),
+ /// "hello-world = Hello, { $user }!"
+ /// );
+ /// ```
+ pub fn source(&self) -> &str {
+ &self.0.borrow_owner()
+ }
+
+ /// Returns an iterator over [`entries`](fluent_syntax::ast::Entry) of the [`FluentResource`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_bundle::FluentResource;
+ /// use fluent_syntax::ast;
+ ///
+ /// let source = r#"
+ ///
+ /// hello-world = Hello, { $user }!
+ ///
+ /// "#;
+ ///
+ /// let resource = FluentResource::try_new(source.to_string())
+ /// .expect("Failed to parse FTL.");
+ ///
+ /// assert_eq!(
+ /// resource.entries().count(),
+ /// 1
+ /// );
+ /// assert!(matches!(resource.entries().next(), Some(ast::Entry::Message(_))));
+ /// ```
+ pub fn entries(&self) -> impl Iterator<Item = &ast::Entry<&str>> {
+ self.0.borrow_dependent().body.iter()
+ }
+
+ /// Returns an [`Entry`](fluent_syntax::ast::Entry) at the
+ /// given index out of the [`FluentResource`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_bundle::FluentResource;
+ /// use fluent_syntax::ast;
+ ///
+ /// let source = r#"
+ ///
+ /// hello-world = Hello, { $user }!
+ ///
+ /// "#;
+ ///
+ /// let resource = FluentResource::try_new(source.to_string())
+ /// .expect("Failed to parse FTL.");
+ ///
+ /// assert!(matches!(resource.get_entry(0), Some(ast::Entry::Message(_))));
+ /// ```
+ pub fn get_entry(&self, idx: usize) -> Option<&ast::Entry<&str>> {
+ self.0.borrow_dependent().body.get(idx)
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/types/mod.rs b/third_party/rust/fluent-bundle/src/types/mod.rs
new file mode 100644
index 0000000000..714fe4c76f
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/types/mod.rs
@@ -0,0 +1,202 @@
+//! `types` module contains types necessary for Fluent runtime
+//! value handling.
+//! The core struct is [`FluentValue`] which is a type that can be passed
+//! to the [`FluentBundle::format_pattern`](crate::bundle::FluentBundle) as an argument, it can be passed
+//! to any Fluent Function, and any function may return it.
+//!
+//! This part of functionality is not fully hashed out yet, since we're waiting
+//! for the internationalization APIs to mature, at which point all number
+//! formatting operations will be moved out of Fluent.
+//!
+//! For now, [`FluentValue`] can be a string, a number, or a custom [`FluentType`]
+//! which allows users of the library to implement their own types of values,
+//! such as dates, or more complex structures needed for their bindings.
+mod number;
+mod plural;
+
+pub use number::*;
+use plural::PluralRules;
+
+use std::any::Any;
+use std::borrow::{Borrow, Cow};
+use std::fmt;
+use std::str::FromStr;
+
+use intl_pluralrules::{PluralCategory, PluralRuleType};
+
+use crate::memoizer::MemoizerKind;
+use crate::resolver::Scope;
+use crate::resource::FluentResource;
+
+pub trait FluentType: fmt::Debug + AnyEq + 'static {
+ fn duplicate(&self) -> Box<dyn FluentType + Send>;
+ fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str>;
+ fn as_string_threadsafe(
+ &self,
+ intls: &intl_memoizer::concurrent::IntlLangMemoizer,
+ ) -> Cow<'static, str>;
+}
+
+impl PartialEq for dyn FluentType + Send {
+ fn eq(&self, other: &Self) -> bool {
+ self.equals(other.as_any())
+ }
+}
+
+pub trait AnyEq: Any + 'static {
+ fn equals(&self, other: &dyn Any) -> bool;
+ fn as_any(&self) -> &dyn Any;
+}
+
+impl<T: Any + PartialEq> AnyEq for T {
+ fn equals(&self, other: &dyn Any) -> bool {
+ other
+ .downcast_ref::<Self>()
+ .map_or(false, |that| self == that)
+ }
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+/// The `FluentValue` enum represents values which can be formatted to a String.
+///
+/// Those values are either passed as arguments to [`FluentBundle::format_pattern`][] or
+/// produced by functions, or generated in the process of pattern resolution.
+///
+/// [`FluentBundle::format_pattern`]: ../bundle/struct.FluentBundle.html#method.format_pattern
+#[derive(Debug)]
+pub enum FluentValue<'source> {
+ String(Cow<'source, str>),
+ Number(FluentNumber),
+ Custom(Box<dyn FluentType + Send>),
+ None,
+ Error,
+}
+
+impl<'s> PartialEq for FluentValue<'s> {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (FluentValue::String(s), FluentValue::String(s2)) => s == s2,
+ (FluentValue::Number(s), FluentValue::Number(s2)) => s == s2,
+ (FluentValue::Custom(s), FluentValue::Custom(s2)) => s == s2,
+ _ => false,
+ }
+ }
+}
+
+impl<'s> Clone for FluentValue<'s> {
+ fn clone(&self) -> Self {
+ match self {
+ FluentValue::String(s) => FluentValue::String(s.clone()),
+ FluentValue::Number(s) => FluentValue::Number(s.clone()),
+ FluentValue::Custom(s) => {
+ let new_value: Box<dyn FluentType + Send> = s.duplicate();
+ FluentValue::Custom(new_value)
+ }
+ FluentValue::Error => FluentValue::Error,
+ FluentValue::None => FluentValue::None,
+ }
+ }
+}
+
+impl<'source> FluentValue<'source> {
+ pub fn try_number<S: ToString>(v: S) -> Self {
+ let s = v.to_string();
+ if let Ok(num) = FluentNumber::from_str(&s) {
+ num.into()
+ } else {
+ s.into()
+ }
+ }
+
+ pub fn matches<R: Borrow<FluentResource>, M>(
+ &self,
+ other: &FluentValue,
+ scope: &Scope<R, M>,
+ ) -> bool
+ where
+ M: MemoizerKind,
+ {
+ match (self, other) {
+ (&FluentValue::String(ref a), &FluentValue::String(ref b)) => a == b,
+ (&FluentValue::Number(ref a), &FluentValue::Number(ref b)) => a == b,
+ (&FluentValue::String(ref a), &FluentValue::Number(ref b)) => {
+ let cat = match a.as_ref() {
+ "zero" => PluralCategory::ZERO,
+ "one" => PluralCategory::ONE,
+ "two" => PluralCategory::TWO,
+ "few" => PluralCategory::FEW,
+ "many" => PluralCategory::MANY,
+ "other" => PluralCategory::OTHER,
+ _ => return false,
+ };
+ scope
+ .bundle
+ .intls
+ .with_try_get_threadsafe::<PluralRules, _, _>(
+ (PluralRuleType::CARDINAL,),
+ |pr| pr.0.select(b) == Ok(cat),
+ )
+ .unwrap()
+ }
+ _ => false,
+ }
+ }
+
+ pub fn write<W, R, M>(&self, w: &mut W, scope: &Scope<R, M>) -> fmt::Result
+ where
+ W: fmt::Write,
+ R: Borrow<FluentResource>,
+ M: MemoizerKind,
+ {
+ if let Some(formatter) = &scope.bundle.formatter {
+ if let Some(val) = formatter(self, &scope.bundle.intls) {
+ return w.write_str(&val);
+ }
+ }
+ match self {
+ FluentValue::String(s) => w.write_str(s),
+ FluentValue::Number(n) => w.write_str(&n.as_string()),
+ FluentValue::Custom(s) => w.write_str(&scope.bundle.intls.stringify_value(&**s)),
+ FluentValue::Error => Ok(()),
+ FluentValue::None => Ok(()),
+ }
+ }
+
+ pub fn as_string<R: Borrow<FluentResource>, M>(&self, scope: &Scope<R, M>) -> Cow<'source, str>
+ where
+ M: MemoizerKind,
+ {
+ if let Some(formatter) = &scope.bundle.formatter {
+ if let Some(val) = formatter(self, &scope.bundle.intls) {
+ return val.into();
+ }
+ }
+ match self {
+ FluentValue::String(s) => s.clone(),
+ FluentValue::Number(n) => n.as_string(),
+ FluentValue::Custom(s) => scope.bundle.intls.stringify_value(&**s),
+ FluentValue::Error => "".into(),
+ FluentValue::None => "".into(),
+ }
+ }
+}
+
+impl<'source> From<String> for FluentValue<'source> {
+ fn from(s: String) -> Self {
+ FluentValue::String(s.into())
+ }
+}
+
+impl<'source> From<&'source str> for FluentValue<'source> {
+ fn from(s: &'source str) -> Self {
+ FluentValue::String(s.into())
+ }
+}
+
+impl<'source> From<Cow<'source, str>> for FluentValue<'source> {
+ fn from(s: Cow<'source, str>) -> Self {
+ FluentValue::String(s)
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/types/number.rs b/third_party/rust/fluent-bundle/src/types/number.rs
new file mode 100644
index 0000000000..d39291ff46
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/types/number.rs
@@ -0,0 +1,252 @@
+use std::borrow::Cow;
+use std::convert::TryInto;
+use std::default::Default;
+use std::str::FromStr;
+
+use intl_pluralrules::operands::PluralOperands;
+
+use crate::args::FluentArgs;
+use crate::types::FluentValue;
+
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum FluentNumberStyle {
+ Decimal,
+ Currency,
+ Percent,
+}
+
+impl std::default::Default for FluentNumberStyle {
+ fn default() -> Self {
+ Self::Decimal
+ }
+}
+
+impl From<&str> for FluentNumberStyle {
+ fn from(input: &str) -> Self {
+ match input {
+ "decimal" => Self::Decimal,
+ "currency" => Self::Currency,
+ "percent" => Self::Percent,
+ _ => Self::default(),
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
+pub enum FluentNumberCurrencyDisplayStyle {
+ Symbol,
+ Code,
+ Name,
+}
+
+impl std::default::Default for FluentNumberCurrencyDisplayStyle {
+ fn default() -> Self {
+ Self::Symbol
+ }
+}
+
+impl From<&str> for FluentNumberCurrencyDisplayStyle {
+ fn from(input: &str) -> Self {
+ match input {
+ "symbol" => Self::Symbol,
+ "code" => Self::Code,
+ "name" => Self::Name,
+ _ => Self::default(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub struct FluentNumberOptions {
+ pub style: FluentNumberStyle,
+ pub currency: Option<String>,
+ pub currency_display: FluentNumberCurrencyDisplayStyle,
+ pub use_grouping: bool,
+ pub minimum_integer_digits: Option<usize>,
+ pub minimum_fraction_digits: Option<usize>,
+ pub maximum_fraction_digits: Option<usize>,
+ pub minimum_significant_digits: Option<usize>,
+ pub maximum_significant_digits: Option<usize>,
+}
+
+impl Default for FluentNumberOptions {
+ fn default() -> Self {
+ Self {
+ style: Default::default(),
+ currency: None,
+ currency_display: Default::default(),
+ use_grouping: true,
+ minimum_integer_digits: None,
+ minimum_fraction_digits: None,
+ maximum_fraction_digits: None,
+ minimum_significant_digits: None,
+ maximum_significant_digits: None,
+ }
+ }
+}
+
+impl FluentNumberOptions {
+ pub fn merge(&mut self, opts: &FluentArgs) {
+ for (key, value) in opts.iter() {
+ match (key, value) {
+ ("style", FluentValue::String(n)) => {
+ self.style = n.as_ref().into();
+ }
+ ("currency", FluentValue::String(n)) => {
+ self.currency = Some(n.to_string());
+ }
+ ("currencyDisplay", FluentValue::String(n)) => {
+ self.currency_display = n.as_ref().into();
+ }
+ ("useGrouping", FluentValue::String(n)) => {
+ self.use_grouping = n != "false";
+ }
+ ("minimumIntegerDigits", FluentValue::Number(n)) => {
+ self.minimum_integer_digits = Some(n.into());
+ }
+ ("minimumFractionDigits", FluentValue::Number(n)) => {
+ self.minimum_fraction_digits = Some(n.into());
+ }
+ ("maximumFractionDigits", FluentValue::Number(n)) => {
+ self.maximum_fraction_digits = Some(n.into());
+ }
+ ("minimumSignificantDigits", FluentValue::Number(n)) => {
+ self.minimum_significant_digits = Some(n.into());
+ }
+ ("maximumSignificantDigits", FluentValue::Number(n)) => {
+ self.maximum_significant_digits = Some(n.into());
+ }
+ _ => {}
+ }
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct FluentNumber {
+ pub value: f64,
+ pub options: FluentNumberOptions,
+}
+
+impl FluentNumber {
+ pub const fn new(value: f64, options: FluentNumberOptions) -> Self {
+ Self { value, options }
+ }
+
+ pub fn as_string(&self) -> Cow<'static, str> {
+ let mut val = self.value.to_string();
+ if let Some(minfd) = self.options.minimum_fraction_digits {
+ if let Some(pos) = val.find('.') {
+ let frac_num = val.len() - pos - 1;
+ let missing = if frac_num > minfd {
+ 0
+ } else {
+ minfd - frac_num
+ };
+ val = format!("{}{}", val, "0".repeat(missing));
+ } else {
+ val = format!("{}.{}", val, "0".repeat(minfd));
+ }
+ }
+ val.into()
+ }
+}
+
+impl FromStr for FluentNumber {
+ type Err = std::num::ParseFloatError;
+
+ fn from_str(input: &str) -> Result<Self, Self::Err> {
+ f64::from_str(input).map(|n| {
+ let mfd = input.find('.').map(|pos| input.len() - pos - 1);
+ let opts = FluentNumberOptions {
+ minimum_fraction_digits: mfd,
+ ..Default::default()
+ };
+ Self::new(n, opts)
+ })
+ }
+}
+
+impl<'l> From<FluentNumber> for FluentValue<'l> {
+ fn from(input: FluentNumber) -> Self {
+ FluentValue::Number(input)
+ }
+}
+
+macro_rules! from_num {
+ ($num:ty) => {
+ impl From<$num> for FluentNumber {
+ fn from(n: $num) -> Self {
+ Self {
+ value: n as f64,
+ options: FluentNumberOptions::default(),
+ }
+ }
+ }
+ impl From<&$num> for FluentNumber {
+ fn from(n: &$num) -> Self {
+ Self {
+ value: *n as f64,
+ options: FluentNumberOptions::default(),
+ }
+ }
+ }
+ impl From<FluentNumber> for $num {
+ fn from(input: FluentNumber) -> Self {
+ input.value as $num
+ }
+ }
+ impl From<&FluentNumber> for $num {
+ fn from(input: &FluentNumber) -> Self {
+ input.value as $num
+ }
+ }
+ impl From<$num> for FluentValue<'_> {
+ fn from(n: $num) -> Self {
+ FluentValue::Number(n.into())
+ }
+ }
+ impl From<&$num> for FluentValue<'_> {
+ fn from(n: &$num) -> Self {
+ FluentValue::Number(n.into())
+ }
+ }
+ };
+ ($($num:ty)+) => {
+ $(from_num!($num);)+
+ };
+}
+
+impl From<&FluentNumber> for PluralOperands {
+ fn from(input: &FluentNumber) -> Self {
+ let mut operands: Self = input
+ .value
+ .try_into()
+ .expect("Failed to generate operands out of FluentNumber");
+ if let Some(mfd) = input.options.minimum_fraction_digits {
+ if mfd > operands.v {
+ operands.f *= 10_u64.pow(mfd as u32 - operands.v as u32);
+ operands.v = mfd;
+ }
+ }
+ // XXX: Add support for other options.
+ operands
+ }
+}
+
+from_num!(i8 i16 i32 i64 i128 isize);
+from_num!(u8 u16 u32 u64 u128 usize);
+from_num!(f32 f64);
+
+#[cfg(test)]
+mod tests {
+ use crate::types::FluentValue;
+
+ #[test]
+ fn value_from_copy_ref() {
+ let x = 1i16;
+ let y = &x;
+ let z: FluentValue = y.into();
+ assert_eq!(z, FluentValue::try_number(1));
+ }
+}
diff --git a/third_party/rust/fluent-bundle/src/types/plural.rs b/third_party/rust/fluent-bundle/src/types/plural.rs
new file mode 100644
index 0000000000..1151fd6d36
--- /dev/null
+++ b/third_party/rust/fluent-bundle/src/types/plural.rs
@@ -0,0 +1,22 @@
+use fluent_langneg::{negotiate_languages, NegotiationStrategy};
+use intl_memoizer::Memoizable;
+use intl_pluralrules::{PluralRuleType, PluralRules as IntlPluralRules};
+use unic_langid::LanguageIdentifier;
+
+pub struct PluralRules(pub IntlPluralRules);
+
+impl Memoizable for PluralRules {
+ type Args = (PluralRuleType,);
+ type Error = &'static str;
+ fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
+ let default_lang: LanguageIdentifier = "en".parse().unwrap();
+ let pr_lang = negotiate_languages(
+ &[lang],
+ &IntlPluralRules::get_locales(args.0),
+ Some(&default_lang),
+ NegotiationStrategy::Lookup,
+ )[0]
+ .clone();
+ Ok(Self(IntlPluralRules::create(pr_lang, args.0)?))
+ }
+}
diff --git a/third_party/rust/fluent-fallback/.cargo-checksum.json b/third_party/rust/fluent-fallback/.cargo-checksum.json
new file mode 100644
index 0000000000..fd4e643540
--- /dev/null
+++ b/third_party/rust/fluent-fallback/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"CHANGELOG.md":"009535601424c3994d30788b8baa50b3527c5c36e26c882e87f023ac23feaee6","Cargo.lock":"ed57b6934296727509b6cf3379bc5039ff63a28f936eac8ec972428aec569793","Cargo.toml":"6ed10beb1162c35f9ff8d0027b1e4a89f4363fda0fe0a361627d3a8580161829","LICENSE-APACHE":"5db2b182453ff32ed40f7da63589c9667a3f8bd8b16b1471b152caae56f77e45","LICENSE-MIT":"49c0b000c03731d9e3970dc059ad4ca345d773681f4a612b0024435b663e0220","README.md":"f722df51c6c20153f073b5dda208b2a23e1d6a6351af0d5dac8dd35090c10b1f","examples/resources/en-US/simple.ftl":"55e8a72973d239c6ed3eb3d9cbc21d37dc90cea9fad85d1d8d73c96d63941629","examples/resources/pl/simple.ftl":"d63d7c62c225897d9f28f166c17e038b8f780dca9e9ee640e81360f23219a212","examples/simple-fallback.rs":"c61dc1a42b09bda3137bb72a08205db4630f47ce6faf9e0aceca4cade02422af","src/bundles.rs":"0696cff42b360fd018746d96136af2a3c38b7b3fdd681642168db8f915f729e3","src/cache.rs":"d0e886b95999120baf513d0438491c68cf37e9f99c515cdcf250ffc8f263f439","src/env.rs":"c11b27dcaf76a0f69d31007f8027cbdc6fb1330082afd28cf9f30aeb0352d127","src/errors.rs":"7424e9cc2cabc20cd987901a663671bfe063749547d271f47ddf67c3d12e1646","src/generator.rs":"f0c9f71baa3ef0e0f205692eb8719390234f7c4d609614b86969d69b8cee52de","src/lib.rs":"f8ee9e689af6d35faa8a2c49b73d3d41c6c27ac655ea0cce045d684e5c3307e4","src/localization.rs":"3fa84b81308e890d9a013e0b05cde7f7811e4f708ca5e5fa9e24ec67315de33c","src/pin_cell/README.md":"b230e479f0ce5de00ce6638aa47cdf1bd30a934df5f3ad33523c3b9f16ffb02b","src/pin_cell/mod.rs":"7247334eb4c6753babe8aeceacf1b36bdbbd60aed86754da61e09e87f1da24d1","src/pin_cell/pin_mut.rs":"116d0ac2353fbc4d2d1084610f90a9aa414b4607bb01595528025ed49889127c","src/pin_cell/pin_ref.rs":"e67ef14faf7d1d47e082732d3a5446e4ae78a25b9577f0819faa1705b236f01d","src/types.rs":"dbe93bf1cfec9b1e28612124c13d37cb0c1e1ba405ce4f4bd16cf3dde98cbb01","tests/localization_test.rs":"c98a27cf8fff60790a67447da584851201a4cadc51482bc81a8c950391513069","tests/resources/en-US/test.ftl":"1103dafb98582e728ddcd30b3fa8ffc1b9d4231cc86296f4e2d8865e9d40b25d","tests/resources/en-US/test2.ftl":"821c99ed74f57635e02877fccf6c2ac9e388997e646c850cd0f86cbd3238b490","tests/resources/pl/test.ftl":"5f7f7a9a8ef2c7175c2a9e2d68ff3748667e8ede877bb6b8a72337ac24e5dfeb","tests/resources/pl/test2.ftl":"68550e8e37adfb49c03a95e6b0a6501d58fbfb6e498cda00b52fc258758245b9"},"package":"08fdcccdeb6c01cb085f2bb3420506e6c67f025cee5db047529838c673a7d82b"} \ No newline at end of file
diff --git a/third_party/rust/fluent-fallback/CHANGELOG.md b/third_party/rust/fluent-fallback/CHANGELOG.md
new file mode 100644
index 0000000000..1c6598c441
--- /dev/null
+++ b/third_party/rust/fluent-fallback/CHANGELOG.md
@@ -0,0 +1,68 @@
+# Changelog
+
+## Unreleased
+
+ - …
+
+## fluent-fallback 0.7.0 (Nov 9, 2022)
+ - The `ResourceId`s are now stored as a `HashSet` rather than as a Vec. Adding a
+ duplicate `ResourceId` is now a noop.
+
+## fluent-fallback 0.6.0 (Dec 17, 2021)
+ - Add `ResourceId` struct which allows fluent resources to be optional.
+
+## fluent-fallback 0.5.0 (Jul 8, 2021)
+ - Separate out `Bundles` for state management.
+
+## fluent-fallback 0.4.4 (May 3, 2021)
+ - Fix waiting from multiple tasks. (#224)
+ - Bind locale iterator generics of `LocalesProvider` and `BundleGenerator`.
+
+## fluent-fallback 0.4.3 (April 26, 2021)
+ - Align errors even closer to fluent.js
+
+## fluent-fallback 0.4.2 (April 9, 2021)
+ - Align errors closer to fluent.js
+
+## fluent-fallback 0.4.0 (February 9, 2021)
+ - Use `fluent-bundle` 0.15.
+
+## fluent-fallback 0.3.0 (February 3, 2021)
+ - Handle locale management in `Localization`.
+
+## fluent-fallback 0.2.2 (January 16, 2021)
+ - Invalidate bundles on resource list change.
+
+## fluent-fallback 0.2.1 (January 15, 2021)
+ - Add `Localization::is_sync`
+
+## fluent-fallback 0.2.0 (January 12, 2021)
+ - Separate `Sync` and `Async` bundle generators.
+ - Reorganize fallback logic.
+ - Separate out prefetching trait.
+ - Vendor in pin-cell.
+
+## fluent-fallback 0.1.0 (January 3, 2021)
+ - Update `fluent-bundle` to 0.14.
+ - Switch from `elsa` to `chunky-vec`.
+ - Add `Localization::with_generator`.
+ - Add support for Streamed bundles.
+ - Add `LocalizationError`.
+ - Make `L10nKey`, `L10nMessage` and `L10nAttribute` types.
+
+## fluent-fallback 0.0.4 (May 6, 2020)
+ - Update `fluent-bundle` to 0.12.
+ - Update `unic-langid` to 0.9.
+
+## fluent-fallback 0.0.3 (February 13, 2020)
+ - Update `fluent-bundle` to 0.10.
+ - Update `unic-langid` to 0.8.
+
+## fluent-fallback 0.0.2 (November 26, 2019)
+ - Update `fluent-bundle` to 0.9.
+ - Update `unic-langid` to 0.7.
+
+## fluent-fallback 0.0.1 (August 1, 2019)
+
+ - This is the first release to be listed in the CHANGELOG.
+ - Basic support for language fallbacking and runtime locale changes.
diff --git a/third_party/rust/fluent-fallback/Cargo.lock b/third_party/rust/fluent-fallback/Cargo.lock
new file mode 100644
index 0000000000..f16220d211
--- /dev/null
+++ b/third_party/rust/fluent-fallback/Cargo.lock
@@ -0,0 +1,415 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "async-trait"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "chunky-vec"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb7bdea464ae038f09197b82430b921c53619fc8d2bcaf7b151013b3ca008017"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "fluent-bundle"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd"
+dependencies = [
+ "fluent-langneg",
+ "fluent-syntax",
+ "intl-memoizer",
+ "intl_pluralrules",
+ "rustc-hash",
+ "self_cell",
+ "smallvec",
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-fallback"
+version = "0.7.0"
+dependencies = [
+ "async-trait",
+ "chunky-vec",
+ "fluent-bundle",
+ "fluent-langneg",
+ "futures",
+ "once_cell",
+ "rustc-hash",
+ "tokio",
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-langneg"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94"
+dependencies = [
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-syntax"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
+
+[[package]]
+name = "futures-task"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
+
+[[package]]
+name = "futures-util"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "intl-memoizer"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f"
+dependencies = [
+ "type-map",
+ "unic-langid",
+]
+
+[[package]]
+name = "intl_pluralrules"
+version = "7.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
+dependencies = [
+ "unic-langid",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "num_cpus"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "self_cell"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af"
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "syn"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8aeafdfd935e4a7fe16a91ab711fa52d54df84f9c8f7ca5837a9d1d902ef4c2"
+dependencies = [
+ "displaydoc",
+]
+
+[[package]]
+name = "tokio"
+version = "1.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
+dependencies = [
+ "autocfg",
+ "num_cpus",
+ "pin-project-lite",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "type-map"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46"
+dependencies = [
+ "rustc-hash",
+]
+
+[[package]]
+name = "unic-langid"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "398f9ad7239db44fd0f80fe068d12ff22d78354080332a5077dc6f52f14dcf2f"
+dependencies = [
+ "unic-langid-impl",
+ "unic-langid-macros",
+]
+
+[[package]]
+name = "unic-langid-impl"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff"
+dependencies = [
+ "tinystr",
+]
+
+[[package]]
+name = "unic-langid-macros"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "055e618bf694161ffff0466d95cef3e1a5edc59f6ba1888e97801f2b4ebdc4fe"
+dependencies = [
+ "proc-macro-hack",
+ "tinystr",
+ "unic-langid-impl",
+ "unic-langid-macros-impl",
+]
+
+[[package]]
+name = "unic-langid-macros-impl"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f5cdec05b907f4e2f6843f4354f4ce6a5bebe1a56df320a49134944477ce4d8"
+dependencies = [
+ "proc-macro-hack",
+ "quote",
+ "syn",
+ "unic-langid-impl",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
diff --git a/third_party/rust/fluent-fallback/Cargo.toml b/third_party/rust/fluent-fallback/Cargo.toml
new file mode 100644
index 0000000000..105de93998
--- /dev/null
+++ b/third_party/rust/fluent-fallback/Cargo.toml
@@ -0,0 +1,74 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+name = "fluent-fallback"
+version = "0.7.0"
+authors = [
+ "Zibi Braniecki <gandalf@mozilla.com>",
+ "Staś Małolepszy <stas@mozilla.com>",
+]
+description = """
+High-level abstraction model for managing localization resources
+and runtime localization lifecycle.
+"""
+homepage = "http://www.projectfluent.org"
+readme = "README.md"
+keywords = [
+ "localization",
+ "l10n",
+ "i18n",
+ "intl",
+ "internationalization",
+]
+categories = [
+ "localization",
+ "internationalization",
+]
+license = "Apache-2.0/MIT"
+repository = "https://github.com/projectfluent/fluent-rs"
+resolver = "1"
+
+[dependencies.async-trait]
+version = "0.1"
+
+[dependencies.chunky-vec]
+version = "0.1"
+
+[dependencies.fluent-bundle]
+version = "0.15.2"
+
+[dependencies.futures]
+version = "0.3"
+
+[dependencies.once_cell]
+version = "1.9"
+
+[dependencies.rustc-hash]
+version = "1"
+
+[dependencies.unic-langid]
+version = "0.9"
+
+[dev-dependencies.fluent-langneg]
+version = "0.13"
+
+[dev-dependencies.tokio]
+version = "1.0"
+features = [
+ "rt-multi-thread",
+ "macros",
+]
+
+[dev-dependencies.unic-langid]
+version = "0.9"
+features = ["macros"]
diff --git a/third_party/rust/fluent-fallback/LICENSE-APACHE b/third_party/rust/fluent-fallback/LICENSE-APACHE
new file mode 100644
index 0000000000..35582f166b
--- /dev/null
+++ b/third_party/rust/fluent-fallback/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017 Mozilla
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/third_party/rust/fluent-fallback/LICENSE-MIT b/third_party/rust/fluent-fallback/LICENSE-MIT
new file mode 100644
index 0000000000..5655fa311c
--- /dev/null
+++ b/third_party/rust/fluent-fallback/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright 2017 Mozilla
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/rust/fluent-fallback/README.md b/third_party/rust/fluent-fallback/README.md
new file mode 100644
index 0000000000..25f9350f2e
--- /dev/null
+++ b/third_party/rust/fluent-fallback/README.md
@@ -0,0 +1,102 @@
+# Fluent
+
+`fluent-fallback` is a Rust implementation of the [Project Fluent][] higher level API.
+
+The `Localization` struct encapsulates a persistant localization context providing
+language fallbacking. The instance remains available throughout the whole life cycle of
+the corresponding UI, reacting to events such as locale changes, resource updates etc.
+
+The API can be used directly, or can serve as an example of state manager for `fluent-bundle` and `fluent-resmgr`.
+
+[![crates.io](https://img.shields.io/crates/v/fluent-fallback.svg)](https://crates.io/crates/fluent-fallback)
+[![Build and test](https://github.com/projectfluent/fluent-rs/workflows/Build%20and%20test/badge.svg)](https://github.com/projectfluent/fluent-rs/actions?query=branch%3Amaster+workflow%3A%22Build+and+test%22)
+[![Coverage Status](https://coveralls.io/repos/github/projectfluent/fluent-rs/badge.svg?branch=master)](https://coveralls.io/github/projectfluent/fluent-rs?branch=master)
+
+Project Fluent keeps simple things simple and makes complex things possible.
+The syntax used for describing translations is easy to read and understand. At
+the same time it allows, when necessary, to represent complex concepts from
+natural languages like gender, plurals, conjugations, and others.
+
+[Documentation][]
+
+[Project Fluent]: http://projectfluent.org
+[Documentation]: https://docs.rs/fluent/
+
+Usage
+-----
+
+```rust
+use fluent_fallback::Localization;
+
+fn main() {
+ // generate_messages is a closure that returns an iterator over FluentBundle
+ // instances.
+ let loc = Localization::new(vec!["simple.ftl".into()], generate_messages);
+
+ let value = bundle.format_value("hello-world", None);
+
+ assert_eq!(&value, "Hello, world!");
+}
+```
+
+
+Status
+------
+
+The implementation is in its early stages and supports only some of the Project
+Fluent's spec. Consult the [list of milestones][] for more information about
+release planning and scope.
+
+[list of milestones]: https://github.com/projectfluent/fluent-rs/milestones
+
+
+Local Development
+-----------------
+
+ cargo build
+ cargo test
+ cargo run --example simple-fallback
+
+When submitting a PR please use [`cargo fmt`][] (nightly).
+
+[`cargo fmt`]: https://github.com/rust-lang-nursery/rustfmt
+
+
+Learn the FTL syntax
+--------------------
+
+FTL is a localization file format used for describing translation resources.
+FTL stands for _Fluent Translation List_.
+
+FTL is designed to be simple to read, but at the same time allows to represent
+complex concepts from natural languages like gender, plurals, conjugations, and
+others.
+
+ hello-user = Hello, { $username }!
+
+[Read the Fluent Syntax Guide][] in order to learn more about the syntax. If
+you're a tool author you may be interested in the formal [EBNF grammar][].
+
+[Read the Fluent Syntax Guide]: http://projectfluent.org/fluent/guide/
+[EBNF grammar]: https://github.com/projectfluent/fluent/tree/master/spec
+
+
+Get Involved
+------------
+
+`fluent-rs` is open-source, licensed under the Apache License, Version 2.0. We
+encourage everyone to take a look at our code and we'll listen to your
+feedback.
+
+
+Discuss
+-------
+
+We'd love to hear your thoughts on Project Fluent! Whether you're a localizer
+looking for a better way to express yourself in your language, or a developer
+trying to make your app localizable and multilingual, or a hacker looking for
+a project to contribute to, please do get in touch on the mailing list and the
+IRC channel.
+
+ - Discourse: https://discourse.mozilla.org/c/fluent
+ - IRC channel: [irc://irc.mozilla.org/l20n](irc://irc.mozilla.org/l20n)
diff --git a/third_party/rust/fluent-fallback/examples/resources/en-US/simple.ftl b/third_party/rust/fluent-fallback/examples/resources/en-US/simple.ftl
new file mode 100644
index 0000000000..99f0a6bb6f
--- /dev/null
+++ b/third_party/rust/fluent-fallback/examples/resources/en-US/simple.ftl
@@ -0,0 +1,7 @@
+missing-arg-error = Error: Please provide a number as argument.
+input-parse-error = Error: Could not parse input `{ $input }`. Reason: { $reason }
+response-msg =
+ { $value ->
+ [one] "{ $input }" has one Collatz step.
+ *[other] "{ $input }" has { $value } Collatz steps.
+ }
diff --git a/third_party/rust/fluent-fallback/examples/resources/pl/simple.ftl b/third_party/rust/fluent-fallback/examples/resources/pl/simple.ftl
new file mode 100644
index 0000000000..16173dd92e
--- /dev/null
+++ b/third_party/rust/fluent-fallback/examples/resources/pl/simple.ftl
@@ -0,0 +1,8 @@
+missing-arg-error = Błąd: Proszę wprowadzić liczbę jako argument.
+input-parse-error = Błąd: Nie udało się sparsować `{ $input }`. Powód: { $reason }
+response-msg =
+ { $value ->
+ [one] "{ $input }" ma jeden krok Collatza.
+ [few] "{ $input }" ma { $value } kroki Collatza.
+ *[many] "{ $input }" ma { $value } kroków Collatza.
+ }
diff --git a/third_party/rust/fluent-fallback/examples/simple-fallback.rs b/third_party/rust/fluent-fallback/examples/simple-fallback.rs
new file mode 100644
index 0000000000..efdc04af2c
--- /dev/null
+++ b/third_party/rust/fluent-fallback/examples/simple-fallback.rs
@@ -0,0 +1,237 @@
+//! This is an example of a simple application
+//! which calculates the Collatz conjecture.
+//!
+//! The function itself is trivial on purpose,
+//! so that we can focus on understanding how
+//! the application can be made localizable
+//! via Fluent.
+//!
+//! To try the app launch `cargo run --example simple-fallback NUM (LOCALES)`
+//!
+//! NUM is a number to be calculated, and LOCALES is an optional
+//! parameter with a comma-separated list of locales requested by the user.
+//!
+//! Example:
+//!
+//! cargo run --example simple-fallback 123 de,pl
+//!
+//! If the second argument is omitted, `en-US` locale is used as the
+//! default one.
+
+use std::{env, fs, io, path::PathBuf, str::FromStr};
+
+use fluent_bundle::{FluentArgs, FluentBundle, FluentResource};
+use fluent_fallback::{
+ generator::{BundleGenerator, FluentBundleResult},
+ types::ResourceId,
+ Localization,
+};
+use fluent_langneg::{negotiate_languages, NegotiationStrategy};
+
+use rustc_hash::FxHashSet;
+use unic_langid::{langid, LanguageIdentifier};
+
+/// This helper struct holds the scheme for converting
+/// resource paths into full paths. It is used to customise
+/// `fluent-fallback::SyncLocalization`.
+struct Bundles {
+ res_path_scheme: PathBuf,
+}
+
+/// This helper function allows us to read the list
+/// of available locales by reading the list of
+/// directories in `./examples/resources`.
+///
+/// It is expected that every directory inside it
+/// has a name that is a valid BCP47 language tag.
+fn get_available_locales() -> io::Result<Vec<LanguageIdentifier>> {
+ let mut dir = env::current_dir()?;
+ if dir.to_string_lossy().ends_with("fluent-rs") {
+ dir.push("fluent-fallback");
+ }
+ dir.push("examples");
+ dir.push("resources");
+ let res_dir = fs::read_dir(dir)?;
+
+ let locales = res_dir
+ .into_iter()
+ .filter_map(|entry| entry.ok())
+ .filter(|entry| entry.path().is_dir())
+ .filter_map(|dir| {
+ let file_name = dir.file_name();
+ let name = file_name.to_str()?;
+ Some(name.parse().expect("Parsing failed."))
+ })
+ .collect();
+ Ok(locales)
+}
+
+fn resolve_app_locales<'l>(args: &[String]) -> Vec<LanguageIdentifier> {
+ let default_locale = langid!("en-US");
+ let available = get_available_locales().expect("Retrieving available locales failed.");
+
+ let requested: Vec<LanguageIdentifier> = args.get(2).map_or(vec![], |arg| {
+ arg.split(",")
+ .map(|s| s.parse().expect("Parsing locale failed."))
+ .collect()
+ });
+
+ negotiate_languages(
+ &requested,
+ &available,
+ Some(&default_locale),
+ NegotiationStrategy::Filtering,
+ )
+ .into_iter()
+ .cloned()
+ .collect()
+}
+
+fn get_resource_manager() -> Bundles {
+ let mut res_path_scheme = env::current_dir().expect("Failed to retrieve current dir.");
+
+ if res_path_scheme.to_string_lossy().ends_with("fluent-rs") {
+ res_path_scheme.push("fluent-fallback");
+ }
+ res_path_scheme.push("examples");
+ res_path_scheme.push("resources");
+
+ res_path_scheme.push("{locale}");
+ res_path_scheme.push("{res_id}");
+
+ Bundles { res_path_scheme }
+}
+
+static L10N_RESOURCES: &[&str] = &["simple.ftl"];
+
+fn main() {
+ let args: Vec<String> = env::args().collect();
+
+ let app_locales: Vec<LanguageIdentifier> = resolve_app_locales(&args);
+
+ let bundles = get_resource_manager();
+
+ let loc = Localization::with_env(
+ L10N_RESOURCES.iter().map(|&res| res.into()),
+ true,
+ app_locales,
+ bundles,
+ );
+ let bundles = loc.bundles();
+
+ let mut errors = vec![];
+
+ match args.get(1) {
+ Some(input) => match isize::from_str(&input) {
+ Ok(i) => {
+ let mut args = FluentArgs::new();
+ args.set("input", i);
+ args.set("value", collatz(i));
+ let value = bundles
+ .format_value_sync("response-msg", Some(&args), &mut errors)
+ .unwrap()
+ .unwrap();
+ println!("{}", value);
+ }
+ Err(err) => {
+ let mut args = FluentArgs::new();
+ args.set("input", input.as_str());
+ args.set("reason", err.to_string());
+ let value = bundles
+ .format_value_sync("input-parse-error-msg", Some(&args), &mut errors)
+ .unwrap()
+ .unwrap();
+ println!("{}", value);
+ }
+ },
+ None => {
+ let value = bundles
+ .format_value_sync("missing-arg-error", None, &mut errors)
+ .unwrap()
+ .unwrap();
+ println!("{}", value);
+ }
+ }
+}
+
+/// Collatz conjecture calculating function.
+fn collatz(n: isize) -> isize {
+ match n {
+ 1 => 0,
+ _ => match n % 2 {
+ 0 => 1 + collatz(n / 2),
+ _ => 1 + collatz(n * 3 + 1),
+ },
+ }
+}
+
+/// Bundle iterator used by BundleGeneratorSync implementation for Locales.
+struct BundleIter {
+ res_path_scheme: String,
+ locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
+ res_ids: FxHashSet<ResourceId>,
+}
+
+impl Iterator for BundleIter {
+ type Item = FluentBundleResult<FluentResource>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let locale = self.locales.next()?;
+ let res_path_scheme = self
+ .res_path_scheme
+ .as_str()
+ .replace("{locale}", &locale.to_string());
+ let mut bundle = FluentBundle::new(vec![locale]);
+
+ let mut errors = vec![];
+
+ for res_id in &self.res_ids {
+ let res_path = res_path_scheme.as_str().replace("{res_id}", &res_id.value);
+ let source = fs::read_to_string(res_path).unwrap();
+ let res = match FluentResource::try_new(source) {
+ Ok(res) => res,
+ Err((res, err)) => {
+ errors.extend(err.into_iter().map(Into::into));
+ res
+ }
+ };
+ bundle.add_resource(res).unwrap();
+ }
+
+ if errors.is_empty() {
+ Some(Ok(bundle))
+ } else {
+ Some(Err((bundle, errors)))
+ }
+ }
+}
+
+impl futures::Stream for BundleIter {
+ type Item = FluentBundleResult<FluentResource>;
+
+ fn poll_next(
+ self: std::pin::Pin<&mut Self>,
+ _cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll<Option<Self::Item>> {
+ todo!()
+ }
+}
+
+impl BundleGenerator for Bundles {
+ type Resource = FluentResource;
+ type LocalesIter = std::vec::IntoIter<LanguageIdentifier>;
+ type Iter = BundleIter;
+ type Stream = BundleIter;
+
+ fn bundles_iter(
+ &self,
+ locales: std::vec::IntoIter<LanguageIdentifier>,
+ res_ids: FxHashSet<ResourceId>,
+ ) -> Self::Iter {
+ BundleIter {
+ res_path_scheme: self.res_path_scheme.to_string_lossy().to_string(),
+ locales,
+ res_ids,
+ }
+ }
+}
diff --git a/third_party/rust/fluent-fallback/src/bundles.rs b/third_party/rust/fluent-fallback/src/bundles.rs
new file mode 100644
index 0000000000..7ab726d684
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/bundles.rs
@@ -0,0 +1,426 @@
+use crate::{
+ cache::{AsyncCache, Cache},
+ env::LocalesProvider,
+ errors::LocalizationError,
+ generator::{BundleGenerator, BundleIterator, BundleStream},
+ types::{L10nAttribute, L10nKey, L10nMessage, ResourceId},
+};
+use fluent_bundle::{FluentArgs, FluentBundle, FluentError};
+use rustc_hash::FxHashSet;
+use std::borrow::Cow;
+
+pub enum BundlesInner<G>
+where
+ G: BundleGenerator,
+{
+ Iter(Cache<G::Iter, G::Resource>),
+ Stream(AsyncCache<G::Stream, G::Resource>),
+}
+
+pub struct Bundles<G>(BundlesInner<G>)
+where
+ G: BundleGenerator;
+
+impl<G> Bundles<G>
+where
+ G: BundleGenerator,
+ G::Iter: BundleIterator,
+{
+ pub fn prefetch_sync(&self) {
+ match &self.0 {
+ BundlesInner::Iter(iter) => iter.prefetch(),
+ BundlesInner::Stream(_) => panic!("Can't prefetch a sync bundle set asynchronously"),
+ }
+ }
+}
+
+impl<G> Bundles<G>
+where
+ G: BundleGenerator,
+ G::Stream: BundleStream,
+{
+ pub async fn prefetch_async(&self) {
+ match &self.0 {
+ BundlesInner::Iter(_) => panic!("Can't prefetch a async bundle set synchronously"),
+ BundlesInner::Stream(stream) => stream.prefetch().await,
+ }
+ }
+}
+
+impl<G> Bundles<G>
+where
+ G: BundleGenerator,
+{
+ pub fn new<P>(sync: bool, res_ids: FxHashSet<ResourceId>, generator: &G, provider: &P) -> Self
+ where
+ G: BundleGenerator<LocalesIter = P::Iter>,
+ P: LocalesProvider,
+ {
+ Self(if sync {
+ BundlesInner::Iter(Cache::new(
+ generator.bundles_iter(provider.locales(), res_ids),
+ ))
+ } else {
+ BundlesInner::Stream(AsyncCache::new(
+ generator.bundles_stream(provider.locales(), res_ids),
+ ))
+ })
+ }
+
+ pub async fn format_value<'l>(
+ &'l self,
+ id: &'l str,
+ args: Option<&'l FluentArgs<'_>>,
+ errors: &mut Vec<LocalizationError>,
+ ) -> Option<Cow<'l, str>> {
+ match &self.0 {
+ BundlesInner::Iter(cache) => Self::format_value_from_iter(cache, id, args, errors),
+ BundlesInner::Stream(stream) => {
+ Self::format_value_from_stream(stream, id, args, errors).await
+ }
+ }
+ }
+
+ pub async fn format_values<'l>(
+ &'l self,
+ keys: &'l [L10nKey<'l>],
+ errors: &mut Vec<LocalizationError>,
+ ) -> Vec<Option<Cow<'l, str>>> {
+ match &self.0 {
+ BundlesInner::Iter(cache) => Self::format_values_from_iter(cache, keys, errors),
+ BundlesInner::Stream(stream) => {
+ Self::format_values_from_stream(stream, keys, errors).await
+ }
+ }
+ }
+
+ pub async fn format_messages<'l>(
+ &'l self,
+ keys: &'l [L10nKey<'l>],
+ errors: &mut Vec<LocalizationError>,
+ ) -> Vec<Option<L10nMessage<'l>>> {
+ match &self.0 {
+ BundlesInner::Iter(cache) => Self::format_messages_from_iter(cache, keys, errors),
+ BundlesInner::Stream(stream) => {
+ Self::format_messages_from_stream(stream, keys, errors).await
+ }
+ }
+ }
+
+ pub fn format_value_sync<'l>(
+ &'l self,
+ id: &'l str,
+ args: Option<&'l FluentArgs>,
+ errors: &mut Vec<LocalizationError>,
+ ) -> Result<Option<Cow<'l, str>>, LocalizationError> {
+ match &self.0 {
+ BundlesInner::Iter(cache) => Ok(Self::format_value_from_iter(cache, id, args, errors)),
+ BundlesInner::Stream(_) => Err(LocalizationError::SyncRequestInAsyncMode),
+ }
+ }
+
+ pub fn format_values_sync<'l>(
+ &'l self,
+ keys: &'l [L10nKey<'l>],
+ errors: &mut Vec<LocalizationError>,
+ ) -> Result<Vec<Option<Cow<'l, str>>>, LocalizationError> {
+ match &self.0 {
+ BundlesInner::Iter(cache) => Ok(Self::format_values_from_iter(cache, keys, errors)),
+ BundlesInner::Stream(_) => Err(LocalizationError::SyncRequestInAsyncMode),
+ }
+ }
+
+ pub fn format_messages_sync<'l>(
+ &'l self,
+ keys: &'l [L10nKey<'l>],
+ errors: &mut Vec<LocalizationError>,
+ ) -> Result<Vec<Option<L10nMessage<'l>>>, LocalizationError> {
+ match &self.0 {
+ BundlesInner::Iter(cache) => Ok(Self::format_messages_from_iter(cache, keys, errors)),
+ BundlesInner::Stream(_) => Err(LocalizationError::SyncRequestInAsyncMode),
+ }
+ }
+}
+
+macro_rules! format_value_from_inner {
+ ($step:expr, $id:expr, $args:expr, $errors:expr) => {
+ let mut found_message = false;
+
+ while let Some(bundle) = $step {
+ let bundle = bundle.as_ref().unwrap_or_else(|(bundle, err)| {
+ $errors.extend(err.iter().cloned().map(Into::into));
+ bundle
+ });
+
+ if let Some(msg) = bundle.get_message($id) {
+ found_message = true;
+ if let Some(value) = msg.value() {
+ let mut format_errors = vec![];
+ let result = bundle.format_pattern(value, $args, &mut format_errors);
+ if !format_errors.is_empty() {
+ $errors.push(LocalizationError::Resolver {
+ id: $id.to_string(),
+ locale: bundle.locales[0].clone(),
+ errors: format_errors,
+ });
+ }
+ return Some(result);
+ } else {
+ $errors.push(LocalizationError::MissingValue {
+ id: $id.to_string(),
+ locale: Some(bundle.locales[0].clone()),
+ });
+ }
+ } else {
+ $errors.push(LocalizationError::MissingMessage {
+ id: $id.to_string(),
+ locale: Some(bundle.locales[0].clone()),
+ });
+ }
+ }
+ if found_message {
+ $errors.push(LocalizationError::MissingValue {
+ id: $id.to_string(),
+ locale: None,
+ });
+ } else {
+ $errors.push(LocalizationError::MissingMessage {
+ id: $id.to_string(),
+ locale: None,
+ });
+ }
+ return None;
+ };
+}
+
+#[derive(Clone)]
+enum Value<'l> {
+ Present(Cow<'l, str>),
+ Missing,
+ None,
+}
+
+macro_rules! format_values_from_inner {
+ ($step:expr, $keys:expr, $errors:expr) => {
+ let mut cells = vec![Value::None; $keys.len()];
+
+ while let Some(bundle) = $step {
+ let bundle = bundle.as_ref().unwrap_or_else(|(bundle, err)| {
+ $errors.extend(err.iter().cloned().map(Into::into));
+ bundle
+ });
+
+ let mut has_missing = false;
+
+ for (key, cell) in $keys
+ .iter()
+ .zip(&mut cells)
+ .filter(|(_, cell)| !matches!(cell, Value::Present(_)))
+ {
+ if let Some(msg) = bundle.get_message(&key.id) {
+ if let Some(value) = msg.value() {
+ let mut format_errors = vec![];
+ *cell = Value::Present(bundle.format_pattern(
+ value,
+ key.args.as_ref(),
+ &mut format_errors,
+ ));
+ if !format_errors.is_empty() {
+ $errors.push(LocalizationError::Resolver {
+ id: key.id.to_string(),
+ locale: bundle.locales[0].clone(),
+ errors: format_errors,
+ });
+ }
+ } else {
+ *cell = Value::Missing;
+ has_missing = true;
+ $errors.push(LocalizationError::MissingValue {
+ id: key.id.to_string(),
+ locale: Some(bundle.locales[0].clone()),
+ });
+ }
+ } else {
+ has_missing = true;
+ $errors.push(LocalizationError::MissingMessage {
+ id: key.id.to_string(),
+ locale: Some(bundle.locales[0].clone()),
+ });
+ }
+ }
+ if !has_missing {
+ break;
+ }
+ }
+
+ return $keys
+ .iter()
+ .zip(cells)
+ .map(|(key, value)| match value {
+ Value::Present(value) => Some(value),
+ Value::Missing => {
+ $errors.push(LocalizationError::MissingValue {
+ id: key.id.to_string(),
+ locale: None,
+ });
+ None
+ }
+ Value::None => {
+ $errors.push(LocalizationError::MissingMessage {
+ id: key.id.to_string(),
+ locale: None,
+ });
+ None
+ }
+ })
+ .collect();
+ };
+}
+
+macro_rules! format_messages_from_inner {
+ ($step:expr, $keys:expr, $errors:expr) => {
+ let mut result = vec![None; $keys.len()];
+
+ let mut is_complete = false;
+
+ while let Some(bundle) = $step {
+ let bundle = bundle.as_ref().unwrap_or_else(|(bundle, err)| {
+ $errors.extend(err.iter().cloned().map(Into::into));
+ bundle
+ });
+
+ let mut has_missing = false;
+ for (key, cell) in $keys
+ .iter()
+ .zip(&mut result)
+ .filter(|(_, cell)| cell.is_none())
+ {
+ let mut format_errors = vec![];
+ let msg = Self::format_message_from_bundle(bundle, key, &mut format_errors);
+
+ if msg.is_none() {
+ has_missing = true;
+ $errors.push(LocalizationError::MissingMessage {
+ id: key.id.to_string(),
+ locale: Some(bundle.locales[0].clone()),
+ });
+ } else if !format_errors.is_empty() {
+ $errors.push(LocalizationError::Resolver {
+ id: key.id.to_string(),
+ locale: bundle.locales.get(0).cloned().unwrap(),
+ errors: format_errors,
+ });
+ }
+
+ *cell = msg;
+ }
+ if !has_missing {
+ is_complete = true;
+ break;
+ }
+ }
+
+ if !is_complete {
+ for (key, _) in $keys
+ .iter()
+ .zip(&mut result)
+ .filter(|(_, cell)| cell.is_none())
+ {
+ $errors.push(LocalizationError::MissingMessage {
+ id: key.id.to_string(),
+ locale: None,
+ });
+ }
+ }
+
+ return result;
+ };
+}
+
+impl<G> Bundles<G>
+where
+ G: BundleGenerator,
+{
+ fn format_value_from_iter<'l>(
+ cache: &'l Cache<G::Iter, G::Resource>,
+ id: &'l str,
+ args: Option<&'l FluentArgs>,
+ errors: &mut Vec<LocalizationError>,
+ ) -> Option<Cow<'l, str>> {
+ let mut bundle_iter = cache.into_iter();
+ format_value_from_inner!(bundle_iter.next(), id, args, errors);
+ }
+
+ async fn format_value_from_stream<'l>(
+ stream: &'l AsyncCache<G::Stream, G::Resource>,
+ id: &'l str,
+ args: Option<&'l FluentArgs<'_>>,
+ errors: &mut Vec<LocalizationError>,
+ ) -> Option<Cow<'l, str>> {
+ use futures::StreamExt;
+
+ let mut bundle_stream = stream.stream();
+ format_value_from_inner!(bundle_stream.next().await, id, args, errors);
+ }
+
+ async fn format_messages_from_stream<'l>(
+ stream: &'l AsyncCache<G::Stream, G::Resource>,
+ keys: &'l [L10nKey<'l>],
+ errors: &mut Vec<LocalizationError>,
+ ) -> Vec<Option<L10nMessage<'l>>> {
+ use futures::StreamExt;
+ let mut bundle_stream = stream.stream();
+ format_messages_from_inner!(bundle_stream.next().await, keys, errors);
+ }
+
+ async fn format_values_from_stream<'l>(
+ stream: &'l AsyncCache<G::Stream, G::Resource>,
+ keys: &'l [L10nKey<'l>],
+ errors: &mut Vec<LocalizationError>,
+ ) -> Vec<Option<Cow<'l, str>>> {
+ use futures::StreamExt;
+ let mut bundle_stream = stream.stream();
+
+ format_values_from_inner!(bundle_stream.next().await, keys, errors);
+ }
+
+ fn format_message_from_bundle<'l>(
+ bundle: &'l FluentBundle<G::Resource>,
+ key: &'l L10nKey,
+ format_errors: &mut Vec<FluentError>,
+ ) -> Option<L10nMessage<'l>> {
+ let msg = bundle.get_message(&key.id)?;
+ let value = msg
+ .value()
+ .map(|pattern| bundle.format_pattern(pattern, key.args.as_ref(), format_errors));
+ let attributes = msg
+ .attributes()
+ .map(|attr| {
+ let value = bundle.format_pattern(attr.value(), key.args.as_ref(), format_errors);
+ L10nAttribute {
+ name: attr.id().into(),
+ value,
+ }
+ })
+ .collect();
+ Some(L10nMessage { value, attributes })
+ }
+
+ fn format_messages_from_iter<'l>(
+ cache: &'l Cache<G::Iter, G::Resource>,
+ keys: &'l [L10nKey<'l>],
+ errors: &mut Vec<LocalizationError>,
+ ) -> Vec<Option<L10nMessage<'l>>> {
+ let mut bundle_iter = cache.into_iter();
+ format_messages_from_inner!(bundle_iter.next(), keys, errors);
+ }
+
+ fn format_values_from_iter<'l>(
+ cache: &'l Cache<G::Iter, G::Resource>,
+ keys: &'l [L10nKey<'l>],
+ errors: &mut Vec<LocalizationError>,
+ ) -> Vec<Option<Cow<'l, str>>> {
+ let mut bundle_iter = cache.into_iter();
+ format_values_from_inner!(bundle_iter.next(), keys, errors);
+ }
+}
diff --git a/third_party/rust/fluent-fallback/src/cache.rs b/third_party/rust/fluent-fallback/src/cache.rs
new file mode 100644
index 0000000000..32bc33fad1
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/cache.rs
@@ -0,0 +1,253 @@
+use std::{
+ cell::{RefCell, UnsafeCell},
+ cmp::Ordering,
+ pin::Pin,
+ task::Context,
+ task::Poll,
+ task::Waker,
+};
+
+use crate::generator::{BundleIterator, BundleStream};
+use crate::pin_cell::{PinCell, PinMut};
+use chunky_vec::ChunkyVec;
+use futures::{ready, Stream};
+
+pub struct Cache<I, R>
+where
+ I: Iterator,
+{
+ iter: RefCell<I>,
+ items: UnsafeCell<ChunkyVec<I::Item>>,
+ res: std::marker::PhantomData<R>,
+}
+
+impl<I, R> Cache<I, R>
+where
+ I: Iterator,
+{
+ pub fn new(iter: I) -> Self {
+ Self {
+ iter: RefCell::new(iter),
+ items: Default::default(),
+ res: std::marker::PhantomData,
+ }
+ }
+
+ pub fn len(&self) -> usize {
+ unsafe {
+ let items = self.items.get();
+ (*items).len()
+ }
+ }
+
+ pub fn get(&self, index: usize) -> Option<&I::Item> {
+ unsafe {
+ let items = self.items.get();
+ (*items).get(index)
+ }
+ }
+
+ /// Push, immediately getting a reference to the element
+ pub fn push_get(&self, new_value: I::Item) -> &I::Item {
+ unsafe {
+ let items = self.items.get();
+ (*items).push_get(new_value)
+ }
+ }
+}
+
+impl<I, R> Cache<I, R>
+where
+ I: BundleIterator + Iterator,
+{
+ pub fn prefetch(&self) {
+ self.iter.borrow_mut().prefetch_sync();
+ }
+}
+
+pub struct CacheIter<'a, I, R>
+where
+ I: Iterator,
+{
+ cache: &'a Cache<I, R>,
+ curr: usize,
+}
+
+impl<'a, I, R> Iterator for CacheIter<'a, I, R>
+where
+ I: Iterator,
+{
+ type Item = &'a I::Item;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let cache_len = self.cache.len();
+ match self.curr.cmp(&cache_len) {
+ Ordering::Less => {
+ // Cached value
+ self.curr += 1;
+ self.cache.get(self.curr - 1)
+ }
+ Ordering::Equal => {
+ // Get the next item from the iterator
+ let item = self.cache.iter.borrow_mut().next();
+ self.curr += 1;
+ if let Some(item) = item {
+ Some(self.cache.push_get(item))
+ } else {
+ None
+ }
+ }
+ Ordering::Greater => {
+ // Ran off the end of the cache
+ None
+ }
+ }
+ }
+}
+
+impl<'a, I, R> IntoIterator for &'a Cache<I, R>
+where
+ I: Iterator,
+{
+ type Item = &'a I::Item;
+ type IntoIter = CacheIter<'a, I, R>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ CacheIter {
+ cache: self,
+ curr: 0,
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+pub struct AsyncCache<S, R>
+where
+ S: Stream,
+{
+ stream: PinCell<S>,
+ items: UnsafeCell<ChunkyVec<S::Item>>,
+ // TODO: Should probably be an SmallVec<[Waker; 1]> or something? I guess
+ // multiple pending wakes are not really all that common.
+ pending_wakes: RefCell<Vec<Waker>>,
+ res: std::marker::PhantomData<R>,
+}
+
+impl<S, R> AsyncCache<S, R>
+where
+ S: Stream,
+{
+ pub fn new(stream: S) -> Self {
+ Self {
+ stream: PinCell::new(stream),
+ items: Default::default(),
+ pending_wakes: Default::default(),
+ res: std::marker::PhantomData,
+ }
+ }
+
+ pub fn len(&self) -> usize {
+ unsafe {
+ let items = self.items.get();
+ (*items).len()
+ }
+ }
+
+ pub fn get(&self, index: usize) -> Poll<Option<&S::Item>> {
+ unsafe {
+ let items = self.items.get();
+ (*items).get(index).into()
+ }
+ }
+
+ /// Push, immediately getting a reference to the element
+ pub fn push_get(&self, new_value: S::Item) -> &S::Item {
+ unsafe {
+ let items = self.items.get();
+ (*items).push_get(new_value)
+ }
+ }
+
+ pub fn stream(&self) -> AsyncCacheStream<'_, S, R> {
+ AsyncCacheStream {
+ cache: self,
+ curr: 0,
+ }
+ }
+}
+
+impl<S, R> AsyncCache<S, R>
+where
+ S: BundleStream + Stream,
+{
+ pub async fn prefetch(&self) {
+ let pin = unsafe { Pin::new_unchecked(&self.stream) };
+ unsafe { PinMut::as_mut(&mut pin.borrow_mut()).get_unchecked_mut() }
+ .prefetch_async()
+ .await
+ }
+}
+
+impl<S, R> AsyncCache<S, R>
+where
+ S: Stream,
+{
+ // Helper function that gets the next value from wrapped stream.
+ fn poll_next_item(&self, cx: &mut Context<'_>) -> Poll<Option<S::Item>> {
+ let pin = unsafe { Pin::new_unchecked(&self.stream) };
+ let poll = PinMut::as_mut(&mut pin.borrow_mut()).poll_next(cx);
+ if poll.is_ready() {
+ let wakers = std::mem::take(&mut *self.pending_wakes.borrow_mut());
+ for waker in wakers {
+ waker.wake();
+ }
+ } else {
+ self.pending_wakes.borrow_mut().push(cx.waker().clone());
+ }
+ poll
+ }
+}
+
+pub struct AsyncCacheStream<'a, S, R>
+where
+ S: Stream,
+{
+ cache: &'a AsyncCache<S, R>,
+ curr: usize,
+}
+
+impl<'a, S, R> Stream for AsyncCacheStream<'a, S, R>
+where
+ S: Stream,
+{
+ type Item = &'a S::Item;
+
+ fn poll_next(
+ mut self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> Poll<Option<Self::Item>> {
+ let cache_len = self.cache.len();
+ match self.curr.cmp(&cache_len) {
+ Ordering::Less => {
+ // Cached value
+ self.curr += 1;
+ self.cache.get(self.curr - 1)
+ }
+ Ordering::Equal => {
+ // Get the next item from the stream
+ let item = ready!(self.cache.poll_next_item(cx));
+ self.curr += 1;
+ if let Some(item) = item {
+ Some(self.cache.push_get(item)).into()
+ } else {
+ None.into()
+ }
+ }
+ Ordering::Greater => {
+ // Ran off the end of the cache
+ None.into()
+ }
+ }
+ }
+}
diff --git a/third_party/rust/fluent-fallback/src/env.rs b/third_party/rust/fluent-fallback/src/env.rs
new file mode 100644
index 0000000000..cf340fcfdf
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/env.rs
@@ -0,0 +1,84 @@
+//! Traits required to provide environment driven data for [`Localization`](crate::Localization).
+//!
+//! Since [`Localization`](crate::Localization) is a long-lived structure,
+//! the model in which the user provides ability for the system to react to changes
+//! is by implementing the given environmental trait and triggering
+//! [`Localization::on_change`](crate::Localization::on_change) method.
+//!
+//! At the moment just a single trait is provided, which allows the
+//! environment to feed a selection of locales to be provided to the instance.
+//!
+//! The locales provided to [`Localization`](crate::Localization) should be
+//! already negotiated to ensure that the resources in those locales
+//! are available. The list should also be sorted according to the user
+//! preference, as the order is significant for how [`Localization`](crate::Localization) performs
+//! fallbacking.
+use unic_langid::LanguageIdentifier;
+
+/// A trait used to provide a selection of locales to be used by the
+/// [`Localization`](crate::Localization) instance for runtime
+/// locale fallbacking.
+///
+/// # Example
+/// ```
+/// use fluent_fallback::{Localization, env::LocalesProvider};
+/// use fluent_resmgr::ResourceManager;
+/// use unic_langid::LanguageIdentifier;
+/// use std::{
+/// rc::Rc,
+/// cell::RefCell
+/// };
+///
+/// #[derive(Clone)]
+/// struct Env {
+/// locales: Rc<RefCell<Vec<LanguageIdentifier>>>,
+/// }
+///
+/// impl Env {
+/// pub fn new(locales: Vec<LanguageIdentifier>) -> Self {
+/// Self { locales: Rc::new(RefCell::new(locales)) }
+/// }
+///
+/// pub fn set_locales(&mut self, new_locales: Vec<LanguageIdentifier>) {
+/// let mut locales = self.locales.borrow_mut();
+/// locales.clear();
+/// locales.extend(new_locales);
+/// }
+/// }
+///
+/// impl LocalesProvider for Env {
+/// type Iter = <Vec<LanguageIdentifier> as IntoIterator>::IntoIter;
+/// fn locales(&self) -> Self::Iter {
+/// self.locales.borrow().clone().into_iter()
+/// }
+/// }
+///
+/// let res_mgr = ResourceManager::new("./path/{locale}/".to_string());
+///
+/// let mut env = Env::new(vec![
+/// "en-GB".parse().unwrap()
+/// ]);
+///
+/// let mut loc = Localization::with_env(vec![], true, env.clone(), res_mgr);
+///
+/// env.set_locales(vec![
+/// "de".parse().unwrap(),
+/// "en-GB".parse().unwrap(),
+/// ]);
+///
+/// loc.on_change();
+///
+/// // The next format call will attempt to localize to `de` first and
+/// // fallback on `en-GB`.
+/// ```
+pub trait LocalesProvider {
+ type Iter: Iterator<Item = LanguageIdentifier>;
+ fn locales(&self) -> Self::Iter;
+}
+
+impl LocalesProvider for Vec<LanguageIdentifier> {
+ type Iter = <Vec<LanguageIdentifier> as IntoIterator>::IntoIter;
+ fn locales(&self) -> Self::Iter {
+ self.clone().into_iter()
+ }
+}
diff --git a/third_party/rust/fluent-fallback/src/errors.rs b/third_party/rust/fluent-fallback/src/errors.rs
new file mode 100644
index 0000000000..9170fb1cb9
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/errors.rs
@@ -0,0 +1,71 @@
+use fluent_bundle::FluentError;
+use std::error::Error;
+use unic_langid::LanguageIdentifier;
+
+#[derive(Debug, PartialEq)]
+pub enum LocalizationError {
+ Bundle {
+ error: FluentError,
+ },
+ Resolver {
+ id: String,
+ locale: LanguageIdentifier,
+ errors: Vec<FluentError>,
+ },
+ MissingMessage {
+ id: String,
+ locale: Option<LanguageIdentifier>,
+ },
+ MissingValue {
+ id: String,
+ locale: Option<LanguageIdentifier>,
+ },
+ SyncRequestInAsyncMode,
+}
+
+impl From<FluentError> for LocalizationError {
+ fn from(error: FluentError) -> Self {
+ Self::Bundle { error }
+ }
+}
+
+impl std::fmt::Display for LocalizationError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Bundle { error } => write!(f, "[fluent][bundle] error: {}", error),
+ Self::Resolver { id, locale, errors } => {
+ let errors: Vec<String> = errors.iter().map(|err| err.to_string()).collect();
+ write!(
+ f,
+ "[fluent][resolver] errors in {}/{}: {}",
+ locale,
+ id,
+ errors.join(", ")
+ )
+ }
+ Self::MissingMessage {
+ id,
+ locale: Some(locale),
+ } => write!(f, "[fluent] Missing message in locale {}: {}", locale, id),
+ Self::MissingMessage { id, locale: None } => {
+ write!(f, "[fluent] Couldn't find a message: {}", id)
+ }
+ Self::MissingValue {
+ id,
+ locale: Some(locale),
+ } => write!(
+ f,
+ "[fluent] Message has no value in locale {}: {}",
+ locale, id
+ ),
+ Self::MissingValue { id, locale: None } => {
+ write!(f, "[fluent] Couldn't find a message with value: {}", id)
+ }
+ Self::SyncRequestInAsyncMode => {
+ write!(f, "Triggered synchronous format while in async mode")
+ }
+ }
+ }
+}
+
+impl Error for LocalizationError {}
diff --git a/third_party/rust/fluent-fallback/src/generator.rs b/third_party/rust/fluent-fallback/src/generator.rs
new file mode 100644
index 0000000000..f13af63cfd
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/generator.rs
@@ -0,0 +1,41 @@
+use fluent_bundle::{FluentBundle, FluentError, FluentResource};
+use futures::Stream;
+use rustc_hash::FxHashSet;
+use std::borrow::Borrow;
+use unic_langid::LanguageIdentifier;
+
+use crate::types::ResourceId;
+
+pub type FluentBundleResult<R> = Result<FluentBundle<R>, (FluentBundle<R>, Vec<FluentError>)>;
+
+pub trait BundleIterator {
+ fn prefetch_sync(&mut self) {}
+}
+
+#[async_trait::async_trait(?Send)]
+pub trait BundleStream {
+ async fn prefetch_async(&mut self) {}
+}
+
+pub trait BundleGenerator {
+ type Resource: Borrow<FluentResource>;
+ type LocalesIter: Iterator<Item = LanguageIdentifier>;
+ type Iter: Iterator<Item = FluentBundleResult<Self::Resource>>;
+ type Stream: Stream<Item = FluentBundleResult<Self::Resource>>;
+
+ fn bundles_iter(
+ &self,
+ _locales: Self::LocalesIter,
+ _res_ids: FxHashSet<ResourceId>,
+ ) -> Self::Iter {
+ unimplemented!();
+ }
+
+ fn bundles_stream(
+ &self,
+ _locales: Self::LocalesIter,
+ _res_ids: FxHashSet<ResourceId>,
+ ) -> Self::Stream {
+ unimplemented!();
+ }
+}
diff --git a/third_party/rust/fluent-fallback/src/lib.rs b/third_party/rust/fluent-fallback/src/lib.rs
new file mode 100644
index 0000000000..9dbadc5b98
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/lib.rs
@@ -0,0 +1,118 @@
+//! Fluent is a modern localization system designed to improve how software is translated.
+//!
+//! `fluent-fallback` is a high-level component of the [Fluent Localization
+//! System](https://www.projectfluent.org).
+//!
+//! The crate builds on top of the mid-level [`fluent-bundle`](../fluent-bundle) package, and provides an ergonomic API for highly flexible localization.
+//!
+//! The functionality of this level is complete, but the API itself is in the
+//! early stages and the goal of being ergonomic is yet to be achieved.
+//!
+//! If the user is willing to work through the challenge of setting up the
+//! boiler-plate that will eventually go away, `fluent-fallback` provides
+//! a powerful abstraction around [`FluentBundle`](fluent_bundle::FluentBundle) coupled
+//! with a localization resource management system.
+//!
+//! The main struct, [`Localization`], is a long-lived, reactive, multi-lingual
+//! struct which allows for strong error recovery and locale
+//! fallbacking, exposing synchronous and asynchronous ergonomic methods
+//! for [`L10nMessage`](types::L10nMessage) retrieval.
+//!
+//! [`Localization`] is also an API that is to be used when designing bindings
+//! to user interface systems, such as DOM, React, and others.
+//!
+//! # Example
+//!
+//! ```
+//! use fluent_fallback::{Localization, types::{ResourceType, ToResourceId}};
+//! use fluent_resmgr::ResourceManager;
+//! use unic_langid::langid;
+//!
+//! let res_mgr = ResourceManager::new("./tests/resources/{locale}/".to_string());
+//!
+//! let loc = Localization::with_env(
+//! vec![
+//! "test.ftl".into(),
+//! "test2.ftl".to_resource_id(ResourceType::Optional),
+//! ],
+//! true,
+//! vec![langid!("en-US")],
+//! res_mgr,
+//! );
+//! let bundles = loc.bundles();
+//!
+//! let mut errors = vec![];
+//! let value = bundles.format_value_sync("hello-world", None, &mut errors)
+//! .expect("Failed to format a value");
+//!
+//! assert_eq!(value, Some("Hello World [en]".into()));
+//! ```
+//!
+//! The above example is far from the ergonomical API style the Fluent project
+//! is aiming for, but it represents the full scope of functionality intended
+//! for the model.
+//!
+//! # Resource Management
+//!
+//! Resource management is one of the most complicated parts of a localization system.
+//! In particular, modern software may have needs for both synchronous
+//! and asynchronous I/O. That, in turn has a large impact on what can happen
+//! in case of missing resources, or errors.
+//!
+//! Resource identifiers can refer to resources that are either required or optional.
+//! In the above example, `"test.ftl"` is a required resource (the default using `.into()`),
+//! and `"test2.ftl" is an optional resource, which you can create via the
+//! [`ToResourceId`](fluent_fallback::types::ToResourceId) trait.
+//!
+//! A required resource must be present in order for the a bundle to be considered valid.
+//! If a required resource is missing for a given locale, a bundle will not be generated for that locale.
+//!
+//! A bundle is still considered valid if an optional resource is missing. A bundle will still be generated
+//! and the entries for the missing optional resource will simply be missing from the bundle. This should be
+//! used sparingly in exceptional cases where you do not want `Localization` to fall back to the next
+//! locale if there is a missing resource. Marking all resources as optional will increase the state space
+//! that the solver has to search through, and will have a negative impact on performance.
+//!
+//! Currently, [`Localization`] can be specialized over an implementation of
+//! [`generator::BundleGenerator`] trait which provides a method to generate an
+//! [`Iterator`] and [`Stream`](futures::stream::Stream).
+//!
+//! This is not very elegant and will likely be improved in the future, but for the time being, if
+//! the customer doesn't need one of the modes, the unnecessary method should use the
+//! `unimplemented!()` macro as its body.
+//!
+//! `fluent-resmgr` provides a simple resource manager which handles synchronous I/O
+//! and uses local file system to store resources in a directory structure.
+//!
+//! That model is often sufficient and the user can either use `fluent-resmgr` or write
+//! a similar API to provide the generator for [`Localization`].
+//!
+//! Alternatively, a much more sophisticated resource manager can be used. Mozilla
+//! for its needs in Firefox uses [`L10nRegistry`](https://github.com/zbraniecki/l10nregistry-rs)
+//! library which implements [`BundleGenerator`](generator::BundleGenerator).
+//!
+//! # Locale Management
+//!
+//! As a long lived structure, the [`Localization`] is intended to handle runtime locale
+//! management.
+//!
+//! In the example above, [`Vec<LagnuageIdentifier>`](unic_langid::LanguageIdentifier)
+//! provides a static list of locales that the [`Localization`] handles, but that's just the
+//! simplest implementation of the [`env::LocalesProvider`], and one can implement
+//! a much more sophisticated one that reacts to user or environment driven changes, and
+//! called [`Localization::on_change`] to trigger a new locales to be used for the
+//! next translation request.
+//!
+//! See [`env::LocalesProvider`] trait for an example of a reactive system implementation.
+mod bundles;
+mod cache;
+pub mod env;
+mod errors;
+pub mod generator;
+mod localization;
+mod pin_cell;
+pub mod types;
+
+pub use bundles::Bundles;
+pub use errors::LocalizationError;
+pub use localization::Localization;
diff --git a/third_party/rust/fluent-fallback/src/localization.rs b/third_party/rust/fluent-fallback/src/localization.rs
new file mode 100644
index 0000000000..5424bcf311
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/localization.rs
@@ -0,0 +1,137 @@
+use crate::{
+ bundles::Bundles,
+ env::LocalesProvider,
+ generator::{BundleGenerator, BundleIterator, BundleStream},
+ types::ResourceId,
+};
+use once_cell::sync::OnceCell;
+use rustc_hash::FxHashSet;
+use std::rc::Rc;
+
+pub struct Localization<G, P>
+where
+ G: BundleGenerator<LocalesIter = P::Iter>,
+ P: LocalesProvider,
+{
+ bundles: OnceCell<Rc<Bundles<G>>>,
+ generator: G,
+ provider: P,
+ sync: bool,
+ res_ids: FxHashSet<ResourceId>,
+}
+
+impl<G, P> Localization<G, P>
+where
+ G: BundleGenerator<LocalesIter = P::Iter> + Default,
+ P: LocalesProvider + Default,
+{
+ pub fn new<I>(res_ids: I, sync: bool) -> Self
+ where
+ I: IntoIterator<Item = ResourceId>,
+ {
+ Self {
+ bundles: OnceCell::new(),
+ generator: G::default(),
+ provider: P::default(),
+ sync,
+ res_ids: FxHashSet::from_iter(res_ids.into_iter()),
+ }
+ }
+}
+
+impl<G, P> Localization<G, P>
+where
+ G: BundleGenerator<LocalesIter = P::Iter>,
+ P: LocalesProvider,
+{
+ pub fn with_env<I>(res_ids: I, sync: bool, provider: P, generator: G) -> Self
+ where
+ I: IntoIterator<Item = ResourceId>,
+ {
+ Self {
+ bundles: OnceCell::new(),
+ generator,
+ provider,
+ sync,
+ res_ids: FxHashSet::from_iter(res_ids.into_iter()),
+ }
+ }
+
+ pub fn is_sync(&self) -> bool {
+ self.sync
+ }
+
+ pub fn add_resource_id<T: Into<ResourceId>>(&mut self, res_id: T) {
+ self.res_ids.insert(res_id.into());
+ self.on_change();
+ }
+
+ pub fn add_resource_ids(&mut self, res_ids: Vec<ResourceId>) {
+ self.res_ids.extend(res_ids);
+ self.on_change();
+ }
+
+ pub fn remove_resource_id<T: PartialEq<ResourceId>>(&mut self, res_id: T) -> usize {
+ self.res_ids.retain(|x| !res_id.eq(x));
+ self.on_change();
+ self.res_ids.len()
+ }
+
+ pub fn remove_resource_ids(&mut self, res_ids: Vec<ResourceId>) -> usize {
+ self.res_ids.retain(|x| !res_ids.contains(x));
+ self.on_change();
+ self.res_ids.len()
+ }
+
+ pub fn set_async(&mut self) {
+ if self.sync {
+ self.sync = false;
+ self.on_change();
+ }
+ }
+
+ pub fn on_change(&mut self) {
+ self.bundles.take();
+ }
+}
+
+impl<G, P> Localization<G, P>
+where
+ G: BundleGenerator<LocalesIter = P::Iter>,
+ G::Iter: BundleIterator,
+ P: LocalesProvider,
+{
+ pub fn prefetch_sync(&mut self) {
+ let bundles = self.bundles();
+ bundles.prefetch_sync();
+ }
+}
+
+impl<G, P> Localization<G, P>
+where
+ G: BundleGenerator<LocalesIter = P::Iter>,
+ G::Stream: BundleStream,
+ P: LocalesProvider,
+{
+ pub async fn prefetch_async(&mut self) {
+ let bundles = self.bundles();
+ bundles.prefetch_async().await
+ }
+}
+
+impl<G, P> Localization<G, P>
+where
+ G: BundleGenerator<LocalesIter = P::Iter>,
+ P: LocalesProvider,
+{
+ pub fn bundles(&self) -> &Rc<Bundles<G>> {
+ self.bundles.get_or_init(|| {
+ Rc::new(Bundles::new(
+ self.sync,
+ self.res_ids.clone(),
+ &self.generator,
+ &self.provider,
+ ))
+ })
+ }
+}
diff --git a/third_party/rust/fluent-fallback/src/pin_cell/README.md b/third_party/rust/fluent-fallback/src/pin_cell/README.md
new file mode 100644
index 0000000000..b1c475f51b
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/pin_cell/README.md
@@ -0,0 +1,2 @@
+This is a temporary fork of https://github.com/withoutboats/pin-cell until
+https://github.com/withoutboats/pin-cell/issues/6 gets resolved.
diff --git a/third_party/rust/fluent-fallback/src/pin_cell/mod.rs b/third_party/rust/fluent-fallback/src/pin_cell/mod.rs
new file mode 100644
index 0000000000..175f9677e0
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/pin_cell/mod.rs
@@ -0,0 +1,97 @@
+#![deny(missing_docs, missing_debug_implementations)]
+//! This library defines the `PinCell` type, a pinning variant of the standard
+//! library's `RefCell`.
+//!
+//! It is not safe to "pin project" through a `RefCell` - getting a pinned
+//! reference to something inside the `RefCell` when you have a pinned
+//! refernece to the `RefCell` - because `RefCell` is too powerful.
+//!
+//! A `PinCell` is slightly less powerful than `RefCell`: unlike a `RefCell`,
+//! one cannot get a mutable reference into a `PinCell`, only a pinned mutable
+//! reference (`Pin<&mut T>`). This makes pin projection safe, allowing you
+//! to use interior mutability with the knowledge that `T` will never actually
+//! be moved out of the `RefCell` that wraps it.
+
+mod pin_mut;
+mod pin_ref;
+
+use core::cell::{BorrowMutError, RefCell, RefMut};
+use core::pin::Pin;
+
+pub use pin_mut::PinMut;
+pub use pin_ref::PinRef;
+
+/// A mutable memory location with dynamically checked borrow rules
+///
+/// Unlike `RefCell`, this type only allows *pinned* mutable access to the
+/// inner value, enabling a "pin-safe" version of interior mutability.
+///
+/// See the standard library documentation for more information.
+#[derive(Default, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)]
+pub struct PinCell<T: ?Sized> {
+ inner: RefCell<T>,
+}
+
+impl<T> PinCell<T> {
+ /// Creates a new `PinCell` containing `value`.
+ pub const fn new(value: T) -> PinCell<T> {
+ PinCell {
+ inner: RefCell::new(value),
+ }
+ }
+}
+
+impl<T: ?Sized> PinCell<T> {
+ /// Mutably borrows the wrapped value, preserving its pinnedness.
+ ///
+ /// The borrow lasts until the returned `PinMut` or all `PinMut`s derived
+ /// from it exit scope. The value cannot be borrowed while this borrow is
+ /// active.
+ pub fn borrow_mut(self: Pin<&Self>) -> PinMut<'_, T> {
+ self.try_borrow_mut().expect("already borrowed")
+ }
+
+ /// Mutably borrows the wrapped value, preserving its pinnedness,
+ /// returning an error if the value is currently borrowed.
+ ///
+ /// The borrow lasts until the returned `PinMut` or all `PinMut`s derived
+ /// from it exit scope. The value cannot be borrowed while this borrow is
+ /// active.
+ ///
+ /// This is the non-panicking variant of `borrow_mut`.
+ pub fn try_borrow_mut<'a>(self: Pin<&'a Self>) -> Result<PinMut<'a, T>, BorrowMutError> {
+ let ref_mut: RefMut<'a, T> = Pin::get_ref(self).inner.try_borrow_mut()?;
+
+ // this is a pin projection from Pin<&PinCell<T>> to Pin<RefMut<T>>
+ // projecting is safe because:
+ //
+ // - for<T: ?Sized> (PinCell<T>: Unpin) imples (RefMut<T>: Unpin)
+ // holds true
+ // - PinCell does not implement Drop
+ //
+ // see discussion on tracking issue #49150 about pin projection
+ // invariants
+ let pin_ref_mut: Pin<RefMut<'a, T>> = unsafe { Pin::new_unchecked(ref_mut) };
+
+ Ok(PinMut { inner: pin_ref_mut })
+ }
+}
+
+impl<T> From<T> for PinCell<T> {
+ fn from(value: T) -> PinCell<T> {
+ PinCell::new(value)
+ }
+}
+
+impl<T> From<RefCell<T>> for PinCell<T> {
+ fn from(cell: RefCell<T>) -> PinCell<T> {
+ PinCell { inner: cell }
+ }
+}
+
+impl<T> From<PinCell<T>> for RefCell<T> {
+ fn from(input: PinCell<T>) -> Self {
+ input.inner
+ }
+}
+// TODO CoerceUnsized
diff --git a/third_party/rust/fluent-fallback/src/pin_cell/pin_mut.rs b/third_party/rust/fluent-fallback/src/pin_cell/pin_mut.rs
new file mode 100644
index 0000000000..09a4d4a6fb
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/pin_cell/pin_mut.rs
@@ -0,0 +1,50 @@
+use core::cell::RefMut;
+use core::fmt;
+use core::ops::Deref;
+use core::pin::Pin;
+
+#[derive(Debug)]
+/// A wrapper type for a mutably borrowed value from a `PinCell<T>`.
+pub struct PinMut<'a, T: ?Sized> {
+ pub(crate) inner: Pin<RefMut<'a, T>>,
+}
+
+impl<'a, T: ?Sized> Deref for PinMut<'a, T> {
+ type Target = T;
+
+ fn deref(&self) -> &T {
+ &*self.inner
+ }
+}
+
+impl<'a, T: ?Sized> PinMut<'a, T> {
+ /// Get a pinned mutable reference to the value inside this wrapper.
+ pub fn as_mut<'b>(orig: &'b mut PinMut<'a, T>) -> Pin<&'b mut T> {
+ orig.inner.as_mut()
+ }
+}
+
+/* TODO implement these APIs
+
+impl<'a, T: ?Sized> PinMut<'a, T> {
+ pub fn map<U, F>(orig: PinMut<'a, T>, f: F) -> PinMut<'a, U> where
+ F: FnOnce(Pin<&mut T>) -> Pin<&mut U>,
+ {
+ panic!()
+ }
+
+ pub fn map_split<U, V, F>(orig: PinMut<'a, T>, f: F) -> (PinMut<'a, U>, PinMut<'a, V>) where
+ F: FnOnce(Pin<&mut T>) -> (Pin<&mut U>, Pin<&mut V>)
+ {
+ panic!()
+ }
+}
+*/
+
+impl<'a, T: fmt::Display + ?Sized> fmt::Display for PinMut<'a, T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ <T as fmt::Display>::fmt(&**self, f)
+ }
+}
+
+// TODO CoerceUnsized
diff --git a/third_party/rust/fluent-fallback/src/pin_cell/pin_ref.rs b/third_party/rust/fluent-fallback/src/pin_cell/pin_ref.rs
new file mode 100644
index 0000000000..46f9cfdabb
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/pin_cell/pin_ref.rs
@@ -0,0 +1,47 @@
+use core::cell::Ref;
+use core::fmt;
+use core::ops::Deref;
+use core::pin::Pin;
+
+#[derive(Debug)]
+/// A wrapper type for a immutably borrowed value from a `PinCell<T>`.
+pub struct PinRef<'a, T: ?Sized> {
+ pub(crate) inner: Pin<Ref<'a, T>>,
+}
+
+impl<'a, T: ?Sized> Deref for PinRef<'a, T> {
+ type Target = T;
+
+ fn deref(&self) -> &T {
+ &*self.inner
+ }
+}
+
+/* TODO implement these APIs
+
+impl<'a, T: ?Sized> PinRef<'a, T> {
+ pub fn clone(orig: &PinRef<'a, T>) -> PinRef<'a, T> {
+ panic!()
+ }
+
+ pub fn map<U, F>(orig: PinRef<'a, T>, f: F) -> PinRef<'a, U> where
+ F: FnOnce(Pin<&T>) -> Pin<&U>,
+ {
+ panic!()
+ }
+
+ pub fn map_split<U, V, F>(orig: PinRef<'a, T>, f: F) -> (PinRef<'a, U>, PinRef<'a, V>) where
+ F: FnOnce(Pin<&T>) -> (Pin<&U>, Pin<&V>)
+ {
+ panic!()
+ }
+}
+*/
+
+impl<'a, T: fmt::Display + ?Sized> fmt::Display for PinRef<'a, T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ <T as fmt::Display>::fmt(&**self, f)
+ }
+}
+
+// TODO CoerceUnsized
diff --git a/third_party/rust/fluent-fallback/src/types.rs b/third_party/rust/fluent-fallback/src/types.rs
new file mode 100644
index 0000000000..6b87fa0522
--- /dev/null
+++ b/third_party/rust/fluent-fallback/src/types.rs
@@ -0,0 +1,141 @@
+use fluent_bundle::FluentArgs;
+use std::borrow::Cow;
+
+#[derive(Debug)]
+pub struct L10nKey<'l> {
+ pub id: Cow<'l, str>,
+ pub args: Option<FluentArgs<'l>>,
+}
+
+impl<'l> From<&'l str> for L10nKey<'l> {
+ fn from(id: &'l str) -> Self {
+ Self {
+ id: id.into(),
+ args: None,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct L10nAttribute<'l> {
+ pub name: Cow<'l, str>,
+ pub value: Cow<'l, str>,
+}
+
+#[derive(Debug, Clone)]
+pub struct L10nMessage<'l> {
+ pub value: Option<Cow<'l, str>>,
+ pub attributes: Vec<L10nAttribute<'l>>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum ResourceType {
+ /// This is a required resource.
+ ///
+ /// A bundle generator should not consider a solution as valid
+ /// if this resource is missing.
+ ///
+ /// This is the default when creating a [`ResourceId`].
+ Required,
+
+ /// This is an optional resource.
+ ///
+ /// A bundle generator should still populate a partial solution
+ /// even if this resource is missing.
+ ///
+ /// This is intended for experimental and/or under-development
+ /// resources that may not have content for all supported locales.
+ ///
+ /// This should be used sparingly, as it will greatly increase
+ /// the state space of the search for valid solutions which can
+ /// have a severe impact on performance.
+ Optional,
+}
+
+/// A resource identifier for a localization resource.
+#[derive(Debug, Clone, Hash)]
+pub struct ResourceId {
+ /// The resource identifier.
+ pub value: String,
+
+ /// The [`ResourceType`] for this resource.
+ ///
+ /// The default value (when converting from another type) is
+ /// [`ResourceType::Required`]. You should only set this to
+ /// [`ResourceType::Optional`] for experimental or under-development
+ /// features that may not yet have content in all eventually-supported locales.
+ ///
+ /// Setting this value to [`ResourceType::Optional`] for all resources
+ /// may have a severe impact on performance due to increasing the state space
+ /// of the solver.
+ pub resource_type: ResourceType,
+}
+
+impl ResourceId {
+ pub fn new<S: Into<String>>(value: S, resource_type: ResourceType) -> Self {
+ Self {
+ value: value.into(),
+ resource_type,
+ }
+ }
+
+ /// Returns [`true`] if the resource has [`ResourceType::Required`],
+ /// otherwise returns [`false`].
+ pub fn is_required(&self) -> bool {
+ matches!(self.resource_type, ResourceType::Required)
+ }
+
+ /// Returns [`true`] if the resource has [`ResourceType::Optional`],
+ /// otherwise returns [`false`].
+ pub fn is_optional(&self) -> bool {
+ matches!(self.resource_type, ResourceType::Optional)
+ }
+}
+
+impl<S: Into<String>> From<S> for ResourceId {
+ fn from(id: S) -> Self {
+ Self {
+ value: id.into(),
+ resource_type: ResourceType::Required,
+ }
+ }
+}
+
+impl std::fmt::Display for ResourceId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.value)
+ }
+}
+
+impl PartialEq<str> for ResourceId {
+ fn eq(&self, other: &str) -> bool {
+ self.value.as_str().eq(other)
+ }
+}
+
+impl Eq for ResourceId {}
+impl PartialEq for ResourceId {
+ fn eq(&self, other: &Self) -> bool {
+ self.value.eq(&other.value)
+ }
+}
+
+/// A trait for creating a [`ResourceId`] from another type.
+///
+/// This differs from the [`From`] trait in that the [`From`] trait
+/// always takes the default resource type of [`ResourceType::Required`].
+///
+/// If you need to create a resource with a non-default [`ResourceType`],
+/// such as [`ResourceType::Optional`], then use this trait.
+///
+/// This trait is automatically implemented for types that implement [`Into<String>`].
+pub trait ToResourceId {
+ /// Creates a [`ResourceId`] from [`self`], given a [`ResourceType`].
+ fn to_resource_id(self, resource_type: ResourceType) -> ResourceId;
+}
+
+impl<S: Into<String>> ToResourceId for S {
+ fn to_resource_id(self, resource_type: ResourceType) -> ResourceId {
+ ResourceId::new(self.into(), resource_type)
+ }
+}
diff --git a/third_party/rust/fluent-fallback/tests/localization_test.rs b/third_party/rust/fluent-fallback/tests/localization_test.rs
new file mode 100644
index 0000000000..b48f0a05b9
--- /dev/null
+++ b/third_party/rust/fluent-fallback/tests/localization_test.rs
@@ -0,0 +1,518 @@
+use std::borrow::Cow;
+use std::fs;
+
+use fluent_bundle::{
+ resolver::errors::{ReferenceKind, ResolverError},
+ FluentArgs, FluentBundle, FluentError, FluentResource,
+};
+use fluent_fallback::{
+ env::LocalesProvider,
+ generator::{BundleGenerator, FluentBundleResult},
+ types::{L10nKey, ResourceId},
+ Localization, LocalizationError,
+};
+use rustc_hash::FxHashSet;
+use std::cell::RefCell;
+use std::rc::Rc;
+use unic_langid::{langid, LanguageIdentifier};
+
+struct InnerLocales {
+ locales: RefCell<Vec<LanguageIdentifier>>,
+}
+
+impl InnerLocales {
+ pub fn insert(&self, index: usize, element: LanguageIdentifier) {
+ self.locales.borrow_mut().insert(index, element);
+ }
+}
+
+#[derive(Clone)]
+struct Locales {
+ inner: Rc<InnerLocales>,
+}
+
+impl Locales {
+ pub fn new(locales: Vec<LanguageIdentifier>) -> Self {
+ Self {
+ inner: Rc::new(InnerLocales {
+ locales: RefCell::new(locales),
+ }),
+ }
+ }
+
+ pub fn insert(&mut self, index: usize, element: LanguageIdentifier) {
+ self.inner.insert(index, element);
+ }
+}
+
+impl LocalesProvider for Locales {
+ type Iter = <Vec<LanguageIdentifier> as IntoIterator>::IntoIter;
+ fn locales(&self) -> Self::Iter {
+ self.inner.locales.borrow().clone().into_iter()
+ }
+}
+
+// Due to limitation of trait, we need a nameable Iterator type. Due to the
+// lack of GATs, these have to own members instead of taking slices.
+struct BundleIter {
+ locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
+ res_ids: FxHashSet<ResourceId>,
+}
+
+impl Iterator for BundleIter {
+ type Item = FluentBundleResult<FluentResource>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let locale = self.locales.next()?;
+
+ let mut bundle = FluentBundle::new(vec![locale.clone()]);
+ bundle.set_use_isolating(false);
+
+ let mut errors = vec![];
+
+ for res_id in &self.res_ids {
+ let full_path = format!("./tests/resources/{}/{}", locale, res_id);
+ let source = fs::read_to_string(full_path).unwrap();
+ let res = match FluentResource::try_new(source) {
+ Ok(res) => res,
+ Err((res, err)) => {
+ errors.extend(err.into_iter().map(Into::into));
+ res
+ }
+ };
+ bundle.add_resource(res).unwrap();
+ }
+ if errors.is_empty() {
+ Some(Ok(bundle))
+ } else {
+ Some(Err((bundle, errors)))
+ }
+ }
+}
+
+impl futures::Stream for BundleIter {
+ type Item = FluentBundleResult<FluentResource>;
+
+ fn poll_next(
+ mut self: std::pin::Pin<&mut Self>,
+ _cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll<Option<Self::Item>> {
+ if let Some(locale) = self.locales.next() {
+ let mut bundle = FluentBundle::new(vec![locale.clone()]);
+ bundle.set_use_isolating(false);
+
+ let mut errors = vec![];
+ for res_id in &self.res_ids {
+ let full_path = format!("./tests/resources/{}/{}", locale, res_id.value);
+ let source = fs::read_to_string(full_path).unwrap();
+ let res = match FluentResource::try_new(source) {
+ Ok(res) => res,
+ Err((res, err)) => {
+ errors.extend(err.into_iter().map(Into::into));
+ res
+ }
+ };
+ bundle.add_resource(res).unwrap();
+ }
+ if errors.is_empty() {
+ Some(Ok(bundle)).into()
+ } else {
+ Some(Err((bundle, errors))).into()
+ }
+ } else {
+ None.into()
+ }
+ }
+}
+
+struct ResourceManager;
+
+impl BundleGenerator for ResourceManager {
+ type Resource = FluentResource;
+ type LocalesIter = std::vec::IntoIter<LanguageIdentifier>;
+ type Iter = BundleIter;
+ type Stream = BundleIter;
+
+ fn bundles_iter(
+ &self,
+ locales: Self::LocalesIter,
+ res_ids: FxHashSet<ResourceId>,
+ ) -> Self::Iter {
+ BundleIter { locales, res_ids }
+ }
+
+ fn bundles_stream(
+ &self,
+ locales: Self::LocalesIter,
+ res_ids: FxHashSet<ResourceId>,
+ ) -> Self::Stream {
+ BundleIter { locales, res_ids }
+ }
+}
+
+#[test]
+fn localization_format() {
+ let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
+ let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
+ let res_mgr = ResourceManager;
+ let mut errors = vec![];
+
+ let loc = Localization::with_env(resource_ids, true, locales, res_mgr);
+ let bundles = loc.bundles();
+
+ let value = bundles
+ .format_value_sync("hello-world", None, &mut errors)
+ .unwrap();
+ assert_eq!(value, Some(Cow::Borrowed("Hello World [pl]")));
+
+ let value = bundles
+ .format_value_sync("missing-message", None, &mut errors)
+ .unwrap();
+ assert_eq!(value, None);
+
+ let value = bundles
+ .format_value_sync("hello-world-3", None, &mut errors)
+ .unwrap();
+ assert_eq!(value, Some(Cow::Borrowed("Hello World 3 [en]")));
+
+ assert_eq!(errors.len(), 4);
+}
+
+#[test]
+fn localization_on_change() {
+ let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
+
+ let mut locales = Locales::new(vec![langid!("en-US")]);
+ let res_mgr = ResourceManager;
+ let mut errors = vec![];
+
+ let mut loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
+ let bundles = loc.bundles();
+
+ let value = bundles
+ .format_value_sync("hello-world", None, &mut errors)
+ .unwrap();
+ assert_eq!(value, Some(Cow::Borrowed("Hello World [en]")));
+
+ locales.insert(0, langid!("pl"));
+ loc.on_change();
+
+ let bundles = loc.bundles();
+ let value = bundles
+ .format_value_sync("hello-world", None, &mut errors)
+ .unwrap();
+ assert_eq!(value, Some(Cow::Borrowed("Hello World [pl]")));
+}
+
+#[test]
+fn localization_format_value_missing_errors() {
+ let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
+
+ let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
+ let res_mgr = ResourceManager;
+ let mut errors = vec![];
+
+ let loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
+ let bundles = loc.bundles();
+
+ let _ = bundles
+ .format_value_sync("missing-message", None, &mut errors)
+ .unwrap();
+ assert_eq!(
+ errors,
+ vec![
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: Some(langid!("pl"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: Some(langid!("en-US"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: None
+ },
+ ]
+ );
+
+ errors.clear();
+
+ let _ = bundles
+ .format_value_sync("message-3", None, &mut errors)
+ .unwrap();
+ assert_eq!(
+ errors,
+ vec![
+ LocalizationError::MissingValue {
+ id: "message-3".to_string(),
+ locale: Some(langid!("pl"))
+ },
+ LocalizationError::MissingValue {
+ id: "message-3".to_string(),
+ locale: Some(langid!("en-US"))
+ },
+ LocalizationError::MissingValue {
+ id: "message-3".to_string(),
+ locale: None
+ },
+ ]
+ );
+}
+
+#[test]
+fn localization_format_value_sync_missing_errors() {
+ let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
+
+ let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
+ let res_mgr = ResourceManager;
+ let mut errors = vec![];
+
+ let loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
+ let bundles = loc.bundles();
+
+ let _ = bundles
+ .format_value_sync("missing-message", None, &mut errors)
+ .unwrap();
+ assert_eq!(
+ errors,
+ vec![
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: Some(langid!("pl"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: Some(langid!("en-US"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: None
+ },
+ ]
+ );
+
+ errors.clear();
+
+ let _ = bundles
+ .format_value_sync("message-3", None, &mut errors)
+ .unwrap();
+ assert_eq!(
+ errors,
+ vec![
+ LocalizationError::MissingValue {
+ id: "message-3".to_string(),
+ locale: Some(langid!("pl"))
+ },
+ LocalizationError::MissingValue {
+ id: "message-3".to_string(),
+ locale: Some(langid!("en-US"))
+ },
+ LocalizationError::MissingValue {
+ id: "message-3".to_string(),
+ locale: None
+ },
+ ]
+ );
+}
+
+#[test]
+fn localization_format_values_sync_missing_errors() {
+ let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
+
+ let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
+ let res_mgr = ResourceManager;
+ let mut errors = vec![];
+
+ let loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
+ let bundles = loc.bundles();
+
+ let _ = bundles
+ .format_values_sync(
+ &["missing-message".into(), "missing-message-2".into()],
+ &mut errors,
+ )
+ .unwrap();
+ assert_eq!(
+ errors,
+ vec![
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: Some(langid!("pl"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message-2".to_string(),
+ locale: Some(langid!("pl"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: Some(langid!("en-US"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message-2".to_string(),
+ locale: Some(langid!("en-US"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: None
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message-2".to_string(),
+ locale: None
+ },
+ ]
+ );
+
+ errors.clear();
+
+ let _ = bundles
+ .format_values_sync(&["message-3".into()], &mut errors)
+ .unwrap();
+ assert_eq!(
+ errors,
+ vec![
+ LocalizationError::MissingValue {
+ id: "message-3".to_string(),
+ locale: Some(langid!("pl"))
+ },
+ LocalizationError::MissingValue {
+ id: "message-3".to_string(),
+ locale: Some(langid!("en-US"))
+ },
+ LocalizationError::MissingValue {
+ id: "message-3".to_string(),
+ locale: None
+ },
+ ]
+ );
+}
+
+#[test]
+fn localization_format_messages_sync_missing_errors() {
+ let resource_ids: Vec<ResourceId> = vec!["test.ftl".into(), "test2.ftl".into()];
+
+ let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
+ let res_mgr = ResourceManager;
+ let mut errors = vec![];
+
+ let loc = Localization::with_env(resource_ids, true, locales.clone(), res_mgr);
+ let bundles = loc.bundles();
+
+ let _ = bundles
+ .format_messages_sync(
+ &["missing-message".into(), "missing-message-2".into()],
+ &mut errors,
+ )
+ .unwrap();
+ assert_eq!(
+ errors,
+ vec![
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: Some(langid!("pl"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message-2".to_string(),
+ locale: Some(langid!("pl"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: Some(langid!("en-US"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message-2".to_string(),
+ locale: Some(langid!("en-US"))
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message".to_string(),
+ locale: None
+ },
+ LocalizationError::MissingMessage {
+ id: "missing-message-2".to_string(),
+ locale: None
+ },
+ ]
+ );
+}
+
+#[test]
+fn localization_format_missing_argument_error() {
+ let resource_ids: Vec<ResourceId> = vec!["test2.ftl".into()];
+ let locales = Locales::new(vec![langid!("en-US")]);
+ let res_mgr = ResourceManager;
+ let mut errors = vec![];
+
+ let loc = Localization::with_env(resource_ids, true, locales, res_mgr);
+ let bundles = loc.bundles();
+
+ let mut args = FluentArgs::new();
+ args.set("userName", "John");
+ let keys = vec![L10nKey {
+ id: "message-4".into(),
+ args: Some(args),
+ }];
+
+ let msgs = bundles.format_messages_sync(&keys, &mut errors).unwrap();
+ assert_eq!(
+ msgs.get(0).unwrap().as_ref().unwrap().value,
+ Some(Cow::Borrowed("Hello, John. [en]"))
+ );
+ assert_eq!(errors.len(), 0);
+
+ let keys = vec![L10nKey {
+ id: "message-4".into(),
+ args: None,
+ }];
+ let msgs = bundles.format_messages_sync(&keys, &mut errors).unwrap();
+ assert_eq!(
+ msgs.get(0).unwrap().as_ref().unwrap().value,
+ Some(Cow::Borrowed("Hello, {$userName}. [en]"))
+ );
+ assert_eq!(
+ errors,
+ vec![LocalizationError::Resolver {
+ id: "message-4".to_string(),
+ locale: langid!("en-US"),
+ errors: vec![FluentError::ResolverError(ResolverError::Reference(
+ ReferenceKind::Variable {
+ id: "userName".to_string(),
+ }
+ ))],
+ },]
+ );
+}
+
+#[tokio::test]
+async fn localization_handle_state_changes_mid_async() {
+ let resource_ids: Vec<ResourceId> = vec!["test.ftl".into()];
+ let locales = Locales::new(vec![langid!("en-US")]);
+ let res_mgr = ResourceManager;
+ let mut errors = vec![];
+
+ let mut loc = Localization::with_env(resource_ids, false, locales, res_mgr);
+
+ let bundles = loc.bundles().clone();
+
+ loc.add_resource_id("test2.ftl".to_string());
+
+ bundles.format_value("key", None, &mut errors).await;
+}
+
+#[test]
+fn localization_duplicate_resources() {
+ let resource_ids: Vec<ResourceId> =
+ vec!["test.ftl".into(), "test2.ftl".into(), "test2.ftl".into()];
+ let locales = Locales::new(vec![langid!("pl"), langid!("en-US")]);
+ let res_mgr = ResourceManager;
+ let mut errors = vec![];
+
+ let loc = Localization::with_env(resource_ids, true, locales, res_mgr);
+ let bundles = loc.bundles();
+
+ let value = bundles
+ .format_value_sync("hello-world", None, &mut errors)
+ .unwrap();
+ assert_eq!(value, Some(Cow::Borrowed("Hello World [pl]")));
+
+ assert_eq!(errors.len(), 0, "There were no errors");
+}
diff --git a/third_party/rust/fluent-fallback/tests/resources/en-US/test.ftl b/third_party/rust/fluent-fallback/tests/resources/en-US/test.ftl
new file mode 100644
index 0000000000..c6a7390acf
--- /dev/null
+++ b/third_party/rust/fluent-fallback/tests/resources/en-US/test.ftl
@@ -0,0 +1,4 @@
+hello-world = Hello World [en]
+
+message-1 = Message 1 Value [en]
+ .attr1 = Message 1 Attribute [en]
diff --git a/third_party/rust/fluent-fallback/tests/resources/en-US/test2.ftl b/third_party/rust/fluent-fallback/tests/resources/en-US/test2.ftl
new file mode 100644
index 0000000000..faeae76cfe
--- /dev/null
+++ b/third_party/rust/fluent-fallback/tests/resources/en-US/test2.ftl
@@ -0,0 +1,10 @@
+hello-world-2 = Hello World 2 [en]
+hello-world-3 = Hello World 3 [en]
+
+message-2 = Message 2 Value [en]
+ .attr1 = Message 2 Attribute [en]
+
+message-3 =
+ .attr1 = Message 3 Attribute [en]
+
+message-4 = Hello, { $userName }. [en]
diff --git a/third_party/rust/fluent-fallback/tests/resources/pl/test.ftl b/third_party/rust/fluent-fallback/tests/resources/pl/test.ftl
new file mode 100644
index 0000000000..6e2f8835f1
--- /dev/null
+++ b/third_party/rust/fluent-fallback/tests/resources/pl/test.ftl
@@ -0,0 +1,4 @@
+hello-world = Hello World [pl]
+
+message-1 = Message 1 Value [pl]
+ .attr1 = Message 1 Attribute [pl]
diff --git a/third_party/rust/fluent-fallback/tests/resources/pl/test2.ftl b/third_party/rust/fluent-fallback/tests/resources/pl/test2.ftl
new file mode 100644
index 0000000000..35d646a520
--- /dev/null
+++ b/third_party/rust/fluent-fallback/tests/resources/pl/test2.ftl
@@ -0,0 +1,9 @@
+hello-world-2 = Hello World 2 [pl]
+
+message-2 = Message 2 Value [pl]
+ .attr1 = Message 2 Attribute [pl]
+
+message-3 =
+ .attr1 = Message 3 Attribute [pl]
+
+message-4 = Hello, { $userName }. [pl]
diff --git a/third_party/rust/fluent-langneg/.cargo-checksum.json b/third_party/rust/fluent-langneg/.cargo-checksum.json
new file mode 100644
index 0000000000..bf0abede56
--- /dev/null
+++ b/third_party/rust/fluent-langneg/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"1b11d8d30fe978704012e27981f8d50a3462319594b54ed2e71eaf85284d61eb","README.md":"a4f17c795725dcb84cdf1e327a61306e82aaa2ca1908c9ea95c0fbe9d53216fd","benches/negotiate.rs":"f14c49d75413fb4b248f8f586c046340d61f0682eb0860db326f1f415e1bceb9","src/accepted_languages.rs":"74fe73bb8c3f36d3b8b85bfdc55731c234c20e92245b0f89eb1e8b68af47c17c","src/lib.rs":"529e3c9810688c3a5d216c977b968a775f83a85c2da90d669f2cfc5eb6c71361","src/negotiate/likely_subtags.rs":"44531e2bbf3a2155771f197f863dffdce403d3e8dd0e1d4f36f7178e52e5a3a3","src/negotiate/mod.rs":"e8aa5ecf08b866d83c957230586cb9c03880473406d7cca28cadf9e883310a15"},"package":"2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94"} \ No newline at end of file
diff --git a/third_party/rust/fluent-langneg/Cargo.toml b/third_party/rust/fluent-langneg/Cargo.toml
new file mode 100644
index 0000000000..58aae3c672
--- /dev/null
+++ b/third_party/rust/fluent-langneg/Cargo.toml
@@ -0,0 +1,61 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+edition = "2018"
+name = "fluent-langneg"
+version = "0.13.0"
+authors = ["Zibi Braniecki <gandalf@mozilla.com>"]
+include = ["src/**/*", "benches/*.rs", "Cargo.toml", "README.md"]
+description = "A library for language and locale negotiation.\n"
+homepage = "http://projectfluent.org/"
+readme = "README.md"
+categories = ["internationalization", "localization"]
+license = "Apache-2.0"
+repository = "https://github.com/projectfluent/fluent-langneg-rs"
+
+[[bench]]
+name = "negotiate"
+harness = false
+[dependencies.unic-langid]
+version = "0.9"
+[dev-dependencies.criterion]
+version = "0.3"
+
+[dev-dependencies.serde]
+version = "1.0"
+features = ["derive"]
+
+[dev-dependencies.serde_json]
+version = "1.0"
+
+[dev-dependencies.unic-langid]
+version = "0.9"
+features = ["macros"]
+
+[dev-dependencies.unic-locale]
+version = "0.9"
+features = ["macros"]
+
+[features]
+cldr = ["unic-langid/likelysubtags"]
+default = []
+[badges.coveralls]
+branch = "master"
+repository = "projectfluent/fluent-langneg-rs"
+service = "github"
+
+[badges.maintenance]
+status = "actively-developed"
+
+[badges.travis-ci]
+repository = "projectfluent/fluent-langneg-rs"
diff --git a/third_party/rust/fluent-langneg/README.md b/third_party/rust/fluent-langneg/README.md
new file mode 100644
index 0000000000..bdff7649f8
--- /dev/null
+++ b/third_party/rust/fluent-langneg/README.md
@@ -0,0 +1,113 @@
+# Fluent LangNeg
+
+**Fluent LangNeg is a library for language and locale identifier negotiation.**
+
+[![crates.io](http://meritbadge.herokuapp.com/fluent-langneg)](https://crates.io/crates/fluent-langneg)
+[![Build Status](https://travis-ci.org/projectfluent/fluent-langneg-rs.svg?branch=master)](https://travis-ci.org/projectfluent/fluent-langneg-rs)
+[![Coverage Status](https://coveralls.io/repos/github/projectfluent/fluent-langneg-rs/badge.svg?branch=master)](https://coveralls.io/github/projectfluent/fluent-langneg-rs?branch=master)
+
+Introduction
+------------
+
+This is a Rust implementation of fluent-langneg library which is a part of Project Fluent.
+
+The library uses [unic-langid](https://github.com/zbraniecki/unic-locale) and [unic-locale](https://github.com/zbraniecki/unic-locale) to retrieve and operate on Unicode Language and Locale Identifiers.
+The library provides algorithm for negotiating between lists of locales.
+
+Usage
+-----
+
+```rust
+use fluent_langneg::negotiate_languages;
+use fluent_langneg::NegotiationStrategy;
+use fluent_langneg::convert_vec_str_to_langids_lossy;
+use unic_langid::LanguageIdentifier
+
+// Since langid parsing from string is fallible, we'll use a helper
+// function which strips any langids that failed to parse.
+let requested = convert_vec_str_to_langids_lossy(&["de-DE", "fr-FR", "en-US"]);
+let available = convert_vec_str_to_langids_lossy(&["it", "fr", "de-AT", "fr-CA", "en-US"]);
+let default: LanguageIdentifier = "en-US".parse().expect("Parsing langid failed.");
+
+let supported = negotiate_languages(
+ &requested,
+ &available,
+ Some(&default),
+ NegotiationStrategy::Filtering
+);
+
+let expected = convert_vec_str_to_langids_lossy(&["de-AT", "fr", "fr-CA", "en-US"]);
+assert_eq!(supported,
+ expected.iter().map(|t| t.as_ref()).collect::<Vec<&LanguageIdentifier>>());
+```
+
+See [docs.rs][] for more examples.
+
+[docs.rs]: https://docs.rs/fluent-langneg/
+
+Status
+------
+
+The implementation is complete according to fluent-langneg
+corpus of tests, which means that it parses, serializes and negotiates as expected.
+
+The negotiation methods can operate on lists of `LanguageIdentifier` or `Locale`.
+
+The remaining work is on the path to 1.0 is to gain in-field experience of using it,
+add more tests and ensure that bad input is correctly handled.
+
+Compatibility
+-------------
+
+The API is based on [UTS 35][] definition of [Unicode Locale Identifier][] and is aiming to
+parse and serialize all locale identifiers according to that definition.
+
+*Note*: Unicode Locale Identifier is similar, but different, from what [BCP47][] specifies under
+the name Language Tag.
+For most locale management and negotiation needs, the Unicode Locale Identifier used in this crate is likely a better choice,
+but in some case, like HTTP Accepted Headers, you may need the complete BCP47 Language Tag implementation which
+this crate does not provide.
+
+Language negotiation algorithms are custom Project Fluent solutions,
+based on [RFC4647][].
+
+The language negotiation strategies aim to replicate the best-effort matches with
+the most limited amount of data. The algorithm returns reasonable
+results without any database, but the results can be improved with either limited
+or full [CLDR likely-subtags][] database.
+
+The result is a balance chosen for Project Fluent and may differ from other
+implementations of language negotiation algorithms which may choose different
+tradeoffs.
+
+[BCP47]: https://tools.ietf.org/html/bcp47
+[RFC6067]: https://www.ietf.org/rfc/rfc6067.txt
+[UTS 35]: http://www.unicode.org/reports/tr35/#Locale_Extension_Key_and_Type_Data
+[RFC4647]: https://tools.ietf.org/html/rfc4647
+[CLDR likely-subtags]: http://www.unicode.org/cldr/charts/latest/supplemental/likely_subtags.html
+[Unicode Locale Identifier]: (http://unicode.org/reports/tr35/#Identifiers)
+
+Alternatives
+------------
+
+Although Fluent Locale aims to stay close to W3C Accepted Languages, it does not aim
+to implement the full behavior and some aspects of the language negotiation strategy
+recommended by W3C, such as weights, are not a target right now.
+
+For such purposes, [rust-language-tags][] crate seems to be a better choice.
+
+[rust-language-tags]: https://github.com/pyfisch/rust-language-tags
+
+Performance
+-----------
+
+The crate is considered to be fully optimized for production.
+
+
+Develop
+-------
+
+ cargo build
+ cargo test
+ cargo bench
+
diff --git a/third_party/rust/fluent-langneg/benches/negotiate.rs b/third_party/rust/fluent-langneg/benches/negotiate.rs
new file mode 100644
index 0000000000..2ca70d59ec
--- /dev/null
+++ b/third_party/rust/fluent-langneg/benches/negotiate.rs
@@ -0,0 +1,40 @@
+use criterion::criterion_group;
+use criterion::criterion_main;
+use criterion::Criterion;
+
+use fluent_langneg::convert_vec_str_to_langids_lossy;
+use fluent_langneg::negotiate_languages;
+
+use unic_langid::LanguageIdentifier;
+
+#[no_mangle]
+#[inline(never)]
+fn do_negotiate<'a>(
+ requested: &[LanguageIdentifier],
+ available: &'a [LanguageIdentifier],
+) -> Vec<&'a LanguageIdentifier> {
+ negotiate_languages(
+ requested,
+ available,
+ None,
+ fluent_langneg::NegotiationStrategy::Filtering,
+ )
+}
+
+fn negotiate_bench(c: &mut Criterion) {
+ let requested = &["de", "it", "ru"];
+ let available = &[
+ "en-US", "fr", "de", "en-GB", "it", "pl", "ru", "sr-Cyrl", "sr-Latn", "zh-Hant", "zh-Hans",
+ "ja-JP", "he-IL", "de-DE", "de-IT",
+ ];
+
+ let requested = convert_vec_str_to_langids_lossy(requested);
+ let available = convert_vec_str_to_langids_lossy(available);
+
+ c.bench_function("negotiate", |b| {
+ b.iter(|| do_negotiate(&requested, &available))
+ });
+}
+
+criterion_group!(benches, negotiate_bench);
+criterion_main!(benches);
diff --git a/third_party/rust/fluent-langneg/src/accepted_languages.rs b/third_party/rust/fluent-langneg/src/accepted_languages.rs
new file mode 100644
index 0000000000..58cf277703
--- /dev/null
+++ b/third_party/rust/fluent-langneg/src/accepted_languages.rs
@@ -0,0 +1,41 @@
+//! This function parses Accept-Language string into a list of language tags that
+//! can be later passed to language negotiation functions.
+//!
+//! # Example:
+//!
+//! ```
+//! use fluent_langneg::negotiate_languages;
+//! use fluent_langneg::NegotiationStrategy;
+//! use fluent_langneg::parse_accepted_languages;
+//! use fluent_langneg::convert_vec_str_to_langids_lossy;
+//! use unic_langid::LanguageIdentifier;
+//!
+//! let requested = parse_accepted_languages("de-AT;0.9,de-DE;0.8,de;0.7;en-US;0.5");
+//! let available = convert_vec_str_to_langids_lossy(&["fr", "pl", "de", "en-US"]);
+//! let default: LanguageIdentifier = "en-US".parse().expect("Failed to parse a langid.");
+//!
+//! let supported = negotiate_languages(
+//! &requested,
+//! &available,
+//! Some(&default),
+//! NegotiationStrategy::Filtering
+//! );
+//!
+//! let expected = convert_vec_str_to_langids_lossy(&["de", "en-US"]);
+//! assert_eq!(supported,
+//! expected.iter().map(|t| t.as_ref()).collect::<Vec<&LanguageIdentifier>>());
+//! ```
+//!
+//! This function ignores the weights associated with the locales, since Fluent Locale
+//! language negotiation only uses the order of locales, not the weights.
+//!
+
+use unic_langid::LanguageIdentifier;
+
+pub fn parse(s: &str) -> Vec<LanguageIdentifier> {
+ s.split(',')
+ .map(|t| t.trim().split(';').nth(0).unwrap())
+ .filter(|t| !t.is_empty())
+ .filter_map(|t| t.parse().ok())
+ .collect()
+}
diff --git a/third_party/rust/fluent-langneg/src/lib.rs b/third_party/rust/fluent-langneg/src/lib.rs
new file mode 100644
index 0000000000..865bfc2758
--- /dev/null
+++ b/third_party/rust/fluent-langneg/src/lib.rs
@@ -0,0 +1,49 @@
+//! fluent-langneg is an API for operating on locales and language tags.
+//! It's part of Project Fluent, a localization framework designed to unleash
+//! the expressive power of the natural language.
+//!
+//! The primary use of fluent-langneg is to parse/modify/serialize language tags
+//! and to perform language negotiation.
+//!
+//! fluent-langneg operates on a subset of [BCP47](http://tools.ietf.org/html/bcp47).
+//! It can parse full BCP47 language tags, and will serialize them back,
+//! but currently only allows for operations on primary subtags and
+//! unicode extension keys.
+//!
+//! In result fluent-langneg is not suited to replace full implementations of
+//! BCP47 like [rust-language-tags](https://github.com/pyfisch/rust-language-tags),
+//! but is arguably a better option for use cases involving operations on
+//! language tags and for language negotiation.
+
+pub mod accepted_languages;
+pub mod negotiate;
+
+pub use accepted_languages::parse as parse_accepted_languages;
+pub use negotiate::negotiate_languages;
+pub use negotiate::NegotiationStrategy;
+
+use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
+
+pub fn convert_vec_str_to_langids<'a, I, J>(
+ input: I,
+) -> Result<Vec<LanguageIdentifier>, LanguageIdentifierError>
+where
+ I: IntoIterator<Item = J>,
+ J: AsRef<[u8]> + 'a,
+{
+ input
+ .into_iter()
+ .map(|s| LanguageIdentifier::from_bytes(s.as_ref()))
+ .collect()
+}
+
+pub fn convert_vec_str_to_langids_lossy<'a, I, J>(input: I) -> Vec<LanguageIdentifier>
+where
+ I: IntoIterator<Item = J>,
+ J: AsRef<[u8]> + 'a,
+{
+ input
+ .into_iter()
+ .filter_map(|t| LanguageIdentifier::from_bytes(t.as_ref()).ok())
+ .collect()
+}
diff --git a/third_party/rust/fluent-langneg/src/negotiate/likely_subtags.rs b/third_party/rust/fluent-langneg/src/negotiate/likely_subtags.rs
new file mode 100644
index 0000000000..60a7b7a525
--- /dev/null
+++ b/third_party/rust/fluent-langneg/src/negotiate/likely_subtags.rs
@@ -0,0 +1,39 @@
+use unic_langid::LanguageIdentifier;
+
+static REGION_MATCHING_KEYS: &[&str] = &[
+ "az", "bg", "cs", "de", "es", "fi", "fr", "hu", "it", "lt", "lv", "nl", "pl", "ro", "ru",
+];
+
+pub trait MockLikelySubtags {
+ fn maximize(&mut self) -> bool;
+}
+
+impl MockLikelySubtags for LanguageIdentifier {
+ fn maximize(&mut self) -> bool {
+ let extended = match self.to_string().as_str() {
+ "en" => "en-Latn-US",
+ "fr" => "fr-Latn-FR",
+ "sr" => "sr-Cyrl-SR",
+ "sr-RU" => "sr-Latn-SR",
+ "az-IR" => "az-Arab-IR",
+ "zh-GB" => "zh-Hant-GB",
+ "zh-US" => "zh-Hant-US",
+ _ => {
+ let lang = self.language;
+
+ for subtag in REGION_MATCHING_KEYS {
+ if lang == *subtag {
+ self.region = Some(subtag.parse().unwrap());
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ let langid: LanguageIdentifier = extended.parse().expect("Failed to parse langid.");
+ self.language = langid.language;
+ self.script = langid.script;
+ self.region = langid.region;
+ true
+ }
+}
diff --git a/third_party/rust/fluent-langneg/src/negotiate/mod.rs b/third_party/rust/fluent-langneg/src/negotiate/mod.rs
new file mode 100644
index 0000000000..4b3587fd40
--- /dev/null
+++ b/third_party/rust/fluent-langneg/src/negotiate/mod.rs
@@ -0,0 +1,233 @@
+//! Language Negotiation is a process in which locales from different
+//! sources are filtered and sorted in an effort to produce the best
+//! possible selection of them.
+//!
+//! There are multiple language negotiation strategies, most popular is
+//! described in [RFC4647](https://www.ietf.org/rfc/rfc4647.txt).
+//!
+//! The algorithm is based on the BCP4647 3.3.2 Extended Filtering algorithm,
+//! with several modifications.
+//!
+//! # Example:
+//!
+//! ```
+//! use fluent_langneg::negotiate_languages;
+//! use fluent_langneg::NegotiationStrategy;
+//! use fluent_langneg::convert_vec_str_to_langids_lossy;
+//! use unic_langid::LanguageIdentifier;
+//!
+//! let requested = convert_vec_str_to_langids_lossy(&["pl", "fr", "en-US"]);
+//! let available = convert_vec_str_to_langids_lossy(&["it", "de", "fr", "en-GB", "en_US"]);
+//! let default: LanguageIdentifier = "en-US".parse().expect("Parsing langid failed.");
+//!
+//! let supported = negotiate_languages(
+//! &requested,
+//! &available,
+//! Some(&default),
+//! NegotiationStrategy::Filtering
+//! );
+//!
+//! let expected = convert_vec_str_to_langids_lossy(&["fr", "en-US", "en-GB"]);
+//! assert_eq!(supported,
+//! expected.iter().map(|t| t.as_ref()).collect::<Vec<&LanguageIdentifier>>());
+//! ```
+//!
+//! # The exact algorithm is custom, and consists of a 6 level strategy:
+//!
+//! ### 1) Attempt to find an exact match for each requested locale in available locales.
+//!
+//! Example:
+//!
+//! ```text
+//! // [requested] * [available] = [supported]
+//!
+//! ["en-US"] * ["en-US"] = ["en-US"]
+//! ```
+//!
+//! ### 2) Attempt to match a requested locale to an available locale treated as a locale range.
+//!
+//! Example:
+//!
+//! ```text
+//! // [requested] * [available] = [supported]
+//!
+//! ["en-US"] * ["en"] = ["en"]
+//! ^^
+//! |-- becomes "en-*-*-*"
+//! ```
+//!
+//! ### 3) Maximize the requested locale to find the best match in available locales.
+//!
+//! This part uses ICU's likelySubtags or similar database.
+//!
+//! Example:
+//!
+//! ```text
+//! // [requested] * [available] = [supported]
+//!
+//! ["en"] * ["en-GB", "en-US"] = ["en-US"]
+//! ^^ ^^^^^ ^^^^^
+//! | | |
+//! | |----------- become "en-*-GB-*" and "en-*-US-*"
+//! |
+//! |-- ICU likelySubtags expands it to "en-Latn-US"
+//! ```
+//!
+//! ### 4) Attempt to look up for a different variant of the same locale.
+//!
+//! Example:
+//!
+//! ```text
+//! // [requested] * [available] = [supported]
+//!
+//! ["ja-JP-win"] * ["ja-JP-mac"] = ["ja-JP-mac"]
+//! ^^^^^^^^^ ^^^^^^^^^
+//! | |-- become "ja-*-JP-mac"
+//! |
+//! |----------- replace variant with range: "ja-JP-*"
+//! ```
+//!
+//! ### 5) Look up for a maximized version of the requested locale, stripped of the region code.
+//!
+//! Example:
+//!
+//! ```text
+//! // [requested] * [available] = [supported]
+//!
+//! ["en-CA"] * ["en-ZA", "en-US"] = ["en-US", "en-ZA"]
+//! ^^^^^
+//! | ^^^^^ ^^^^^
+//! | | |
+//! | |----------- become "en-*-ZA-*" and "en-*-US-*"
+//! |
+//! |----------- strip region produces "en", then lookup likelySubtag: "en-Latn-US"
+//! ```
+//!
+//!
+//! ### 6) Attempt to look up for a different region of the same locale.
+//!
+//! Example:
+//!
+//! ```text
+//! // [requested] * [available] = [supported]
+//!
+//! ["en-GB"] * ["en-AU"] = ["en-AU"]
+//! ^^^^^ ^^^^^
+//! | |-- become "en-*-AU-*"
+//! |
+//! |----- replace region with range: "en-*"
+//! ```
+//!
+
+use unic_langid::LanguageIdentifier;
+
+#[cfg(not(feature = "cldr"))]
+mod likely_subtags;
+#[cfg(not(feature = "cldr"))]
+use likely_subtags::MockLikelySubtags;
+
+#[derive(PartialEq, Debug, Clone, Copy)]
+pub enum NegotiationStrategy {
+ Filtering,
+ Matching,
+ Lookup,
+}
+
+pub fn filter_matches<'a, R: 'a + AsRef<LanguageIdentifier>, A: 'a + AsRef<LanguageIdentifier>>(
+ requested: &[R],
+ available: &'a [A],
+ strategy: NegotiationStrategy,
+) -> Vec<&'a A> {
+ let mut supported_locales = vec![];
+
+ let mut available_locales: Vec<&A> = available.iter().collect();
+
+ for req in requested {
+ let mut req = req.as_ref().to_owned();
+ macro_rules! test_strategy {
+ ($self_as_range:expr, $other_as_range:expr) => {{
+ let mut match_found = false;
+ available_locales.retain(|locale| {
+ if strategy != NegotiationStrategy::Filtering && match_found {
+ return true;
+ }
+
+ if locale
+ .as_ref()
+ .matches(&req, $self_as_range, $other_as_range)
+ {
+ match_found = true;
+ supported_locales.push(*locale);
+ return false;
+ }
+ true
+ });
+
+ if match_found {
+ match strategy {
+ NegotiationStrategy::Filtering => {}
+ NegotiationStrategy::Matching => continue,
+ NegotiationStrategy::Lookup => break,
+ }
+ }
+ }};
+ }
+
+ // 1) Try to find a simple (case-insensitive) string match for the request.
+ test_strategy!(false, false);
+
+ // 2) Try to match against the available locales treated as ranges.
+ test_strategy!(true, false);
+
+ // Per Unicode TR35, 4.4 Locale Matching, we don't add likely subtags to
+ // requested locales, so we'll skip it from the rest of the steps.
+ if req.language.is_empty() {
+ continue;
+ }
+
+ // 3) Try to match against a maximized version of the requested locale
+ if req.maximize() {
+ test_strategy!(true, false);
+ }
+
+ // 4) Try to match against a variant as a range
+ req.clear_variants();
+ test_strategy!(true, true);
+
+ // 5) Try to match against the likely subtag without region
+ req.region = None;
+ if req.maximize() {
+ test_strategy!(true, false);
+ }
+
+ // 6) Try to match against a region as a range
+ req.region = None;
+ test_strategy!(true, true);
+ }
+
+ supported_locales
+}
+
+pub fn negotiate_languages<
+ 'a,
+ R: 'a + AsRef<LanguageIdentifier>,
+ A: 'a + AsRef<LanguageIdentifier> + PartialEq,
+>(
+ requested: &[R],
+ available: &'a [A],
+ default: Option<&'a A>,
+ strategy: NegotiationStrategy,
+) -> Vec<&'a A> {
+ let mut supported = filter_matches(requested, available, strategy);
+
+ if let Some(default) = default {
+ if strategy == NegotiationStrategy::Lookup {
+ if supported.is_empty() {
+ supported.push(default);
+ }
+ } else if !supported.contains(&default) {
+ supported.push(default);
+ }
+ }
+ supported
+}
diff --git a/third_party/rust/fluent-pseudo/.cargo-checksum.json b/third_party/rust/fluent-pseudo/.cargo-checksum.json
new file mode 100644
index 0000000000..58022a7611
--- /dev/null
+++ b/third_party/rust/fluent-pseudo/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"3490fcbcbf99a8488b8cf276fe21f248e32084cf607afd418b5d99c6f558fb24","LICENSE-APACHE":"5db2b182453ff32ed40f7da63589c9667a3f8bd8b16b1471b152caae56f77e45","LICENSE-MIT":"49c0b000c03731d9e3970dc059ad4ca345d773681f4a612b0024435b663e0220","README.md":"f1b5a3664d893f6779fe7573cc34e8ab38e4ef9f2d4e19019d7aa46b3e4d91b5","src/lib.rs":"6ae0a9b042f5627d69f91d21dbd8845bf11cc66528065a7ba93fc982cf8bfa17"},"package":"fded62f95114baa010b4fcd54aedc7b0762059cf6dfb0097f5b5c95fb56ed42f"} \ No newline at end of file
diff --git a/third_party/rust/fluent-pseudo/Cargo.toml b/third_party/rust/fluent-pseudo/Cargo.toml
new file mode 100644
index 0000000000..5abfc069a1
--- /dev/null
+++ b/third_party/rust/fluent-pseudo/Cargo.toml
@@ -0,0 +1,27 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+edition = "2018"
+name = "fluent-pseudo"
+version = "0.3.1"
+authors = ["Zibi Braniecki <gandalf@mozilla.com>", "Staś Małolepszy <stas@mozilla.com>"]
+include = ["src/**/*", "benches/*.rs", "Cargo.toml", "README.md", "LICENSE-APACHE", "LICENSE-MIT"]
+description = "Pseudolocalization transformation API for use with Project Fluent API.\n"
+homepage = "http://www.projectfluent.org"
+readme = "README.md"
+keywords = ["localization", "l10n", "i18n", "intl", "internationalization"]
+categories = ["localization", "internationalization"]
+license = "Apache-2.0/MIT"
+repository = "https://github.com/projectfluent/fluent-rs"
+[dependencies.regex]
+version = "1"
diff --git a/third_party/rust/fluent-pseudo/LICENSE-APACHE b/third_party/rust/fluent-pseudo/LICENSE-APACHE
new file mode 100644
index 0000000000..35582f166b
--- /dev/null
+++ b/third_party/rust/fluent-pseudo/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017 Mozilla
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/third_party/rust/fluent-pseudo/LICENSE-MIT b/third_party/rust/fluent-pseudo/LICENSE-MIT
new file mode 100644
index 0000000000..5655fa311c
--- /dev/null
+++ b/third_party/rust/fluent-pseudo/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright 2017 Mozilla
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/rust/fluent-pseudo/README.md b/third_party/rust/fluent-pseudo/README.md
new file mode 100644
index 0000000000..152ed4df53
--- /dev/null
+++ b/third_party/rust/fluent-pseudo/README.md
@@ -0,0 +1,48 @@
+# Fluent
+
+`fluent-pseudo` is a Rust implementation of the pseudolocalization API for [Project Fluent](https://projectfluent.org/), a localization
+framework designed to unleash the entire expressive power of natural language
+translations.
+
+[![crates.io](https://meritbadge.herokuapp.com/fluent-pseudo)](https://crates.io/crates/fluent-pseudo)
+[![Build and test](https://github.com/projectfluent/fluent-rs/workflows/Build%20and%20test/badge.svg)](https://github.com/projectfluent/fluent-rs/actions?query=branch%3Amaster+workflow%3A%22Build+and+test%22)
+[![Coverage Status](https://coveralls.io/repos/github/projectfluent/fluent-rs/badge.svg?branch=master)](https://coveralls.io/github/projectfluent/fluent-rs?branch=master)
+
+Usage
+-----
+
+```rust
+use fluent_bundle::{FluentBundle, FluentResource};
+use unic_langid::langid;
+use fluent_pseudo::transform;
+
+fn transform_wrapper(s: &str) -> Cow<str> {
+ // Not flipped and elongated pseudolocalization.
+ transform(s, false, true, false)
+}
+
+
+fn main() {
+ let ftl_string = "hello-world = Hello, world!".to_owned();
+ let res = FluentResource::try_new(ftl_string)
+ .expect("Could not parse an FTL string.");
+
+ let langid_en = langid!("en");
+ let mut bundle = FluentBundle::new(vec![langid_en]);
+
+ // Set pseudolocalization
+ bundle.set_transform(Some(transform_wrapper));
+
+ bundle.add_resource(&res)
+ .expect("Failed to add FTL resources to the bundle.");
+
+ let msg = bundle.get_message("hello-world")
+ .expect("Failed to retrieve a message.");
+ let val = msg.value.expect("Message has no value.");
+
+ let mut errors = vec![];
+ let value = bundle.format_pattern(val, None, &mut errors);
+
+ assert_eq!(&value, "Ħḗḗŀŀǿǿ Ẇǿǿřŀḓ!");
+}
+```
diff --git a/third_party/rust/fluent-pseudo/src/lib.rs b/third_party/rust/fluent-pseudo/src/lib.rs
new file mode 100644
index 0000000000..607b91eaef
--- /dev/null
+++ b/third_party/rust/fluent-pseudo/src/lib.rs
@@ -0,0 +1,135 @@
+use regex::Captures;
+use regex::Regex;
+use std::borrow::Cow;
+
+static TRANSFORM_SMALL_MAP: &[char] = &[
+ 'a', 'ƀ', 'ƈ', 'ḓ', 'e', 'ƒ', 'ɠ', 'ħ', 'i', 'ĵ', 'ķ', 'ŀ', 'ḿ', 'ƞ', 'o', 'ƥ', 'ɋ', 'ř', 'ş',
+ 'ŧ', 'u', 'ṽ', 'ẇ', 'ẋ', 'ẏ', 'ẑ',
+];
+static TRANSFORM_CAPS_MAP: &[char] = &[
+ 'A', 'Ɓ', 'Ƈ', 'Ḓ', 'E', 'Ƒ', 'Ɠ', 'Ħ', 'I', 'Ĵ', 'Ķ', 'Ŀ', 'Ḿ', 'Ƞ', 'O', 'Ƥ', 'Ɋ', 'Ř', 'Ş',
+ 'Ŧ', 'U', 'Ṽ', 'Ẇ', 'Ẋ', 'Ẏ', 'Ẑ',
+];
+
+static FLIPPED_SMALL_MAP: &[char] = &[
+ 'ɐ', 'q', 'ɔ', 'p', 'ǝ', 'ɟ', 'ƃ', 'ɥ', 'ı', 'ɾ', 'ʞ', 'ʅ', 'ɯ', 'u', 'o', 'd', 'b', 'ɹ', 's',
+ 'ʇ', 'n', 'ʌ', 'ʍ', 'x', 'ʎ', 'z',
+];
+static FLIPPED_CAPS_MAP: &[char] = &[
+ '∀', 'Ԑ', 'Ↄ', 'ᗡ', 'Ǝ', 'Ⅎ', '⅁', 'H', 'I', 'ſ', 'Ӽ', '⅂', 'W', 'N', 'O', 'Ԁ', 'Ò', 'ᴚ', 'S',
+ '⊥', '∩', 'Ʌ', 'M', 'X', '⅄', 'Z',
+];
+
+static mut RE_EXCLUDED: Option<Regex> = None;
+static mut RE_AZ: Option<Regex> = None;
+
+pub fn transform_dom(s: &str, flipped: bool, elongate: bool, with_markers: bool) -> Cow<str> {
+ // Exclude access-keys and other single-char messages
+ if s.len() == 1 {
+ return s.into();
+ }
+
+ // XML entities (&#x202a;) and XML tags.
+ let re_excluded =
+ unsafe { RE_EXCLUDED.get_or_insert_with(|| Regex::new(r"&[#\w]+;|<\s*.+?\s*>").unwrap()) };
+
+ let mut result = Cow::from(s);
+
+ let mut pos = 0;
+ let mut diff = 0;
+
+ for cap in re_excluded.captures_iter(s) {
+ let capture = cap.get(0).unwrap();
+
+ let sub_len = capture.start() - pos;
+ let range = pos..capture.start();
+ let result_range = pos + diff..capture.start() + diff;
+ let sub = &s[range.clone()];
+ let transform_sub = transform(&sub, false, true);
+ diff += transform_sub.len() - sub_len;
+ result
+ .to_mut()
+ .replace_range(result_range.clone(), &transform_sub);
+ pos = capture.end();
+ }
+ let range = pos..s.len();
+ let result_range = pos + diff..result.len();
+ let transform_sub = transform(&s[range], flipped, elongate);
+ result.to_mut().replace_range(result_range, &transform_sub);
+
+ if with_markers {
+ return Cow::from("[") + result + "]"
+ }
+
+ result
+}
+
+pub fn transform(s: &str, flipped: bool, elongate: bool) -> Cow<str> {
+ let re_az = unsafe { RE_AZ.get_or_insert_with(|| Regex::new(r"[a-zA-Z]").unwrap()) };
+
+ let (small_map, caps_map) = if flipped {
+ (FLIPPED_SMALL_MAP, FLIPPED_CAPS_MAP)
+ } else {
+ (TRANSFORM_SMALL_MAP, TRANSFORM_CAPS_MAP)
+ };
+
+ re_az.replace_all(s, |caps: &Captures| {
+ let ch = caps[0].chars().next().unwrap();
+ let cc = ch as u8;
+ if (97..=122).contains(&cc) {
+ let pos = cc - 97;
+ let new_char = small_map[pos as usize];
+ // duplicate "a", "e", "o" and "u" to emulate ~30% longer text
+ if elongate && (cc == 97 || cc == 101 || cc == 111 || cc == 117) {
+ let mut s = new_char.to_string();
+ s.push(new_char);
+ s
+ } else {
+ new_char.to_string()
+ }
+ } else if (65..=90).contains(&cc) {
+ let pos = cc - 65;
+ let new_char = caps_map[pos as usize];
+ new_char.to_string()
+ } else {
+ ch.to_string()
+ }
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn it_works() {
+ let x = transform("Hello World", false, true);
+ assert_eq!(x, "Ħeeŀŀoo Ẇoořŀḓ");
+
+ let x = transform("Hello World", false, false);
+ assert_eq!(x, "Ħeŀŀo Ẇořŀḓ");
+
+ let x = transform("Hello World", true, false);
+ assert_eq!(x, "Hǝʅʅo Moɹʅp");
+
+ let x = transform("f", false, true);
+ assert_eq!(x, "ƒ");
+ }
+
+ #[test]
+ fn dom_test() {
+ let x = transform_dom("Hello <a>World</a>", false, true, false);
+ assert_eq!(x, "Ħeeŀŀoo <a>Ẇoořŀḓ</a>");
+
+ let x = transform_dom("Hello <a>World</a> in <b>my</b> House.", false, true, false);
+ assert_eq!(x, "Ħeeŀŀoo <a>Ẇoořŀḓ</a> iƞ <b>ḿẏ</b> Ħoouuşee.");
+
+ // Use markers.
+ let x = transform_dom("Hello World within markers", false, false, true);
+ assert_eq!(x, "[Ħeŀŀo Ẇořŀḓ ẇiŧħiƞ ḿařķeřş]");
+
+ // Don't touch single character values.
+ let x = transform_dom("f", false, true, false);
+ assert_eq!(x, "f");
+ }
+}
diff --git a/third_party/rust/fluent-syntax/.cargo-checksum.json b/third_party/rust/fluent-syntax/.cargo-checksum.json
new file mode 100644
index 0000000000..d1941314cd
--- /dev/null
+++ b/third_party/rust/fluent-syntax/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.lock":"3fd2bd8414b6f818747e28ac2e78d0d99795946f2b4c74ca5e5ca9ce1bc8f8e2","Cargo.toml":"a98f67a3a7a2c050115fffb1863b9eb7ff0de131f2125b25753aa5eb3793bef8","LICENSE-APACHE":"5db2b182453ff32ed40f7da63589c9667a3f8bd8b16b1471b152caae56f77e45","LICENSE-MIT":"49c0b000c03731d9e3970dc059ad4ca345d773681f4a612b0024435b663e0220","README.md":"0ceb23fff33406f7663d967601e1ad7650a4664565c42d618961cc3a8fc81ea5","benches/contexts/README.md":"562d317f507caedd62cbe00e6b2bb350cb970168e5ebd5b3c497e68696e21e8b","benches/parser.rs":"b207acac3cc05025a323646dc72bc32b675740ebdcc602fc37f2ca42df9c6fb3","benches/parser_iai.rs":"10253310d8abbe979ae0833df4bb0d1e2b770d1e94fe99416dec7fcd23aa3f16","src/ast/helper.rs":"39187bbc97823ba0f3a9baadd76ddc59140bf09cddeb92997dd872e41b305375","src/ast/mod.rs":"389032943da7c1809d6b4bc903ab13662d715945bbd3df974a47b29e0884715f","src/bin/parser.rs":"06aa90e1b78f5f845c46b8abba86028c4c157555b580ea59b8a08b7ccba9907c","src/bin/update_fixtures.rs":"387ca0e6331942369a3a3aa6c5ae013aa1c87c612215ce20f486ca37d4acd55d","src/lib.rs":"b4a11659d3d233073a1d820872a1d48141d3343401295032f494f251af591d78","src/parser/comment.rs":"dff6546043538e3573eea98ea7eda3fd55d5846e4dbcb38a0595844862d979af","src/parser/core.rs":"4ef1a455ef50235bb3ab8da0c8da90caf73fd346f1b5e96d67ee8b3fc039ad04","src/parser/errors.rs":"7ac92c323c15f9efa10a37a5ba3894f7a348cadbd5bfc053afef901bbe53ad4b","src/parser/expression.rs":"737b6670e849e8f04b06146ebddb59b8da9f082660f427a564e7eb52ec674326","src/parser/helper.rs":"26ba1d0d64455220cc054a2c7c23051854b1915bf7d1028f4db7ec8a8dc9114b","src/parser/macros.rs":"d5927d8e6757b50bbdeb122eb3a9bde7ab939a7ba41c9dfeff216d3839a5162b","src/parser/mod.rs":"27378f63f5e98890b0928d54e0a7edf4828179bf37f9ed9e5e8c2eaac111bd75","src/parser/pattern.rs":"4566796f34b5b47f76aa682355b664001e370ff313537e32649d815e4d5edd5b","src/parser/runtime.rs":"280aefcd960a4e1d402132c38c6764582e471853aa0ef143b099edbdd41f4b6c","src/parser/slice.rs":"aad45bc35ecc3ff68bcd5b2671fecb134a4535d6ee463092422dfc5cc8b25a1d","src/unicode.rs":"799299d1895e0123dc837648bdd5adb2d16f9b8312d1f070570a03d40c2d4a07"},"package":"c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78"} \ No newline at end of file
diff --git a/third_party/rust/fluent-syntax/Cargo.lock b/third_party/rust/fluent-syntax/Cargo.lock
new file mode 100644
index 0000000000..46ad03deaf
--- /dev/null
+++ b/third_party/rust/fluent-syntax/Cargo.lock
@@ -0,0 +1,672 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "bstr"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d"
+dependencies = [
+ "lazy_static",
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9"
+
+[[package]]
+name = "byteorder"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
+
+[[package]]
+name = "cast"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
+dependencies = [
+ "rustc_version",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "bitflags",
+ "textwrap",
+ "unicode-width",
+]
+
+[[package]]
+name = "const_fn"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
+
+[[package]]
+name = "criterion"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab327ed7354547cc2ef43cbe20ef68b988e70b4b593cbd66a2a61733123a3d23"
+dependencies = [
+ "atty",
+ "cast",
+ "clap",
+ "criterion-plot",
+ "csv",
+ "itertools 0.10.0",
+ "lazy_static",
+ "num-traits",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_cbor",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d"
+dependencies = [
+ "cast",
+ "itertools 0.9.0",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d"
+dependencies = [
+ "cfg-if",
+ "const_fn",
+ "crossbeam-utils",
+ "lazy_static",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "lazy_static",
+]
+
+[[package]]
+name = "csv"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d58633299b24b515ac72a3f869f8b91306a3cec616a602843a383acd6f9e97"
+dependencies = [
+ "bstr",
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "fluent-syntax"
+version = "0.11.0"
+dependencies = [
+ "criterion",
+ "glob",
+ "iai",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
+[[package]]
+name = "half"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "iai"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678"
+
+[[package]]
+name = "itertools"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "js-sys"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c"
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+
+[[package]]
+name = "memoffset"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "oorandom"
+version = "11.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
+
+[[package]]
+name = "plotters"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45ca0ae5f169d0917a7c7f5a9c1a3d3d9598f18f529dd2b8373ed988efea307a"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b07fffcddc1cb3a1de753caa4e4df03b79922ba43cf882acc1bdd7e8df9f4590"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b38a02e23bd9604b842a812063aec4ef702b57989c37b655254bb61c471ad211"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rayon"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674"
+dependencies = [
+ "autocfg",
+ "crossbeam-deque",
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "lazy_static",
+ "num_cpus",
+]
+
+[[package]]
+name = "regex"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
+
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_cbor"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622"
+dependencies = [
+ "half",
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2ada8616fad06a2d0c455adc530de4ef57605a8120cc65da9653e0e9623ca74"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
+[[package]]
+name = "walkdir"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64"
+
+[[package]]
+name = "web-sys"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/third_party/rust/fluent-syntax/Cargo.toml b/third_party/rust/fluent-syntax/Cargo.toml
new file mode 100644
index 0000000000..cc6851d45f
--- /dev/null
+++ b/third_party/rust/fluent-syntax/Cargo.toml
@@ -0,0 +1,78 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+edition = "2018"
+name = "fluent-syntax"
+version = "0.11.0"
+authors = ["Zibi Braniecki <gandalf@mozilla.com>", "Staś Małolepszy <stas@mozilla.com>"]
+include = ["src/**/*", "benches/*.rs", "Cargo.toml", "README.md", "LICENSE-APACHE", "LICENSE-MIT"]
+description = "Parser/Serializer tools for Fluent Syntax. \n"
+homepage = "http://www.projectfluent.org"
+readme = "README.md"
+keywords = ["localization", "l10n", "i18n", "intl", "internationalization"]
+categories = ["localization", "internationalization"]
+license = "Apache-2.0/MIT"
+repository = "https://github.com/projectfluent/fluent-rs"
+
+[[bin]]
+name = "parser"
+path = "src/bin/parser.rs"
+
+[[bin]]
+name = "update_fixtures"
+path = "src/bin/update_fixtures.rs"
+required-features = ["json"]
+
+[[test]]
+name = "parser_fixtures"
+path = "tests/parser_fixtures.rs"
+required-features = ["json"]
+
+[[bench]]
+name = "parser"
+harness = false
+
+[[bench]]
+name = "parser_iai"
+harness = false
+[dependencies.serde]
+version = "1.0"
+features = ["derive"]
+optional = true
+
+[dependencies.serde_json]
+version = "1.0"
+optional = true
+
+[dependencies.thiserror]
+version = "1.0"
+[dev-dependencies.criterion]
+version = "0.3"
+
+[dev-dependencies.glob]
+version = "0.3"
+
+[dev-dependencies.iai]
+version = "0.1"
+
+[dev-dependencies.serde]
+version = "1.0"
+features = ["derive"]
+
+[dev-dependencies.serde_json]
+version = "1.0"
+
+[features]
+all-benchmarks = []
+default = []
+json = ["serde", "serde_json"]
diff --git a/third_party/rust/fluent-syntax/LICENSE-APACHE b/third_party/rust/fluent-syntax/LICENSE-APACHE
new file mode 100644
index 0000000000..35582f166b
--- /dev/null
+++ b/third_party/rust/fluent-syntax/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017 Mozilla
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/third_party/rust/fluent-syntax/LICENSE-MIT b/third_party/rust/fluent-syntax/LICENSE-MIT
new file mode 100644
index 0000000000..5655fa311c
--- /dev/null
+++ b/third_party/rust/fluent-syntax/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright 2017 Mozilla
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/rust/fluent-syntax/README.md b/third_party/rust/fluent-syntax/README.md
new file mode 100644
index 0000000000..f7214aaed5
--- /dev/null
+++ b/third_party/rust/fluent-syntax/README.md
@@ -0,0 +1,63 @@
+# Fluent Syntax
+
+`fluent-syntax` is a parser/serializer API for the Fluent Syntax, part of the [Project Fluent](https://projectfluent.org/), a localization
+framework designed to unleash the entire expressive power of natural language translations.
+
+[![crates.io](https://meritbadge.herokuapp.com/fluent-syntax)](https://crates.io/crates/fluent-syntax)
+[![Build and test](https://github.com/projectfluent/fluent-rs/workflows/Build%20and%20test/badge.svg)](https://github.com/projectfluent/fluent-rs/actions?query=branch%3Amaster+workflow%3A%22Build+and+test%22)
+[![Coverage Status](https://coveralls.io/repos/github/projectfluent/fluent-rs/badge.svg?branch=master)](https://coveralls.io/github/projectfluent/fluent-rs?branch=master)
+
+Status
+------
+
+The crate currently provides just a parser, which is tracking Fluent Syntax on its way to 1.0.
+
+Local Development
+-----------------
+
+ cargo build
+ cargo test
+ cargo bench
+
+When submitting a PR please use [`cargo fmt`][] (nightly).
+
+[`cargo fmt`]: https://github.com/rust-lang-nursery/rustfmt
+
+
+Learn the FTL syntax
+--------------------
+
+FTL is a localization file format used for describing translation resources.
+FTL stands for _Fluent Translation List_.
+
+FTL is designed to be simple to read, but at the same time allows to represent
+complex concepts from natural languages like gender, plurals, conjugations, and
+others.
+
+ hello-user = Hello, { $username }!
+
+[Read the Fluent Syntax Guide][] in order to learn more about the syntax. If
+you're a tool author you may be interested in the formal [EBNF grammar][].
+
+[Read the Fluent Syntax Guide]: http://projectfluent.org/fluent/guide/
+[EBNF grammar]: https://github.com/projectfluent/fluent/tree/master/spec
+
+
+Get Involved
+------------
+
+`fluent-rs` is open-source, licensed under the Apache License, Version 2.0. We
+encourage everyone to take a look at our code and we'll listen to your
+feedback.
+
+
+Discuss
+-------
+
+We'd love to hear your thoughts on Project Fluent! Whether you're a localizer
+looking for a better way to express yourself in your language, or a developer
+trying to make your app localizable and multilingual, or a hacker looking for
+a project to contribute to, please do get in touch on discourse and the IRC channel.
+
+ - Discourse: https://discourse.mozilla.org/c/fluent
+ - IRC channel: [irc://irc.mozilla.org/l20n](irc://irc.mozilla.org/l20n)
diff --git a/third_party/rust/fluent-syntax/benches/contexts/README.md b/third_party/rust/fluent-syntax/benches/contexts/README.md
new file mode 100644
index 0000000000..7d37ac0fe5
--- /dev/null
+++ b/third_party/rust/fluent-syntax/benches/contexts/README.md
@@ -0,0 +1,4 @@
+The following context is extracted from
+the `browser.xhtml` localization context
+from mozilla-central rev 51efc4b931f7
+from 2020-03-03.
diff --git a/third_party/rust/fluent-syntax/benches/parser.rs b/third_party/rust/fluent-syntax/benches/parser.rs
new file mode 100644
index 0000000000..f14725a512
--- /dev/null
+++ b/third_party/rust/fluent-syntax/benches/parser.rs
@@ -0,0 +1,141 @@
+use criterion::criterion_group;
+use criterion::criterion_main;
+use criterion::Criterion;
+use std::collections::HashMap;
+use std::fs;
+use std::io;
+
+use fluent_syntax::parser::parse_runtime;
+
+fn read_file(path: &str) -> Result<String, io::Error> {
+ fs::read_to_string(path)
+}
+
+#[cfg(feature = "all-benchmarks")]
+fn get_resources(tests: &[&'static str]) -> HashMap<&'static str, String> {
+ let mut ftl_strings = HashMap::new();
+ for test in tests {
+ let path = format!("./benches/{}", test);
+ ftl_strings.insert(*test, read_file(&path).expect("Couldn't load file"));
+ }
+ return ftl_strings;
+}
+
+fn get_ctxs(tests: &[&'static str]) -> HashMap<&'static str, Vec<String>> {
+ let mut ftl_strings = HashMap::new();
+ for test in tests {
+ let paths = fs::read_dir(format!("./benches/contexts/{}", test)).unwrap();
+ let strings = paths
+ .into_iter()
+ .map(|p| {
+ let p = p.unwrap().path();
+ let path = p.to_str().unwrap();
+ read_file(path).unwrap()
+ })
+ .collect::<Vec<_>>();
+ ftl_strings.insert(*test, strings);
+ }
+ return ftl_strings;
+}
+
+fn parse_bench(c: &mut Criterion) {
+ #[cfg(feature = "all-benchmarks")]
+ {
+ let tests = &["simple.ftl", "preferences.ftl", "menubar.ftl"];
+
+ let mut group = c.benchmark_group("parse_resource");
+
+ for (name, resource) in get_resources(tests) {
+ group.bench_with_input(name, &resource, |b, source| {
+ b.iter(|| parse_runtime(source.as_str()).expect("Parsing of the FTL failed."))
+ });
+ }
+
+ group.finish();
+ }
+
+ let ctx_names = &["browser", "preferences"];
+
+ #[cfg(feature = "all-benchmarks")]
+ {
+ use fluent_syntax::parser::parse;
+
+ let mut group = c.benchmark_group("parse_ctx");
+
+ for (name, ctx) in get_ctxs(ctx_names) {
+ group.bench_with_input(name, &ctx, |b, ctx| {
+ b.iter(|| {
+ for source in ctx {
+ parse(source.as_str()).expect("Parsing of the FTL failed.");
+ }
+ })
+ });
+ }
+
+ group.finish();
+ }
+
+ {
+ let mut group = c.benchmark_group("parse_ctx_runtime");
+
+ for (name, ctx) in get_ctxs(ctx_names) {
+ group.bench_with_input(name, &ctx, |b, ctx| {
+ b.iter(|| {
+ for source in ctx {
+ parse_runtime(source.as_str()).expect("Parsing of the FTL failed.");
+ }
+ })
+ });
+ }
+
+ group.finish();
+ }
+
+ #[cfg(feature = "all-benchmarks")]
+ {
+ use fluent_syntax::unicode::{unescape_unicode, unescape_unicode_to_string};
+
+ let strings = &[
+ "foo",
+ "This is an example value",
+ "Hello \\u00e3\\u00e9 World",
+ "\\u004c\\u006f\\u0072\\u0065\\u006d \\u0069\\u0070\\u0073\\u0075\\u006d \\u0064\\u006f\\u006c\\u006f\\u0072 \\u0073\\u0069\\u0074 \\u0061\\u006d\\u0065\\u0074",
+ "Let me introduce \\\"The\\\" Fluent",
+ "And here's an example of \\\\ a character to be escaped",
+ "But this message is completely unescape free",
+ "And so is this one",
+ "Maybe this one is as well completely escape free",
+ "Welcome to Mozilla Firefox",
+ "\\u0054\\u0068\\u0065\\u0073\\u0065 \\u0073\\u0065\\u0074\\u0074\\u0069\\u006e\\u0067\\u0073 \\u0061\\u0072\\u0065 \\u0074\\u0061\\u0069\\u006c\\u006f\\u0072\\u0065\\u0064 \\u0074\\u006f \\u0079\\u006f\\u0075\\u0072 \\u0063\\u006f\\u006d\\u0070\\u0075\\u0074\\u0065\\u0072\\u2019\\u0073 \\u0068\\u0061\\u0072\\u0064\\u0077\\u0061\\u0072\\u0065 \\u0061\\u006e\\u0064 \\u006f\\u0070\\u0065\\u0072\\u0061\\u0074\\u0069\\u006e\\u0067 \\u0073\\u0079\\u0073\\u0074\\u0065\\u006d\\u002e",
+ "These settings are tailored to your computer’s hardware and operating system",
+ "Use recommended performance settings",
+ "\\u0041\\u0064\\u0064\\u0069\\u0074\\u0069\\u006f\\u006e\\u0061\\u006c \\u0063\\u006f\\u006e\\u0074\\u0065\\u006e\\u0074 \\u0070\\u0072\\u006f\\u0063\\u0065\\u0073\\u0073\\u0065\\u0073 \\u0063\\u0061\\u006e \\u0069\\u006d\\u0070\\u0072\\u006f\\u0076\\u0065 \\u0070\\u0065\\u0072\\u0066\\u006f\\u0072\\u006d\\u0061\\u006e\\u0063\\u0065 \\u0077\\u0068\\u0065\\u006e \\u0075\\u0073\\u0069\\u006e\\u0067 \\u006d\\u0075\\u006c\\u0074\\u0069\\u0070\\u006c\\u0065 \\u0074\\u0061\\u0062\\u0073\\u002c \\u0062\\u0075\\u0074 \\u0077\\u0069\\u006c\\u006c \\u0061\\u006c\\u0073\\u006f \\u0075\\u0073\\u0065 \\u006d\\u006f\\u0072\\u0065 \\u006d\\u0065\\u006d\\u006f\\u0072\\u0079\\u002e",
+ "Additional content processes can improve performance when using multiple tabs, but will also use more memory.",
+ ];
+
+ let mut group = c.benchmark_group("unicode");
+
+ group.bench_function("writer", |b| {
+ b.iter(|| {
+ let mut result = String::new();
+ for s in strings {
+ unescape_unicode(&mut result, s).unwrap();
+ result.clear();
+ }
+ })
+ });
+
+ group.bench_function("to_string", |b| {
+ b.iter(|| {
+ for s in strings {
+ let _ = unescape_unicode_to_string(s);
+ }
+ })
+ });
+
+ group.finish();
+ }
+}
+
+criterion_group!(benches, parse_bench,);
+criterion_main!(benches);
diff --git a/third_party/rust/fluent-syntax/benches/parser_iai.rs b/third_party/rust/fluent-syntax/benches/parser_iai.rs
new file mode 100644
index 0000000000..f2eed5e5b1
--- /dev/null
+++ b/third_party/rust/fluent-syntax/benches/parser_iai.rs
@@ -0,0 +1,15 @@
+use fluent_syntax::parser::parse_runtime;
+
+fn iai_parse_ctx_runtime() {
+ let files = &[
+ include_str!("contexts/browser/appmenu.ftl"),
+ include_str!("contexts/browser/browser.ftl"),
+ include_str!("contexts/browser/menubar.ftl"),
+ include_str!("contexts/preferences/preferences.ftl"),
+ ];
+ for source in files {
+ parse_runtime(*source).expect("Parsing of the FTL failed.");
+ }
+}
+
+iai::main!(iai_parse_ctx_runtime);
diff --git a/third_party/rust/fluent-syntax/src/ast/helper.rs b/third_party/rust/fluent-syntax/src/ast/helper.rs
new file mode 100644
index 0000000000..923437d23b
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/ast/helper.rs
@@ -0,0 +1,25 @@
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+use super::Comment;
+// This is a helper struct used to properly deserialize referential
+// JSON comments which are single continous String, into a vec of
+// content slices.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(untagged))]
+pub enum CommentDef<S> {
+ Single { content: S },
+ Multi { content: Vec<S> },
+}
+
+impl<'s, S> From<CommentDef<S>> for Comment<S> {
+ fn from(input: CommentDef<S>) -> Self {
+ match input {
+ CommentDef::Single { content } => Self {
+ content: vec![content],
+ },
+ CommentDef::Multi { content } => Self { content },
+ }
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/ast/mod.rs b/third_party/rust/fluent-syntax/src/ast/mod.rs
new file mode 100644
index 0000000000..5b79bb3e02
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/ast/mod.rs
@@ -0,0 +1,1446 @@
+//! Abstract Syntax Tree representation of the Fluent Translation List.
+//!
+//! The AST of Fluent contains all nodes structures to represent a complete
+//! representation of the FTL resource.
+//!
+//! The tree preserves all semantic information and allow for round-trip
+//! of a canonically written FTL resource.
+//!
+//! The root node is called [`Resource`] and contains a list of [`Entry`] nodes
+//! representing all possible entries in the Fluent Translation List.
+//!
+//! # Example
+//!
+//! ```
+//! use fluent_syntax::parser;
+//! use fluent_syntax::ast;
+//!
+//! let ftl = r#"
+//!
+//! ## This is a message comment
+//! hello-world = Hello World!
+//! .tooltip = Tooltip for you, { $userName }.
+//!
+//! "#;
+//!
+//! let resource = parser::parse(ftl)
+//! .expect("Failed to parse an FTL resource.");
+//!
+//! assert_eq!(
+//! resource.body[0],
+//! ast::Entry::Message(
+//! ast::Message {
+//! id: ast::Identifier {
+//! name: "hello-world"
+//! },
+//! value: Some(ast::Pattern {
+//! elements: vec![
+//! ast::PatternElement::TextElement {
+//! value: "Hello World!"
+//! },
+//! ]
+//! }),
+//! attributes: vec![
+//! ast::Attribute {
+//! id: ast::Identifier {
+//! name: "tooltip"
+//! },
+//! value: ast::Pattern {
+//! elements: vec![
+//! ast::PatternElement::TextElement {
+//! value: "Tooltip for you, "
+//! },
+//! ast::PatternElement::Placeable {
+//! expression: ast::Expression::Inline(
+//! ast::InlineExpression::VariableReference {
+//! id: ast::Identifier {
+//! name: "userName"
+//! }
+//! }
+//! )
+//! },
+//! ast::PatternElement::TextElement {
+//! value: "."
+//! },
+//! ]
+//! }
+//! }
+//! ],
+//! comment: Some(
+//! ast::Comment {
+//! content: vec!["This is a message comment"]
+//! }
+//! )
+//! }
+//! ),
+//! );
+//! ```
+//!
+//! ## Errors
+//!
+//! Fluent AST preserves blocks containing invaid syntax as [`Entry::Junk`].
+//!
+//! ## White space
+//!
+//! At the moment, AST does not preserve white space. In result only a
+//! canonical form of the AST is suitable for a round-trip.
+mod helper;
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+/// Root node of a Fluent Translation List.
+///
+/// A [`Resource`] contains a body with a list of [`Entry`] nodes.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = "";
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Resource<S> {
+ pub body: Vec<Entry<S>>,
+}
+
+/// A top-level node representing an entry of a [`Resource`].
+///
+/// Every [`Entry`] is a standalone element and the parser is capable
+/// of recovering from errors by identifying a beginning of a next entry.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// key = Value
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "key"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value"
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }
+/// )
+/// ]
+/// }
+/// );
+/// ```
+///
+/// # Junk Entry
+///
+/// If FTL source contains invalid FTL content, it will be preserved
+/// in form of [`Entry::Junk`] nodes.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// g@rb@ge En!ry
+///
+/// "#;
+///
+/// let (resource, _) = parser::parse(ftl)
+/// .expect_err("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Junk {
+/// content: "g@rb@ge En!ry\n\n"
+/// }
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(tag = "type"))]
+pub enum Entry<S> {
+ Message(Message<S>),
+ Term(Term<S>),
+ Comment(Comment<S>),
+ GroupComment(Comment<S>),
+ ResourceComment(Comment<S>),
+ Junk { content: S },
+}
+
+/// Message node represents the most common [`Entry`] in an FTL [`Resource`].
+///
+/// A message is a localization unit with a [`Identifier`] unique within a given
+/// [`Resource`], and a value or attributes with associated [`Pattern`].
+///
+/// A message can contain a simple text value, or a compound combination of value
+/// and attributes which together can be used to localize a complex User Interface
+/// element.
+///
+/// Finally, each [`Message`] may have an associated [`Comment`].
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// hello-world = Hello, World!
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "hello-world"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Hello, World!"
+/// }
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// })
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Message<S> {
+ pub id: Identifier<S>,
+ pub value: Option<Pattern<S>>,
+ pub attributes: Vec<Attribute<S>>,
+ pub comment: Option<Comment<S>>,
+}
+
+/// A Fluent [`Term`].
+///
+/// Terms are semantically similar to [`Message`] nodes, but
+/// they represent a separate concept in Fluent system.
+///
+/// Every term has to have a value, and the parser will
+/// report errors when term references are used in wrong positions.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// -brand-name = Nightly
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Term(ast::Term {
+/// id: ast::Identifier {
+/// name: "brand-name"
+/// },
+/// value: ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Nightly"
+/// }
+/// ]
+/// },
+/// attributes: vec![],
+/// comment: None,
+/// })
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Term<S> {
+ pub id: Identifier<S>,
+ pub value: Pattern<S>,
+ pub attributes: Vec<Attribute<S>>,
+ pub comment: Option<Comment<S>>,
+}
+
+/// Pattern contains a value of a [`Message`], [`Term`] or an [`Attribute`].
+///
+/// Each pattern is a list of [`PatternElement`] nodes representing
+/// either a simple textual value, or a combination of text literals
+/// and placeholder [`Expression`] nodes.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// hello-world = Hello, World!
+///
+/// welcome = Welcome, { $userName }.
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "hello-world"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Hello, World!"
+/// }
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }),
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "welcome"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Welcome, "
+/// },
+/// ast::PatternElement::Placeable {
+/// expression: ast::Expression::Inline(
+/// ast::InlineExpression::VariableReference {
+/// id: ast::Identifier {
+/// name: "userName"
+/// }
+/// }
+/// )
+/// },
+/// ast::PatternElement::TextElement {
+/// value: "."
+/// }
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }),
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Pattern<S> {
+ pub elements: Vec<PatternElement<S>>,
+}
+
+/// PatternElement is an element of a [`Pattern`].
+///
+/// Each [`PatternElement`] node represents
+/// either a simple textual value, or a combination of text literals
+/// and placeholder [`Expression`] nodes.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// hello-world = Hello, World!
+///
+/// welcome = Welcome, { $userName }.
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "hello-world"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Hello, World!"
+/// }
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }),
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "welcome"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Welcome, "
+/// },
+/// ast::PatternElement::Placeable {
+/// expression: ast::Expression::Inline(
+/// ast::InlineExpression::VariableReference {
+/// id: ast::Identifier {
+/// name: "userName"
+/// }
+/// }
+/// )
+/// },
+/// ast::PatternElement::TextElement {
+/// value: "."
+/// }
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }),
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(tag = "type"))]
+pub enum PatternElement<S> {
+ TextElement { value: S },
+ Placeable { expression: Expression<S> },
+}
+
+/// Attribute represents a part of a [`Message`] or [`Term`].
+///
+/// Attributes are used to express a compound list of keyed
+/// [`Pattern`] elements on an entry.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// hello-world =
+/// .title = This is a title
+/// .accesskey = T
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "hello-world"
+/// },
+/// value: None,
+/// attributes: vec![
+/// ast::Attribute {
+/// id: ast::Identifier {
+/// name: "title"
+/// },
+/// value: ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "This is a title"
+/// },
+/// ]
+/// }
+/// },
+/// ast::Attribute {
+/// id: ast::Identifier {
+/// name: "accesskey"
+/// },
+/// value: ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "T"
+/// },
+/// ]
+/// }
+/// }
+/// ],
+/// comment: None,
+/// }),
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Attribute<S> {
+ pub id: Identifier<S>,
+ pub value: Pattern<S>,
+}
+
+/// Identifier is part of nodes such as [`Message`], [`Term`] and [`Attribute`].
+///
+/// It is used to associate a unique key with an [`Entry`] or an [`Attribute`]
+/// and in [`Expression`] nodes to refer to another entry.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// hello-world = Value
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "hello-world"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value"
+/// }
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }),
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Identifier<S> {
+ pub name: S,
+}
+
+/// Variant is a single branch of a value in a [`Select`](Expression::Select) expression.
+///
+/// It's a pair of [`VariantKey`] and [`Pattern`]. If the selector match the
+/// key, then the value of the variant is returned as the value of the expression.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// hello-world = { $var ->
+/// [key1] Value 1
+/// *[other] Value 2
+/// }
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "hello-world"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::Placeable {
+/// expression: ast::Expression::Select {
+/// selector: ast::InlineExpression::VariableReference {
+/// id: ast::Identifier { name: "var" },
+/// },
+/// variants: vec![
+/// ast::Variant {
+/// key: ast::VariantKey::Identifier {
+/// name: "key1"
+/// },
+/// value: ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 1",
+/// }
+/// ]
+/// },
+/// default: false,
+/// },
+/// ast::Variant {
+/// key: ast::VariantKey::Identifier {
+/// name: "other"
+/// },
+/// value: ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 2",
+/// }
+/// ]
+/// },
+/// default: true,
+/// },
+/// ]
+/// }
+/// }
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }),
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(tag = "type"))]
+pub struct Variant<S> {
+ pub key: VariantKey<S>,
+ pub value: Pattern<S>,
+ pub default: bool,
+}
+
+/// A key of a [`Variant`].
+///
+/// Variant key can be either an identifier or a number.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// hello-world = { $var ->
+/// [0] Value 1
+/// *[other] Value 2
+/// }
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "hello-world"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::Placeable {
+/// expression: ast::Expression::Select {
+/// selector: ast::InlineExpression::VariableReference {
+/// id: ast::Identifier { name: "var" },
+/// },
+/// variants: vec![
+/// ast::Variant {
+/// key: ast::VariantKey::NumberLiteral {
+/// value: "0"
+/// },
+/// value: ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 1",
+/// }
+/// ]
+/// },
+/// default: false,
+/// },
+/// ast::Variant {
+/// key: ast::VariantKey::Identifier {
+/// name: "other"
+/// },
+/// value: ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 2",
+/// }
+/// ]
+/// },
+/// default: true,
+/// },
+/// ]
+/// }
+/// }
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }),
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(tag = "type"))]
+pub enum VariantKey<S> {
+ Identifier { name: S },
+ NumberLiteral { value: S },
+}
+
+/// Fluent [`Comment`].
+///
+/// In Fluent, comments may be standalone, or associated with
+/// an entry such as [`Term`] or [`Message`].
+///
+/// When used as a standalone [`Entry`], comments may appear in one of
+/// three levels:
+///
+/// * Standalone comment
+/// * Group comment associated with a group of messages
+/// * Resource comment associated with the whole resource
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+/// ## A standalone level comment
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Comment(ast::Comment {
+/// content: vec![
+/// "A standalone level comment"
+/// ]
+/// })
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(from = "helper::CommentDef<S>"))]
+pub struct Comment<S> {
+ pub content: Vec<S>,
+}
+
+/// List of arguments for a [`FunctionReference`](InlineExpression::FunctionReference) or a
+/// [`TermReference`](InlineExpression::TermReference).
+///
+/// Function and Term reference may contain a list of positional and
+/// named arguments passed to them.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// key = { FUNC($var1, "literal", style: "long") }
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "key"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::Placeable {
+/// expression: ast::Expression::Inline(
+/// ast::InlineExpression::FunctionReference {
+/// id: ast::Identifier {
+/// name: "FUNC"
+/// },
+/// arguments: ast::CallArguments {
+/// positional: vec![
+/// ast::InlineExpression::VariableReference {
+/// id: ast::Identifier {
+/// name: "var1"
+/// }
+/// },
+/// ast::InlineExpression::StringLiteral {
+/// value: "literal",
+/// }
+/// ],
+/// named: vec![
+/// ast::NamedArgument {
+/// name: ast::Identifier {
+/// name: "style"
+/// },
+/// value: ast::InlineExpression::StringLiteral
+/// {
+/// value: "long"
+/// }
+/// }
+/// ],
+/// }
+/// }
+/// )
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }
+/// )
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone, Default)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(tag = "type"))]
+pub struct CallArguments<S> {
+ pub positional: Vec<InlineExpression<S>>,
+ pub named: Vec<NamedArgument<S>>,
+}
+
+/// A key-value pair used in [`CallArguments`].
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// key = { FUNC(style: "long") }
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "key"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::Placeable {
+/// expression: ast::Expression::Inline(
+/// ast::InlineExpression::FunctionReference {
+/// id: ast::Identifier {
+/// name: "FUNC"
+/// },
+/// arguments: ast::CallArguments {
+/// positional: vec![],
+/// named: vec![
+/// ast::NamedArgument {
+/// name: ast::Identifier {
+/// name: "style"
+/// },
+/// value: ast::InlineExpression::StringLiteral
+/// {
+/// value: "long"
+/// }
+/// }
+/// ],
+/// }
+/// }
+/// )
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }
+/// )
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(tag = "type"))]
+pub struct NamedArgument<S> {
+ pub name: Identifier<S>,
+ pub value: InlineExpression<S>,
+}
+
+/// A subset of expressions which can be used as [`Placeable`](PatternElement::Placeable),
+/// [`selector`](Expression::Select), or in [`CallArguments`].
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// key = { $emailCount }
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "key"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::Placeable {
+/// expression: ast::Expression::Inline(
+/// ast::InlineExpression::VariableReference {
+/// id: ast::Identifier {
+/// name: "emailCount"
+/// },
+/// }
+/// )
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }
+/// )
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(tag = "type"))]
+pub enum InlineExpression<S> {
+ /// Single line string literal enclosed in `"`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_syntax::parser;
+ /// use fluent_syntax::ast;
+ ///
+ /// let ftl = r#"
+ ///
+ /// key = { "this is a literal" }
+ ///
+ /// "#;
+ ///
+ /// let resource = parser::parse(ftl)
+ /// .expect("Failed to parse an FTL resource.");
+ ///
+ /// assert_eq!(
+ /// resource,
+ /// ast::Resource {
+ /// body: vec![
+ /// ast::Entry::Message(
+ /// ast::Message {
+ /// id: ast::Identifier {
+ /// name: "key"
+ /// },
+ /// value: Some(ast::Pattern {
+ /// elements: vec![
+ /// ast::PatternElement::Placeable {
+ /// expression: ast::Expression::Inline(
+ /// ast::InlineExpression::StringLiteral {
+ /// value: "this is a literal",
+ /// }
+ /// )
+ /// },
+ /// ]
+ /// }),
+ /// attributes: vec![],
+ /// comment: None,
+ /// }
+ /// )
+ /// ]
+ /// }
+ /// );
+ /// ```
+ StringLiteral { value: S },
+ /// A number literal.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_syntax::parser;
+ /// use fluent_syntax::ast;
+ ///
+ /// let ftl = r#"
+ ///
+ /// key = { -0.5 }
+ ///
+ /// "#;
+ ///
+ /// let resource = parser::parse(ftl)
+ /// .expect("Failed to parse an FTL resource.");
+ ///
+ /// assert_eq!(
+ /// resource,
+ /// ast::Resource {
+ /// body: vec![
+ /// ast::Entry::Message(
+ /// ast::Message {
+ /// id: ast::Identifier {
+ /// name: "key"
+ /// },
+ /// value: Some(ast::Pattern {
+ /// elements: vec![
+ /// ast::PatternElement::Placeable {
+ /// expression: ast::Expression::Inline(
+ /// ast::InlineExpression::NumberLiteral {
+ /// value: "-0.5",
+ /// }
+ /// )
+ /// },
+ /// ]
+ /// }),
+ /// attributes: vec![],
+ /// comment: None,
+ /// }
+ /// )
+ /// ]
+ /// }
+ /// );
+ /// ```
+ NumberLiteral { value: S },
+ /// A function reference.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_syntax::parser;
+ /// use fluent_syntax::ast;
+ ///
+ /// let ftl = r#"
+ ///
+ /// key = { FUNC() }
+ ///
+ /// "#;
+ ///
+ /// let resource = parser::parse(ftl)
+ /// .expect("Failed to parse an FTL resource.");
+ ///
+ /// assert_eq!(
+ /// resource,
+ /// ast::Resource {
+ /// body: vec![
+ /// ast::Entry::Message(
+ /// ast::Message {
+ /// id: ast::Identifier {
+ /// name: "key"
+ /// },
+ /// value: Some(ast::Pattern {
+ /// elements: vec![
+ /// ast::PatternElement::Placeable {
+ /// expression: ast::Expression::Inline(
+ /// ast::InlineExpression::FunctionReference {
+ /// id: ast::Identifier {
+ /// name: "FUNC"
+ /// },
+ /// arguments: ast::CallArguments::default(),
+ /// }
+ /// )
+ /// },
+ /// ]
+ /// }),
+ /// attributes: vec![],
+ /// comment: None,
+ /// }
+ /// )
+ /// ]
+ /// }
+ /// );
+ /// ```
+ FunctionReference {
+ id: Identifier<S>,
+ arguments: CallArguments<S>,
+ },
+ /// A reference to another message.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_syntax::parser;
+ /// use fluent_syntax::ast;
+ ///
+ /// let ftl = r#"
+ ///
+ /// key = { key2 }
+ ///
+ /// "#;
+ ///
+ /// let resource = parser::parse(ftl)
+ /// .expect("Failed to parse an FTL resource.");
+ ///
+ /// assert_eq!(
+ /// resource,
+ /// ast::Resource {
+ /// body: vec![
+ /// ast::Entry::Message(
+ /// ast::Message {
+ /// id: ast::Identifier {
+ /// name: "key"
+ /// },
+ /// value: Some(ast::Pattern {
+ /// elements: vec![
+ /// ast::PatternElement::Placeable {
+ /// expression: ast::Expression::Inline(
+ /// ast::InlineExpression::MessageReference {
+ /// id: ast::Identifier {
+ /// name: "key2"
+ /// },
+ /// attribute: None,
+ /// }
+ /// )
+ /// },
+ /// ]
+ /// }),
+ /// attributes: vec![],
+ /// comment: None,
+ /// }
+ /// )
+ /// ]
+ /// }
+ /// );
+ /// ```
+ MessageReference {
+ id: Identifier<S>,
+ attribute: Option<Identifier<S>>,
+ },
+ /// A reference to a term.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_syntax::parser;
+ /// use fluent_syntax::ast;
+ ///
+ /// let ftl = r#"
+ ///
+ /// key = { -brand-name }
+ ///
+ /// "#;
+ ///
+ /// let resource = parser::parse(ftl)
+ /// .expect("Failed to parse an FTL resource.");
+ ///
+ /// assert_eq!(
+ /// resource,
+ /// ast::Resource {
+ /// body: vec![
+ /// ast::Entry::Message(
+ /// ast::Message {
+ /// id: ast::Identifier {
+ /// name: "key"
+ /// },
+ /// value: Some(ast::Pattern {
+ /// elements: vec![
+ /// ast::PatternElement::Placeable {
+ /// expression: ast::Expression::Inline(
+ /// ast::InlineExpression::TermReference {
+ /// id: ast::Identifier {
+ /// name: "brand-name"
+ /// },
+ /// attribute: None,
+ /// arguments: None,
+ /// }
+ /// )
+ /// },
+ /// ]
+ /// }),
+ /// attributes: vec![],
+ /// comment: None,
+ /// }
+ /// )
+ /// ]
+ /// }
+ /// );
+ /// ```
+ TermReference {
+ id: Identifier<S>,
+ attribute: Option<Identifier<S>>,
+ arguments: Option<CallArguments<S>>,
+ },
+ /// A reference to a variable.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_syntax::parser;
+ /// use fluent_syntax::ast;
+ ///
+ /// let ftl = r#"
+ ///
+ /// key = { $var1 }
+ ///
+ /// "#;
+ ///
+ /// let resource = parser::parse(ftl)
+ /// .expect("Failed to parse an FTL resource.");
+ ///
+ /// assert_eq!(
+ /// resource,
+ /// ast::Resource {
+ /// body: vec![
+ /// ast::Entry::Message(
+ /// ast::Message {
+ /// id: ast::Identifier {
+ /// name: "key"
+ /// },
+ /// value: Some(ast::Pattern {
+ /// elements: vec![
+ /// ast::PatternElement::Placeable {
+ /// expression: ast::Expression::Inline(
+ /// ast::InlineExpression::VariableReference {
+ /// id: ast::Identifier {
+ /// name: "var1"
+ /// },
+ /// }
+ /// )
+ /// },
+ /// ]
+ /// }),
+ /// attributes: vec![],
+ /// comment: None,
+ /// }
+ /// )
+ /// ]
+ /// }
+ /// );
+ /// ```
+ VariableReference { id: Identifier<S> },
+ /// A placeable which may contain another expression.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use fluent_syntax::parser;
+ /// use fluent_syntax::ast;
+ ///
+ /// let ftl = r#"
+ ///
+ /// key = { { "placeable" } }
+ ///
+ /// "#;
+ ///
+ /// let resource = parser::parse(ftl)
+ /// .expect("Failed to parse an FTL resource.");
+ ///
+ /// assert_eq!(
+ /// resource,
+ /// ast::Resource {
+ /// body: vec![
+ /// ast::Entry::Message(
+ /// ast::Message {
+ /// id: ast::Identifier {
+ /// name: "key"
+ /// },
+ /// value: Some(ast::Pattern {
+ /// elements: vec![
+ /// ast::PatternElement::Placeable {
+ /// expression: ast::Expression::Inline(
+ /// ast::InlineExpression::Placeable {
+ /// expression: Box::new(
+ /// ast::Expression::Inline(
+ /// ast::InlineExpression::StringLiteral {
+ /// value: "placeable"
+ /// }
+ /// )
+ /// )
+ /// }
+ /// )
+ /// },
+ /// ]
+ /// }),
+ /// attributes: vec![],
+ /// comment: None,
+ /// }
+ /// )
+ /// ]
+ /// }
+ /// );
+ /// ```
+ Placeable { expression: Box<Expression<S>> },
+}
+
+/// An expression that is either a select expression or an inline expression.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+///
+/// key = { $var ->
+/// [key1] Value 1
+/// *[other] Value 2
+/// }
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource,
+/// ast::Resource {
+/// body: vec![
+/// ast::Entry::Message(ast::Message {
+/// id: ast::Identifier {
+/// name: "key"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::Placeable {
+/// expression: ast::Expression::Select {
+/// selector: ast::InlineExpression::VariableReference {
+/// id: ast::Identifier { name: "var" },
+/// },
+/// variants: vec![
+/// ast::Variant {
+/// key: ast::VariantKey::Identifier {
+/// name: "key1"
+/// },
+/// value: ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 1",
+/// }
+/// ]
+/// },
+/// default: false,
+/// },
+/// ast::Variant {
+/// key: ast::VariantKey::Identifier {
+/// name: "other"
+/// },
+/// value: ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 2",
+/// }
+/// ]
+/// },
+/// default: true,
+/// },
+/// ]
+/// }
+/// }
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }),
+/// ]
+/// }
+/// );
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(untagged))]
+pub enum Expression<S> {
+ Select {
+ selector: InlineExpression<S>,
+ variants: Vec<Variant<S>>,
+ },
+ Inline(InlineExpression<S>),
+}
diff --git a/third_party/rust/fluent-syntax/src/bin/parser.rs b/third_party/rust/fluent-syntax/src/bin/parser.rs
new file mode 100644
index 0000000000..46275a7290
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/bin/parser.rs
@@ -0,0 +1,42 @@
+use fluent_syntax::parser::parse;
+use std::env;
+use std::fs::File;
+use std::io;
+use std::io::Read;
+
+fn read_file(path: &str) -> Result<String, io::Error> {
+ let mut f = File::open(path)?;
+ let mut s = String::new();
+ f.read_to_string(&mut s)?;
+ Ok(s)
+}
+
+fn main() {
+ let args: Vec<String> = env::args().collect();
+ let source = read_file(args.get(1).expect("Pass an argument")).expect("Failed to fetch file");
+
+ let (ast, errors) = match parse(source.as_str()) {
+ Ok(ast) => (ast, None),
+ Err((ast, err)) => (ast, Some(err)),
+ };
+
+ #[cfg(feature = "json")]
+ {
+ let target_json = serde_json::to_string_pretty(&ast).unwrap();
+ println!("{}", target_json);
+ }
+ #[cfg(not(feature = "json"))]
+ {
+ use std::fmt::Write;
+ let mut result = String::new();
+ write!(result, "{:#?}", ast).unwrap();
+ println!("{}", result);
+ }
+
+ if let Some(errors) = errors {
+ println!("\n======== Errors ========== \n");
+ for err in errors {
+ println!("Err: {:#?}", err);
+ }
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/bin/update_fixtures.rs b/third_party/rust/fluent-syntax/src/bin/update_fixtures.rs
new file mode 100644
index 0000000000..01e7a02af0
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/bin/update_fixtures.rs
@@ -0,0 +1,44 @@
+use std::fs;
+use std::io;
+
+use fluent_syntax::parser::parse;
+
+fn read_file(path: &str) -> Result<String, io::Error> {
+ fs::read_to_string(path)
+}
+
+fn write_file(path: &str, source: &str) -> Result<(), io::Error> {
+ fs::write(path, source)
+}
+
+fn main() {
+ let samples = &["menubar", "preferences", "simple"];
+ let contexts = &["browser", "preferences"];
+
+ for sample in samples {
+ let path = format!("./benches/{}.ftl", sample);
+ let source = read_file(&path).unwrap();
+ let ast = parse(source).unwrap();
+ let target_json = serde_json::to_string_pretty(&ast).unwrap();
+ let new_path = format!("./tests/fixtures/benches/{}.json", sample);
+ write_file(&new_path, &target_json).unwrap();
+ }
+
+ for test in contexts {
+ let paths = fs::read_dir(format!("./benches/contexts/{}", test)).unwrap();
+ for path in paths.into_iter() {
+ let p = path.unwrap().path();
+ let file_name = p.file_name().unwrap().to_str().unwrap();
+ let path = p.to_str().unwrap();
+ let source = read_file(path).unwrap();
+ let ast = parse(source).unwrap();
+ let target_json = serde_json::to_string_pretty(&ast).unwrap();
+ let new_path = format!(
+ "./tests/fixtures/benches/contexts/{}/{}",
+ test,
+ file_name.replace(".ftl", ".json")
+ );
+ write_file(&new_path, &target_json).unwrap();
+ }
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/lib.rs b/third_party/rust/fluent-syntax/src/lib.rs
new file mode 100644
index 0000000000..5b9cbbfe7f
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/lib.rs
@@ -0,0 +1,51 @@
+//! Fluent is a modern localization system designed to improve how software is translated.
+//!
+//! `fluent-syntax` is the lowest level component of the [Fluent Localization
+//! System](https://www.projectfluent.org).
+//!
+//! It exposes components necessary for parsing and tooling operations on Fluent Translation Lists ("FTL").
+//!
+//! The crate provides a [`parser`] module which allows for parsing of an
+//! input string to an Abstract Syntax Tree defined in the [`ast`] module.
+//!
+//! The [`unicode`] module exposes a set of helper functions used to decode
+//! escaped unicode literals according to Fluent specification.
+//!
+//! # Example
+//!
+//! ```
+//! use fluent_syntax::parser;
+//! use fluent_syntax::ast;
+//!
+//! let ftl = r#"
+//!
+//! hello-world = Hello World!
+//!
+//! "#;
+//!
+//! let resource = parser::parse(ftl)
+//! .expect("Failed to parse an FTL resource.");
+//!
+//! assert_eq!(
+//! resource.body[0],
+//! ast::Entry::Message(
+//! ast::Message {
+//! id: ast::Identifier {
+//! name: "hello-world"
+//! },
+//! value: Some(ast::Pattern {
+//! elements: vec![
+//! ast::PatternElement::TextElement {
+//! value: "Hello World!"
+//! },
+//! ]
+//! }),
+//! attributes: vec![],
+//! comment: None,
+//! }
+//! ),
+//! );
+//! ```
+pub mod ast;
+pub mod parser;
+pub mod unicode;
diff --git a/third_party/rust/fluent-syntax/src/parser/comment.rs b/third_party/rust/fluent-syntax/src/parser/comment.rs
new file mode 100644
index 0000000000..a63483c1d3
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/comment.rs
@@ -0,0 +1,89 @@
+use super::{core::Parser, core::Result, Slice};
+use crate::ast;
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub(super) enum Level {
+ None = 0,
+ Regular = 1,
+ Group = 2,
+ Resource = 3,
+}
+
+impl<'s, S> Parser<S>
+where
+ S: Slice<'s>,
+{
+ pub(super) fn get_comment(&mut self) -> Result<(ast::Comment<S>, Level)> {
+ let mut level = Level::None;
+ let mut content = vec![];
+
+ while self.ptr < self.length {
+ let line_level = self.get_comment_level();
+ if line_level == Level::None {
+ self.ptr -= 1;
+ break;
+ } else if level != Level::None && line_level != level {
+ self.ptr -= line_level as usize;
+ break;
+ }
+
+ level = line_level;
+
+ if self.ptr == self.length {
+ break;
+ } else if self.is_current_byte(b'\n') {
+ content.push(self.get_comment_line());
+ } else {
+ if let Err(e) = self.expect_byte(b' ') {
+ if content.is_empty() {
+ return Err(e);
+ } else {
+ self.ptr -= line_level as usize;
+ break;
+ }
+ }
+ content.push(self.get_comment_line());
+ }
+ self.skip_eol();
+ }
+
+ Ok((ast::Comment { content }, level))
+ }
+
+ pub(super) fn skip_comment(&mut self) {
+ loop {
+ while self.ptr < self.length && !self.is_current_byte(b'\n') {
+ self.ptr += 1;
+ }
+ self.ptr += 1;
+ if self.is_current_byte(b'#') {
+ self.ptr += 1;
+ } else {
+ break;
+ }
+ }
+ }
+
+ fn get_comment_level(&mut self) -> Level {
+ if self.take_byte_if(b'#') {
+ if self.take_byte_if(b'#') {
+ if self.take_byte_if(b'#') {
+ return Level::Resource;
+ }
+ return Level::Group;
+ }
+ return Level::Regular;
+ }
+ Level::None
+ }
+
+ fn get_comment_line(&mut self) -> S {
+ let start_pos = self.ptr;
+
+ while !self.is_eol() {
+ self.ptr += 1;
+ }
+
+ self.source.slice(start_pos..self.ptr)
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/parser/core.rs b/third_party/rust/fluent-syntax/src/parser/core.rs
new file mode 100644
index 0000000000..68ad8dc0b6
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/core.rs
@@ -0,0 +1,307 @@
+use super::{
+ comment,
+ errors::{ErrorKind, ParserError},
+ slice::Slice,
+};
+use crate::ast;
+
+pub type Result<T> = std::result::Result<T, ParserError>;
+
+pub struct Parser<S> {
+ pub(super) source: S,
+ pub(super) ptr: usize,
+ pub(super) length: usize,
+}
+
+impl<'s, S> Parser<S>
+where
+ S: Slice<'s>,
+{
+ pub fn new(source: S) -> Self {
+ let length = source.as_ref().as_bytes().len();
+ Self {
+ source,
+ ptr: 0,
+ length,
+ }
+ }
+
+ pub fn parse(
+ mut self,
+ ) -> std::result::Result<ast::Resource<S>, (ast::Resource<S>, Vec<ParserError>)> {
+ let mut errors = vec![];
+
+ let mut body = vec![];
+
+ self.skip_blank_block();
+ let mut last_comment = None;
+ let mut last_blank_count = 0;
+
+ while self.ptr < self.length {
+ let entry_start = self.ptr;
+ let mut entry = self.get_entry(entry_start);
+
+ if let Some(comment) = last_comment.take() {
+ match entry {
+ Ok(ast::Entry::Message(ref mut msg)) if last_blank_count < 2 => {
+ msg.comment = Some(comment);
+ }
+ Ok(ast::Entry::Term(ref mut term)) if last_blank_count < 2 => {
+ term.comment = Some(comment);
+ }
+ _ => {
+ body.push(ast::Entry::Comment(comment));
+ }
+ }
+ }
+
+ match entry {
+ Ok(ast::Entry::Comment(comment)) => {
+ last_comment = Some(comment);
+ }
+ Ok(entry) => {
+ body.push(entry);
+ }
+ Err(mut err) => {
+ self.skip_to_next_entry_start();
+ err.slice = Some(entry_start..self.ptr);
+ errors.push(err);
+ let content = self.source.slice(entry_start..self.ptr);
+ body.push(ast::Entry::Junk { content });
+ }
+ }
+ last_blank_count = self.skip_blank_block();
+ }
+
+ if let Some(last_comment) = last_comment.take() {
+ body.push(ast::Entry::Comment(last_comment));
+ }
+ if errors.is_empty() {
+ Ok(ast::Resource { body })
+ } else {
+ Err((ast::Resource { body }, errors))
+ }
+ }
+
+ fn get_entry(&mut self, entry_start: usize) -> Result<ast::Entry<S>> {
+ let entry = match get_current_byte!(self) {
+ Some(b'#') => {
+ let (comment, level) = self.get_comment()?;
+ match level {
+ comment::Level::Regular => ast::Entry::Comment(comment),
+ comment::Level::Group => ast::Entry::GroupComment(comment),
+ comment::Level::Resource => ast::Entry::ResourceComment(comment),
+ comment::Level::None => unreachable!(),
+ }
+ }
+ Some(b'-') => ast::Entry::Term(self.get_term(entry_start)?),
+ _ => ast::Entry::Message(self.get_message(entry_start)?),
+ };
+ Ok(entry)
+ }
+
+ pub fn get_message(&mut self, entry_start: usize) -> Result<ast::Message<S>> {
+ let id = self.get_identifier()?;
+ self.skip_blank_inline();
+ self.expect_byte(b'=')?;
+ let pattern = self.get_pattern()?;
+
+ self.skip_blank_block();
+
+ let attributes = self.get_attributes();
+
+ if pattern.is_none() && attributes.is_empty() {
+ let entry_id = id.name.as_ref().to_owned();
+ return error!(
+ ErrorKind::ExpectedMessageField { entry_id },
+ entry_start, self.ptr
+ );
+ }
+
+ Ok(ast::Message {
+ id,
+ value: pattern,
+ attributes,
+ comment: None,
+ })
+ }
+
+ pub fn get_term(&mut self, entry_start: usize) -> Result<ast::Term<S>> {
+ self.expect_byte(b'-')?;
+ let id = self.get_identifier()?;
+ self.skip_blank_inline();
+ self.expect_byte(b'=')?;
+ self.skip_blank_inline();
+
+ let value = self.get_pattern()?;
+
+ self.skip_blank_block();
+
+ let attributes = self.get_attributes();
+
+ if let Some(value) = value {
+ Ok(ast::Term {
+ id,
+ value,
+ attributes,
+ comment: None,
+ })
+ } else {
+ error!(
+ ErrorKind::ExpectedTermField {
+ entry_id: id.name.as_ref().to_owned()
+ },
+ entry_start, self.ptr
+ )
+ }
+ }
+
+ fn get_attributes(&mut self) -> Vec<ast::Attribute<S>> {
+ let mut attributes = vec![];
+
+ loop {
+ let line_start = self.ptr;
+ self.skip_blank_inline();
+ if !self.take_byte_if(b'.') {
+ self.ptr = line_start;
+ break;
+ }
+
+ if let Ok(attr) = self.get_attribute() {
+ attributes.push(attr);
+ } else {
+ self.ptr = line_start;
+ break;
+ }
+ }
+ attributes
+ }
+
+ fn get_attribute(&mut self) -> Result<ast::Attribute<S>> {
+ let id = self.get_identifier()?;
+ self.skip_blank_inline();
+ self.expect_byte(b'=')?;
+ let pattern = self.get_pattern()?;
+
+ match pattern {
+ Some(pattern) => Ok(ast::Attribute { id, value: pattern }),
+ None => error!(ErrorKind::MissingValue, self.ptr),
+ }
+ }
+
+ pub(super) fn get_identifier_unchecked(&mut self) -> ast::Identifier<S> {
+ let mut ptr = self.ptr;
+
+ while matches!(get_byte!(self, ptr), Some(b) if b.is_ascii_alphanumeric() || *b == b'-' || *b == b'_')
+ {
+ ptr += 1;
+ }
+
+ let name = self.source.slice(self.ptr - 1..ptr);
+ self.ptr = ptr;
+
+ ast::Identifier { name }
+ }
+
+ pub(super) fn get_identifier(&mut self) -> Result<ast::Identifier<S>> {
+ if !self.is_identifier_start() {
+ return error!(
+ ErrorKind::ExpectedCharRange {
+ range: "a-zA-Z".to_string()
+ },
+ self.ptr
+ );
+ }
+ self.ptr += 1;
+ Ok(self.get_identifier_unchecked())
+ }
+
+ pub(super) fn get_attribute_accessor(&mut self) -> Result<Option<ast::Identifier<S>>> {
+ if self.take_byte_if(b'.') {
+ let ident = self.get_identifier()?;
+ Ok(Some(ident))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn get_variant_key(&mut self) -> Result<ast::VariantKey<S>> {
+ self.skip_blank();
+
+ let key = if self.is_number_start() {
+ ast::VariantKey::NumberLiteral {
+ value: self.get_number_literal()?,
+ }
+ } else {
+ ast::VariantKey::Identifier {
+ name: self.get_identifier()?.name,
+ }
+ };
+
+ self.skip_blank();
+
+ self.expect_byte(b']')?;
+
+ Ok(key)
+ }
+
+ pub(super) fn get_variants(&mut self) -> Result<Vec<ast::Variant<S>>> {
+ let mut variants = Vec::with_capacity(2);
+ let mut has_default = false;
+
+ loop {
+ let default = self.take_byte_if(b'*');
+ if default {
+ if has_default {
+ return error!(ErrorKind::MultipleDefaultVariants, self.ptr);
+ } else {
+ has_default = true;
+ }
+ }
+
+ if !self.take_byte_if(b'[') {
+ break;
+ }
+
+ let key = self.get_variant_key()?;
+
+ let value = self.get_pattern()?;
+
+ if let Some(value) = value {
+ variants.push(ast::Variant {
+ key,
+ value,
+ default,
+ });
+ self.skip_blank();
+ } else {
+ return error!(ErrorKind::MissingValue, self.ptr);
+ }
+ }
+
+ if has_default {
+ Ok(variants)
+ } else {
+ error!(ErrorKind::MissingDefaultVariant, self.ptr)
+ }
+ }
+
+ pub(super) fn get_placeable(&mut self) -> Result<ast::Expression<S>> {
+ self.skip_blank();
+ let exp = self.get_expression()?;
+ self.skip_blank_inline();
+ self.expect_byte(b'}')?;
+
+ let invalid_expression_found = match &exp {
+ ast::Expression::Inline(ast::InlineExpression::TermReference {
+ ref attribute, ..
+ }) => attribute.is_some(),
+ _ => false,
+ };
+ if invalid_expression_found {
+ return error!(ErrorKind::TermAttributeAsPlaceable, self.ptr);
+ }
+
+ Ok(exp)
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/parser/errors.rs b/third_party/rust/fluent-syntax/src/parser/errors.rs
new file mode 100644
index 0000000000..2c29f97bbf
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/errors.rs
@@ -0,0 +1,169 @@
+use std::ops::Range;
+use thiserror::Error;
+
+/// Error containing information about an error encountered by the Fluent Parser.
+///
+/// Errors in Fluent Parser are non-fatal, and the syntax has been
+/// designed to allow for strong recovery.
+///
+/// In result [`ParserError`] is designed to point at the slice of
+/// the input that is most likely to be a complete fragment from after
+/// the end of a valid entry, to the start of the next valid entry, with
+/// the invalid syntax in the middle.
+///
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+/// key1 = Value 1
+///
+/// g@Rb@ge = #2y ds
+///
+/// key2 = Value 2
+///
+/// "#;
+///
+/// let (resource, errors) = parser::parse_runtime(ftl)
+/// .expect_err("Resource should contain errors.");
+///
+/// assert_eq!(
+/// errors,
+/// vec![
+/// parser::ParserError {
+/// pos: 18..19,
+/// slice: Some(17..35),
+/// kind: parser::ErrorKind::ExpectedToken('=')
+/// }
+/// ]
+/// );
+///
+/// assert_eq!(
+/// resource.body[0],
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "key1"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 1"
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }
+/// ),
+/// );
+///
+/// assert_eq!(
+/// resource.body[1],
+/// ast::Entry::Junk {
+/// content: "g@Rb@ge = #2y ds\n\n"
+/// }
+/// );
+///
+/// assert_eq!(
+/// resource.body[2],
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "key2"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 2"
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }
+/// ),
+/// );
+/// ```
+///
+/// The information contained in the `ParserError` should allow the tooling
+/// to display rich contextual annotations of the error slice, using
+/// crates such as `annotate-snippers`.
+#[derive(Error, Debug, PartialEq, Clone)]
+#[error("{}", self.kind)]
+pub struct ParserError {
+ /// Precise location of where the parser encountered the error.
+ pub pos: Range<usize>,
+ /// Slice of the input from the end of the last valid entry to the beginning
+ /// of the next valid entry with the invalid syntax in the middle.
+ pub slice: Option<Range<usize>>,
+ /// The type of the error that the parser encountered.
+ pub kind: ErrorKind,
+}
+
+macro_rules! error {
+ ($kind:expr, $start:expr) => {{
+ Err(ParserError {
+ pos: $start..$start + 1,
+ slice: None,
+ kind: $kind,
+ })
+ }};
+ ($kind:expr, $start:expr, $end:expr) => {{
+ Err(ParserError {
+ pos: $start..$end,
+ slice: None,
+ kind: $kind,
+ })
+ }};
+}
+
+/// Kind of an error associated with the [`ParserError`].
+#[derive(Error, Debug, PartialEq, Clone)]
+pub enum ErrorKind {
+ #[error("Expected a token starting with \"{0}\"")]
+ ExpectedToken(char),
+ #[error("Expected one of \"{range}\"")]
+ ExpectedCharRange { range: String },
+ #[error("Expected a message field for \"{entry_id}\"")]
+ ExpectedMessageField { entry_id: String },
+ #[error("Expected a term field for \"{entry_id}\"")]
+ ExpectedTermField { entry_id: String },
+ #[error("Callee is not allowed here")]
+ ForbiddenCallee,
+ #[error("The select expression must have a default variant")]
+ MissingDefaultVariant,
+ #[error("Expected a value")]
+ MissingValue,
+ #[error("A select expression can only have one default variant")]
+ MultipleDefaultVariants,
+ #[error("Message references can't be used as a selector")]
+ MessageReferenceAsSelector,
+ #[error("Term references can't be used as a selector")]
+ TermReferenceAsSelector,
+ #[error("Message attributes can't be used as a selector")]
+ MessageAttributeAsSelector,
+ #[error("Term attributes can't be used as a selector")]
+ TermAttributeAsPlaceable,
+ #[error("Unterminated string literal")]
+ UnterminatedStringLiteral,
+ #[error("Positional arguments must come before named arguments")]
+ PositionalArgumentFollowsNamed,
+ #[error("The \"{0}\" argument appears twice")]
+ DuplicatedNamedArgument(String),
+ #[error("Unknown escape sequence")]
+ UnknownEscapeSequence(String),
+ #[error("Invalid unicode escape sequence, \"{0}\"")]
+ InvalidUnicodeEscapeSequence(String),
+ #[error("Unbalanced closing brace")]
+ UnbalancedClosingBrace,
+ #[error("Expected an inline expression")]
+ ExpectedInlineExpression,
+ #[error("Expected a simple expression as selector")]
+ ExpectedSimpleExpressionAsSelector,
+ #[error("Expected a string or number literal")]
+ ExpectedLiteral,
+}
diff --git a/third_party/rust/fluent-syntax/src/parser/expression.rs b/third_party/rust/fluent-syntax/src/parser/expression.rs
new file mode 100644
index 0000000000..c5ccb32bf4
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/expression.rs
@@ -0,0 +1,224 @@
+use super::errors::{ErrorKind, ParserError};
+use super::{core::Parser, core::Result, slice::Slice};
+use crate::ast;
+
+impl<'s, S> Parser<S>
+where
+ S: Slice<'s>,
+{
+ pub(super) fn get_expression(&mut self) -> Result<ast::Expression<S>> {
+ let exp = self.get_inline_expression(false)?;
+
+ self.skip_blank();
+
+ if !self.is_current_byte(b'-') || !self.is_byte_at(b'>', self.ptr + 1) {
+ if let ast::InlineExpression::TermReference { ref attribute, .. } = exp {
+ if attribute.is_some() {
+ return error!(ErrorKind::TermAttributeAsPlaceable, self.ptr);
+ }
+ }
+ return Ok(ast::Expression::Inline(exp));
+ }
+
+ match exp {
+ ast::InlineExpression::MessageReference { ref attribute, .. } => {
+ if attribute.is_none() {
+ return error!(ErrorKind::MessageReferenceAsSelector, self.ptr);
+ } else {
+ return error!(ErrorKind::MessageAttributeAsSelector, self.ptr);
+ }
+ }
+ ast::InlineExpression::TermReference { ref attribute, .. } => {
+ if attribute.is_none() {
+ return error!(ErrorKind::TermReferenceAsSelector, self.ptr);
+ }
+ }
+ ast::InlineExpression::StringLiteral { .. }
+ | ast::InlineExpression::NumberLiteral { .. }
+ | ast::InlineExpression::VariableReference { .. }
+ | ast::InlineExpression::FunctionReference { .. } => {}
+ _ => {
+ return error!(ErrorKind::ExpectedSimpleExpressionAsSelector, self.ptr);
+ }
+ };
+
+ self.ptr += 2; // ->
+
+ self.skip_blank_inline();
+ if !self.skip_eol() {
+ return error!(
+ ErrorKind::ExpectedCharRange {
+ range: "\n | \r\n".to_string()
+ },
+ self.ptr
+ );
+ }
+ self.skip_blank();
+
+ let variants = self.get_variants()?;
+
+ Ok(ast::Expression::Select {
+ selector: exp,
+ variants,
+ })
+ }
+
+ pub(super) fn get_inline_expression(
+ &mut self,
+ only_literal: bool,
+ ) -> Result<ast::InlineExpression<S>> {
+ match get_current_byte!(self) {
+ Some(b'"') => {
+ self.ptr += 1; // "
+ let start = self.ptr;
+ while let Some(b) = get_current_byte!(self) {
+ match b {
+ b'\\' => match get_byte!(self, self.ptr + 1) {
+ Some(b'\\') | Some(b'{') | Some(b'"') => self.ptr += 2,
+ Some(b'u') => {
+ self.ptr += 2;
+ self.skip_unicode_escape_sequence(4)?;
+ }
+ Some(b'U') => {
+ self.ptr += 2;
+ self.skip_unicode_escape_sequence(6)?;
+ }
+ b => {
+ let seq = b.unwrap_or(&b' ').to_string();
+ return error!(ErrorKind::UnknownEscapeSequence(seq), self.ptr);
+ }
+ },
+ b'"' => {
+ break;
+ }
+ b'\n' => {
+ return error!(ErrorKind::UnterminatedStringLiteral, self.ptr);
+ }
+ _ => self.ptr += 1,
+ }
+ }
+
+ self.expect_byte(b'"')?;
+ let slice = self.source.slice(start..self.ptr - 1);
+ Ok(ast::InlineExpression::StringLiteral { value: slice })
+ }
+ Some(b) if b.is_ascii_digit() => {
+ let num = self.get_number_literal()?;
+ Ok(ast::InlineExpression::NumberLiteral { value: num })
+ }
+ Some(b'-') if !only_literal => {
+ self.ptr += 1; // -
+ if self.is_identifier_start() {
+ self.ptr += 1;
+ let id = self.get_identifier_unchecked();
+ let attribute = self.get_attribute_accessor()?;
+ let arguments = self.get_call_arguments()?;
+ Ok(ast::InlineExpression::TermReference {
+ id,
+ attribute,
+ arguments,
+ })
+ } else {
+ self.ptr -= 1;
+ let num = self.get_number_literal()?;
+ Ok(ast::InlineExpression::NumberLiteral { value: num })
+ }
+ }
+ Some(b'$') if !only_literal => {
+ self.ptr += 1; // $
+ let id = self.get_identifier()?;
+ Ok(ast::InlineExpression::VariableReference { id })
+ }
+ Some(b) if b.is_ascii_alphabetic() => {
+ self.ptr += 1;
+ let id = self.get_identifier_unchecked();
+ let arguments = self.get_call_arguments()?;
+ if let Some(arguments) = arguments {
+ if !Self::is_callee(&id.name) {
+ return error!(ErrorKind::ForbiddenCallee, self.ptr);
+ }
+
+ Ok(ast::InlineExpression::FunctionReference { id, arguments })
+ } else {
+ let attribute = self.get_attribute_accessor()?;
+ Ok(ast::InlineExpression::MessageReference { id, attribute })
+ }
+ }
+ Some(b'{') if !only_literal => {
+ self.ptr += 1; // {
+ let exp = self.get_placeable()?;
+ Ok(ast::InlineExpression::Placeable {
+ expression: Box::new(exp),
+ })
+ }
+ _ if only_literal => error!(ErrorKind::ExpectedLiteral, self.ptr),
+ _ => error!(ErrorKind::ExpectedInlineExpression, self.ptr),
+ }
+ }
+
+ pub fn get_call_arguments(&mut self) -> Result<Option<ast::CallArguments<S>>> {
+ self.skip_blank();
+ if !self.take_byte_if(b'(') {
+ return Ok(None);
+ }
+
+ let mut positional = vec![];
+ let mut named = vec![];
+ let mut argument_names = vec![];
+
+ self.skip_blank();
+
+ while self.ptr < self.length {
+ if self.is_current_byte(b')') {
+ break;
+ }
+
+ let expr = self.get_inline_expression(false)?;
+
+ if let ast::InlineExpression::MessageReference {
+ ref id,
+ attribute: None,
+ } = expr
+ {
+ self.skip_blank();
+ if self.is_current_byte(b':') {
+ if argument_names.contains(&id.name) {
+ return error!(
+ ErrorKind::DuplicatedNamedArgument(id.name.as_ref().to_owned()),
+ self.ptr
+ );
+ }
+ self.ptr += 1;
+ self.skip_blank();
+ let val = self.get_inline_expression(true)?;
+
+ argument_names.push(id.name.clone());
+ named.push(ast::NamedArgument {
+ name: ast::Identifier {
+ name: id.name.clone(),
+ },
+ value: val,
+ });
+ } else {
+ if !argument_names.is_empty() {
+ return error!(ErrorKind::PositionalArgumentFollowsNamed, self.ptr);
+ }
+ positional.push(expr);
+ }
+ } else {
+ if !argument_names.is_empty() {
+ return error!(ErrorKind::PositionalArgumentFollowsNamed, self.ptr);
+ }
+ positional.push(expr);
+ }
+
+ self.skip_blank();
+ self.take_byte_if(b',');
+ self.skip_blank();
+ }
+
+ self.expect_byte(b')')?;
+
+ Ok(Some(ast::CallArguments { positional, named }))
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/parser/helper.rs b/third_party/rust/fluent-syntax/src/parser/helper.rs
new file mode 100644
index 0000000000..11544d6855
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/helper.rs
@@ -0,0 +1,169 @@
+use super::errors::{ErrorKind, ParserError};
+use super::{core::Parser, core::Result, slice::Slice};
+
+impl<'s, S> Parser<S>
+where
+ S: Slice<'s>,
+{
+ pub(super) fn is_current_byte(&self, b: u8) -> bool {
+ get_current_byte!(self) == Some(&b)
+ }
+
+ pub(super) fn is_byte_at(&self, b: u8, pos: usize) -> bool {
+ get_byte!(self, pos) == Some(&b)
+ }
+
+ pub(super) fn skip_to_next_entry_start(&mut self) {
+ while let Some(b) = get_current_byte!(self) {
+ let new_line = self.ptr == 0 || get_byte!(self, self.ptr - 1) == Some(&b'\n');
+
+ if new_line && (b.is_ascii_alphabetic() || [b'-', b'#'].contains(b)) {
+ break;
+ }
+
+ self.ptr += 1;
+ }
+ }
+
+ pub(super) fn skip_eol(&mut self) -> bool {
+ match get_current_byte!(self) {
+ Some(b'\n') => {
+ self.ptr += 1;
+ true
+ }
+ Some(b'\r') if self.is_byte_at(b'\n', self.ptr + 1) => {
+ self.ptr += 2;
+ true
+ }
+ _ => false,
+ }
+ }
+
+ pub(super) fn skip_unicode_escape_sequence(&mut self, length: usize) -> Result<()> {
+ let start = self.ptr;
+ for _ in 0..length {
+ match get_current_byte!(self) {
+ Some(b) if b.is_ascii_hexdigit() => self.ptr += 1,
+ _ => break,
+ }
+ }
+ if self.ptr - start != length {
+ let end = if self.ptr >= self.length {
+ self.ptr
+ } else {
+ self.ptr + 1
+ };
+ let seq = self.source.slice(start..end).as_ref().to_owned();
+ return error!(ErrorKind::InvalidUnicodeEscapeSequence(seq), self.ptr);
+ }
+ Ok(())
+ }
+
+ pub(super) fn is_identifier_start(&self) -> bool {
+ matches!(get_current_byte!(self), Some(b) if b.is_ascii_alphabetic())
+ }
+
+ pub(super) fn take_byte_if(&mut self, b: u8) -> bool {
+ if self.is_current_byte(b) {
+ self.ptr += 1;
+ true
+ } else {
+ false
+ }
+ }
+
+ pub(super) fn skip_blank_block(&mut self) -> usize {
+ let mut count = 0;
+ loop {
+ let start = self.ptr;
+ self.skip_blank_inline();
+ if !self.skip_eol() {
+ self.ptr = start;
+ break;
+ }
+ count += 1;
+ }
+ count
+ }
+
+ pub(super) fn skip_blank(&mut self) {
+ loop {
+ match get_current_byte!(self) {
+ Some(b' ') | Some(b'\n') => self.ptr += 1,
+ Some(b'\r') if get_byte!(self, self.ptr + 1) == Some(&b'\n') => self.ptr += 2,
+ _ => break,
+ }
+ }
+ }
+
+ pub(super) fn skip_blank_inline(&mut self) -> usize {
+ let start = self.ptr;
+ while let Some(b' ') = get_current_byte!(self) {
+ self.ptr += 1;
+ }
+ self.ptr - start
+ }
+
+ pub(super) fn is_byte_pattern_continuation(b: u8) -> bool {
+ !matches!(b, b'.' | b'}' | b'[' | b'*')
+ }
+
+ pub(super) fn is_callee(name: &S) -> bool {
+ name.as_ref()
+ .as_bytes()
+ .iter()
+ .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || *c == b'_' || *c == b'-')
+ }
+
+ pub(super) fn expect_byte(&mut self, b: u8) -> Result<()> {
+ if !self.is_current_byte(b) {
+ return error!(ErrorKind::ExpectedToken(b as char), self.ptr);
+ }
+ self.ptr += 1;
+ Ok(())
+ }
+
+ pub(super) fn is_number_start(&self) -> bool {
+ matches!(get_current_byte!(self), Some(b) if b.is_ascii_digit() || b == &b'-')
+ }
+
+ pub(super) fn is_eol(&self) -> bool {
+ match get_current_byte!(self) {
+ Some(b'\n') => true,
+ Some(b'\r') if self.is_byte_at(b'\n', self.ptr + 1) => true,
+ None => true,
+ _ => false,
+ }
+ }
+
+ pub(super) fn skip_digits(&mut self) -> Result<()> {
+ let start = self.ptr;
+ loop {
+ match get_current_byte!(self) {
+ Some(b) if b.is_ascii_digit() => self.ptr += 1,
+ _ => break,
+ }
+ }
+ if start == self.ptr {
+ error!(
+ ErrorKind::ExpectedCharRange {
+ range: "0-9".to_string()
+ },
+ self.ptr
+ )
+ } else {
+ Ok(())
+ }
+ }
+
+ pub(super) fn get_number_literal(&mut self) -> Result<S> {
+ let start = self.ptr;
+ self.take_byte_if(b'-');
+ self.skip_digits()?;
+ if self.take_byte_if(b'.') {
+ self.skip_digits()?;
+ }
+
+ Ok(self.source.slice(start..self.ptr))
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/parser/macros.rs b/third_party/rust/fluent-syntax/src/parser/macros.rs
new file mode 100644
index 0000000000..671d543285
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/macros.rs
@@ -0,0 +1,11 @@
+macro_rules! get_byte {
+ ($s:expr, $idx:expr) => {
+ $s.source.as_ref().as_bytes().get($idx)
+ };
+}
+
+macro_rules! get_current_byte {
+ ($s:expr) => {
+ $s.source.as_ref().as_bytes().get($s.ptr)
+ };
+}
diff --git a/third_party/rust/fluent-syntax/src/parser/mod.rs b/third_party/rust/fluent-syntax/src/parser/mod.rs
new file mode 100644
index 0000000000..52edfdc37a
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/mod.rs
@@ -0,0 +1,278 @@
+//! Fluent Translation List parsing utilities
+//!
+//! FTL resources can be parsed using one of two methods:
+//! * [`parse`] - parses an input into a complete Abstract Syntax Tree representation with all source information preserved.
+//! * [`parse_runtime`] - parses an input into a runtime optimized Abstract Syntax Tree
+//! representation with comments stripped.
+//!
+//! # Example
+//!
+//! ```
+//! use fluent_syntax::parser;
+//! use fluent_syntax::ast;
+//!
+//! let ftl = r#"
+//! #### Resource Level Comment
+//!
+//! ## This is a message comment
+//! hello-world = Hello World!
+//!
+//! "#;
+//!
+//! let resource = parser::parse(ftl)
+//! .expect("Failed to parse an FTL resource.");
+//!
+//! assert_eq!(
+//! resource.body[0],
+//! ast::Entry::ResourceComment(
+//! ast::Comment {
+//! content: vec![
+//! "Resource Level Comment"
+//! ]
+//! }
+//! )
+//! );
+//! assert_eq!(
+//! resource.body[1],
+//! ast::Entry::Message(
+//! ast::Message {
+//! id: ast::Identifier {
+//! name: "hello-world"
+//! },
+//! value: Some(ast::Pattern {
+//! elements: vec![
+//! ast::PatternElement::TextElement {
+//! value: "Hello World!"
+//! },
+//! ]
+//! }),
+//! attributes: vec![],
+//! comment: Some(
+//! ast::Comment {
+//! content: vec!["This is a message comment"]
+//! }
+//! )
+//! }
+//! ),
+//! );
+//! ```
+//!
+//! # Error Recovery
+//!
+//! In both modes the parser is lenient, attempting to recover from errors.
+//!
+//! The [`Result`] return the resulting AST in both scenarios, and in the
+//! error scenario a vector of [`ParserError`] elements is returned as well.
+//!
+//! Any unparsed parts of the input are returned as [`ast::Entry::Junk`] elements.
+#[macro_use]
+mod errors;
+#[macro_use]
+mod macros;
+mod comment;
+mod core;
+mod expression;
+mod helper;
+mod pattern;
+mod runtime;
+mod slice;
+
+use crate::ast;
+pub use errors::{ErrorKind, ParserError};
+pub use slice::Slice;
+
+/// Parser result always returns an AST representation of the input,
+/// and if parsing errors were encountered, a list of [`ParserError`] elements
+/// is also returned.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+/// key1 = Value 1
+///
+/// g@Rb@ge = #2y ds
+///
+/// key2 = Value 2
+///
+/// "#;
+///
+/// let (resource, errors) = parser::parse_runtime(ftl)
+/// .expect_err("Resource should contain errors.");
+///
+/// assert_eq!(
+/// errors,
+/// vec![
+/// parser::ParserError {
+/// pos: 18..19,
+/// slice: Some(17..35),
+/// kind: parser::ErrorKind::ExpectedToken('=')
+/// }
+/// ]
+/// );
+///
+/// assert_eq!(
+/// resource.body[0],
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "key1"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 1"
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }
+/// ),
+/// );
+///
+/// assert_eq!(
+/// resource.body[1],
+/// ast::Entry::Junk {
+/// content: "g@Rb@ge = #2y ds\n\n"
+/// }
+/// );
+///
+/// assert_eq!(
+/// resource.body[2],
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "key2"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Value 2"
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }
+/// ),
+/// );
+/// ```
+pub type Result<S> = std::result::Result<ast::Resource<S>, (ast::Resource<S>, Vec<ParserError>)>;
+
+/// Parses an input into a complete Abstract Syntax Tree representation with
+/// all source information preserved.
+///
+/// This mode is intended for tooling, linters and other scenarios where
+/// complete representation, with comments, is preferred over speed and memory
+/// utilization.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+/// #### Resource Level Comment
+///
+/// ## This is a message comment
+/// hello-world = Hello World!
+///
+/// "#;
+///
+/// let resource = parser::parse(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource.body[0],
+/// ast::Entry::ResourceComment(
+/// ast::Comment {
+/// content: vec![
+/// "Resource Level Comment"
+/// ]
+/// }
+/// )
+/// );
+/// assert_eq!(
+/// resource.body[1],
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "hello-world"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Hello World!"
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: Some(
+/// ast::Comment {
+/// content: vec!["This is a message comment"]
+/// }
+/// )
+/// }
+/// ),
+/// );
+/// ```
+pub fn parse<'s, S>(input: S) -> Result<S>
+where
+ S: Slice<'s>,
+{
+ core::Parser::new(input).parse()
+}
+
+/// Parses an input into an Abstract Syntax Tree representation with comments stripped.
+///
+/// This mode is intended for runtime use of Fluent. It currently strips all
+/// comments improving parsing performance and reducing the size of the AST tree.
+///
+/// # Example
+///
+/// ```
+/// use fluent_syntax::parser;
+/// use fluent_syntax::ast;
+///
+/// let ftl = r#"
+/// #### Resource Level Comment
+///
+/// ## This is a message comment
+/// hello-world = Hello World!
+///
+/// "#;
+///
+/// let resource = parser::parse_runtime(ftl)
+/// .expect("Failed to parse an FTL resource.");
+///
+/// assert_eq!(
+/// resource.body[0],
+/// ast::Entry::Message(
+/// ast::Message {
+/// id: ast::Identifier {
+/// name: "hello-world"
+/// },
+/// value: Some(ast::Pattern {
+/// elements: vec![
+/// ast::PatternElement::TextElement {
+/// value: "Hello World!"
+/// },
+/// ]
+/// }),
+/// attributes: vec![],
+/// comment: None,
+/// }
+/// ),
+/// );
+/// ```
+pub fn parse_runtime<'s, S>(input: S) -> Result<S>
+where
+ S: Slice<'s>,
+{
+ core::Parser::new(input).parse_runtime()
+}
diff --git a/third_party/rust/fluent-syntax/src/parser/pattern.rs b/third_party/rust/fluent-syntax/src/parser/pattern.rs
new file mode 100644
index 0000000000..516326d761
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/pattern.rs
@@ -0,0 +1,207 @@
+use super::errors::{ErrorKind, ParserError};
+use super::{core::Parser, core::Result, slice::Slice};
+use crate::ast;
+
+#[derive(Debug, PartialEq)]
+enum TextElementTermination {
+ LineFeed,
+ CRLF,
+ PlaceableStart,
+ EOF,
+}
+
+// This enum tracks the placement of the text element in the pattern, which is needed for
+// dedentation logic.
+#[derive(Debug, PartialEq)]
+enum TextElementPosition {
+ InitialLineStart,
+ LineStart,
+ Continuation,
+}
+
+// This enum allows us to mark pointers in the source which will later become text elements
+// but without slicing them out of the source string. This makes the indentation adjustments
+// cheaper since they'll happen on the pointers, rather than extracted slices.
+#[derive(Debug)]
+enum PatternElementPlaceholders<S> {
+ Placeable(ast::Expression<S>),
+ // (start, end, indent, position)
+ TextElement(usize, usize, usize, TextElementPosition),
+}
+
+// This enum tracks whether the text element is blank or not.
+// This is important to identify text elements which should not be taken into account
+// when calculating common indent.
+#[derive(Debug, PartialEq)]
+enum TextElementType {
+ Blank,
+ NonBlank,
+}
+
+impl<'s, S> Parser<S>
+where
+ S: Slice<'s>,
+{
+ pub(super) fn get_pattern(&mut self) -> Result<Option<ast::Pattern<S>>> {
+ let mut elements = vec![];
+ let mut last_non_blank = None;
+ let mut common_indent = None;
+
+ self.skip_blank_inline();
+
+ let mut text_element_role = if self.skip_eol() {
+ self.skip_blank_block();
+ TextElementPosition::LineStart
+ } else {
+ TextElementPosition::InitialLineStart
+ };
+
+ while self.ptr < self.length {
+ if self.take_byte_if(b'{') {
+ if text_element_role == TextElementPosition::LineStart {
+ common_indent = Some(0);
+ }
+ let exp = self.get_placeable()?;
+ last_non_blank = Some(elements.len());
+ elements.push(PatternElementPlaceholders::Placeable(exp));
+ text_element_role = TextElementPosition::Continuation;
+ } else {
+ let slice_start = self.ptr;
+ let mut indent = 0;
+ if text_element_role == TextElementPosition::LineStart {
+ indent = self.skip_blank_inline();
+ if let Some(b) = get_current_byte!(self) {
+ if indent == 0 {
+ if b != &b'\r' && b != &b'\n' {
+ break;
+ }
+ } else if !Self::is_byte_pattern_continuation(*b) {
+ self.ptr = slice_start;
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+ let (start, end, text_element_type, termination_reason) = self.get_text_slice()?;
+ if start != end {
+ if text_element_role == TextElementPosition::LineStart
+ && text_element_type == TextElementType::NonBlank
+ {
+ if let Some(common) = common_indent {
+ if indent < common {
+ common_indent = Some(indent);
+ }
+ } else {
+ common_indent = Some(indent);
+ }
+ }
+ if text_element_role != TextElementPosition::LineStart
+ || text_element_type == TextElementType::NonBlank
+ || termination_reason == TextElementTermination::LineFeed
+ {
+ if text_element_type == TextElementType::NonBlank {
+ last_non_blank = Some(elements.len());
+ }
+ elements.push(PatternElementPlaceholders::TextElement(
+ slice_start,
+ end,
+ indent,
+ text_element_role,
+ ));
+ }
+ }
+
+ text_element_role = match termination_reason {
+ TextElementTermination::LineFeed => TextElementPosition::LineStart,
+ TextElementTermination::CRLF => TextElementPosition::LineStart,
+ TextElementTermination::PlaceableStart => TextElementPosition::Continuation,
+ TextElementTermination::EOF => TextElementPosition::Continuation,
+ };
+ }
+ }
+
+ if let Some(last_non_blank) = last_non_blank {
+ let elements = elements
+ .into_iter()
+ .take(last_non_blank + 1)
+ .enumerate()
+ .map(|(i, elem)| match elem {
+ PatternElementPlaceholders::Placeable(expression) => {
+ ast::PatternElement::Placeable { expression }
+ }
+ PatternElementPlaceholders::TextElement(start, end, indent, role) => {
+ let start = if role == TextElementPosition::LineStart {
+ common_indent.map_or_else(
+ || start + indent,
+ |common_indent| start + std::cmp::min(indent, common_indent),
+ )
+ } else {
+ start
+ };
+ let mut value = self.source.slice(start..end);
+ if last_non_blank == i {
+ value.trim();
+ }
+ ast::PatternElement::TextElement { value }
+ }
+ })
+ .collect();
+ return Ok(Some(ast::Pattern { elements }));
+ }
+
+ Ok(None)
+ }
+
+ fn get_text_slice(
+ &mut self,
+ ) -> Result<(usize, usize, TextElementType, TextElementTermination)> {
+ let start_pos = self.ptr;
+ let mut text_element_type = TextElementType::Blank;
+
+ while let Some(b) = get_current_byte!(self) {
+ match b {
+ b' ' => self.ptr += 1,
+ b'\n' => {
+ self.ptr += 1;
+ return Ok((
+ start_pos,
+ self.ptr,
+ text_element_type,
+ TextElementTermination::LineFeed,
+ ));
+ }
+ b'\r' if self.is_byte_at(b'\n', self.ptr + 1) => {
+ self.ptr += 1;
+ return Ok((
+ start_pos,
+ self.ptr - 1,
+ text_element_type,
+ TextElementTermination::CRLF,
+ ));
+ }
+ b'{' => {
+ return Ok((
+ start_pos,
+ self.ptr,
+ text_element_type,
+ TextElementTermination::PlaceableStart,
+ ));
+ }
+ b'}' => {
+ return error!(ErrorKind::UnbalancedClosingBrace, self.ptr);
+ }
+ _ => {
+ text_element_type = TextElementType::NonBlank;
+ self.ptr += 1
+ }
+ }
+ }
+ Ok((
+ start_pos,
+ self.ptr,
+ text_element_type,
+ TextElementTermination::EOF,
+ ))
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/parser/runtime.rs b/third_party/rust/fluent-syntax/src/parser/runtime.rs
new file mode 100644
index 0000000000..e116ceaeed
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/runtime.rs
@@ -0,0 +1,61 @@
+use super::{
+ core::{Parser, Result},
+ errors::ParserError,
+ slice::Slice,
+};
+use crate::ast;
+
+impl<'s, S> Parser<S>
+where
+ S: Slice<'s>,
+{
+ pub fn parse_runtime(
+ mut self,
+ ) -> std::result::Result<ast::Resource<S>, (ast::Resource<S>, Vec<ParserError>)> {
+ let mut errors = vec![];
+
+ // That default allocation gives the lowest
+ // number of instructions and cycles in ioi.
+ let mut body = Vec::with_capacity(6);
+
+ self.skip_blank_block();
+
+ while self.ptr < self.length {
+ let entry_start = self.ptr;
+ let entry = self.get_entry_runtime(entry_start);
+
+ match entry {
+ Ok(Some(entry)) => {
+ body.push(entry);
+ }
+ Ok(None) => {}
+ Err(mut err) => {
+ self.skip_to_next_entry_start();
+ err.slice = Some(entry_start..self.ptr);
+ errors.push(err);
+ let content = self.source.slice(entry_start..self.ptr);
+ body.push(ast::Entry::Junk { content });
+ }
+ }
+ self.skip_blank_block();
+ }
+
+ if errors.is_empty() {
+ Ok(ast::Resource { body })
+ } else {
+ Err((ast::Resource { body }, errors))
+ }
+ }
+
+ fn get_entry_runtime(&mut self, entry_start: usize) -> Result<Option<ast::Entry<S>>> {
+ let entry = match get_current_byte!(self) {
+ Some(b'#') => {
+ self.skip_comment();
+ None
+ }
+ Some(b'-') => Some(ast::Entry::Term(self.get_term(entry_start)?)),
+ _ => Some(ast::Entry::Message(self.get_message(entry_start)?)),
+ };
+ Ok(entry)
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/parser/slice.rs b/third_party/rust/fluent-syntax/src/parser/slice.rs
new file mode 100644
index 0000000000..d44f8251fe
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/parser/slice.rs
@@ -0,0 +1,25 @@
+use std::ops::Range;
+pub trait Slice<'s>: AsRef<str> + Clone + PartialEq {
+ fn slice(&self, range: Range<usize>) -> Self;
+ fn trim(&mut self);
+}
+
+impl<'s> Slice<'s> for String {
+ fn slice(&self, range: Range<usize>) -> Self {
+ self[range].to_string()
+ }
+
+ fn trim(&mut self) {
+ *self = self.trim_end().to_string();
+ }
+}
+
+impl<'s> Slice<'s> for &'s str {
+ fn slice(&self, range: Range<usize>) -> Self {
+ &self[range]
+ }
+
+ fn trim(&mut self) {
+ *self = self.trim_end();
+ }
+}
diff --git a/third_party/rust/fluent-syntax/src/unicode.rs b/third_party/rust/fluent-syntax/src/unicode.rs
new file mode 100644
index 0000000000..ab95a86884
--- /dev/null
+++ b/third_party/rust/fluent-syntax/src/unicode.rs
@@ -0,0 +1,159 @@
+//! A set of helper functions for unescaping Fluent unicode escape sequences.
+//!
+//! # Unicode
+//!
+//! Fluent supports UTF-8 in all FTL resources, but it also allows
+//! unicode sequences to be escaped in [`String
+//! Literals`](super::ast::InlineExpression::StringLiteral).
+//!
+//! Four byte sequences are encoded with `\u` and six byte
+//! sqeuences using `\U`.
+//! ## Example
+//!
+//! ```
+//! use fluent_syntax::unicode::unescape_unicode_to_string;
+//!
+//! assert_eq!(
+//! unescape_unicode_to_string("Foo \\u5bd2 Bar"),
+//! "Foo 寒 Bar"
+//! );
+//!
+//! assert_eq!(
+//! unescape_unicode_to_string("Foo \\U01F68A Bar"),
+//! "Foo 🚊 Bar"
+//! );
+//! ```
+//!
+//! # Other unescapes
+//!
+//! This also allows for a char `"` to be present inside an FTL string literal,
+//! and for `\` itself to be escaped.
+//!
+//! ## Example
+//!
+//! ```
+//! use fluent_syntax::unicode::unescape_unicode_to_string;
+//!
+//! assert_eq!(
+//! unescape_unicode_to_string("Foo \\\" Bar"),
+//! "Foo \" Bar"
+//! );
+//! assert_eq!(
+//! unescape_unicode_to_string("Foo \\\\ Bar"),
+//! "Foo \\ Bar"
+//! );
+//! ```
+use std::borrow::Cow;
+use std::char;
+use std::fmt;
+
+const UNKNOWN_CHAR: char = '�';
+
+fn encode_unicode(s: Option<&str>) -> char {
+ s.and_then(|s| u32::from_str_radix(s, 16).ok().and_then(char::from_u32))
+ .unwrap_or(UNKNOWN_CHAR)
+}
+
+/// Unescapes to a writer without allocating.
+///
+/// ## Example
+///
+/// ```
+/// use fluent_syntax::unicode::unescape_unicode;
+///
+/// let mut s = String::new();
+/// unescape_unicode(&mut s, "Foo \\U01F60A Bar");
+/// assert_eq!(s, "Foo 😊 Bar");
+/// ```
+pub fn unescape_unicode<W>(w: &mut W, input: &str) -> fmt::Result
+where
+ W: fmt::Write,
+{
+ let bytes = input.as_bytes();
+
+ let mut start = 0;
+ let mut ptr = 0;
+
+ while let Some(b) = bytes.get(ptr) {
+ if b != &b'\\' {
+ ptr += 1;
+ continue;
+ }
+ if start != ptr {
+ w.write_str(&input[start..ptr])?;
+ }
+
+ ptr += 1;
+
+ let new_char = match bytes.get(ptr) {
+ Some(b'\\') => '\\',
+ Some(b'"') => '"',
+ Some(u @ b'u') | Some(u @ b'U') => {
+ let seq_start = ptr + 1;
+ let len = if u == &b'u' { 4 } else { 6 };
+ ptr += len;
+ encode_unicode(input.get(seq_start..seq_start + len))
+ }
+ _ => UNKNOWN_CHAR,
+ };
+ ptr += 1;
+ w.write_char(new_char)?;
+ start = ptr;
+ }
+ if start != ptr {
+ w.write_str(&input[start..ptr])?;
+ }
+ Ok(())
+}
+
+/// Unescapes to a `Cow<str>` optionally allocating.
+///
+/// ## Example
+///
+/// ```
+/// use fluent_syntax::unicode::unescape_unicode_to_string;
+///
+/// assert_eq!(
+/// unescape_unicode_to_string("Foo \\U01F60A Bar"),
+/// "Foo 😊 Bar"
+/// );
+/// ```
+pub fn unescape_unicode_to_string(input: &str) -> Cow<str> {
+ let bytes = input.as_bytes();
+ let mut result = Cow::from(input);
+
+ let mut ptr = 0;
+
+ while let Some(b) = bytes.get(ptr) {
+ if b != &b'\\' {
+ if let Cow::Owned(ref mut s) = result {
+ s.push(*b as char);
+ }
+ ptr += 1;
+ continue;
+ }
+
+ if let Cow::Borrowed(_) = result {
+ result = Cow::from(&input[0..ptr]);
+ }
+
+ ptr += 1;
+
+ let new_char = match bytes.get(ptr) {
+ Some(b'\\') => '\\',
+ Some(b'"') => '"',
+ Some(u @ b'u') | Some(u @ b'U') => {
+ let start = ptr + 1;
+ let len = if u == &b'u' { 4 } else { 6 };
+ ptr += len;
+ input
+ .get(start..(start + len))
+ .map_or(UNKNOWN_CHAR, |slice| encode_unicode(Some(slice)))
+ }
+ _ => UNKNOWN_CHAR,
+ };
+ result.to_mut().push(new_char);
+ ptr += 1;
+ }
+ result
+}
diff --git a/third_party/rust/fluent-testing/.cargo-checksum.json b/third_party/rust/fluent-testing/.cargo-checksum.json
new file mode 100644
index 0000000000..456dd28082
--- /dev/null
+++ b/third_party/rust/fluent-testing/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"28b6a5f84c171d00533b5bc2c6310b89fc34eece90676fdf7bbe48b04c60bf89","LICENSE-APACHE":"5db2b182453ff32ed40f7da63589c9667a3f8bd8b16b1471b152caae56f77e45","LICENSE-MIT":"49c0b000c03731d9e3970dc059ad4ca345d773681f4a612b0024435b663e0220","README.md":"53a42a2ebba5973f09c6e608e3a41f52711de8306796205080195840ffa0083e","resources/README.md":"71c732e8871072cfafb2121b16258d60fff8292c928b264c4fa9ae9e07bb4045","resources/browser/en-US/branding/brand.ftl":"dd9004ed4b9d87bf3fa17aa9aba1f0088257c73ec52590e00b4ad117013c22be","resources/browser/en-US/browser/aboutDialog.ftl":"67b5a5afe1c26f5aefd0b484348f127e203eaf9999152194d465625920e3ea06","resources/browser/en-US/browser/allTabsMenu.ftl":"1e6cb95ec697c0c0d4c4bdc1bb3d4c4831e38f7438cece0ea5f569266b2f750f","resources/browser/en-US/browser/appmenu.ftl":"1db7f5195c0a4168feae3ee577bbc9c74e99e92f271f61966b323299e2dd404b","resources/browser/en-US/browser/branding/brandings.ftl":"51c33061b83d078cf44f1774520ec4166ff147523ba42299a5708b68e5b1291c","resources/browser/en-US/browser/branding/sync-brand.ftl":"9ce5a0bd81a1eb4c082c5790c4b9e57ebd332463bcac8ba6c8d1b5d9c2df535e","resources/browser/en-US/browser/browser.ftl":"fb208e9f73ee69a1c577aa61eeabe7accfb6925cc082195c2f59d4ce777de9fa","resources/browser/en-US/browser/browserContext.ftl":"d3683b6ee55ab14878fffad065fd18e3c742a188fe5faf2a1b43707af79e247e","resources/browser/en-US/browser/browserSets.ftl":"abcd2b8e5b56c1dafe482d805c561aa624f5477e4469b086a56ed3727e978e17","resources/browser/en-US/browser/downloads.ftl":"e0f92ec12805864b5fea60dc28f086f2a92fe20ddc3281220a878ec5eef48b49","resources/browser/en-US/browser/menubar.ftl":"0717aded2bd4ac646de19dc8849925c868db5856e62b6fc8bdde3375c1bb1fa8","resources/browser/en-US/browser/places.ftl":"570761a59fd7cdab524010c11238b65b28f012e27c8b2379764ec0d86b71f010","resources/browser/en-US/browser/preferences/addEngine.ftl":"f99756f4fbf02af4e1f6defdd956afe710eafc95ad5ba873ef6500c3a01e861b","resources/browser/en-US/browser/preferences/blocklists.ftl":"7786a7d9fbdb49f71c2eac1bf40faaeee2ded9052c37cd8cf23a6a3f6b66744f","resources/browser/en-US/browser/preferences/clearSiteData.ftl":"81886a547285a1d2638b292342a775710ded804d114d2ee0733a9e785de60b64","resources/browser/en-US/browser/preferences/colors.ftl":"64bb27c95a5c4c3b29e35f2a35a15a99039e40705e174288f4dcaacb8054cf16","resources/browser/en-US/browser/preferences/connection.ftl":"0d090b71a09307073f11a6a5a98d1e1856e71bc285877463c53170cc1463d684","resources/browser/en-US/browser/preferences/fonts.ftl":"6899fb6cc7fd3d71f79af8413d3f49a4a7e0fda48e8847ca0b85a3a6fd07a669","resources/browser/en-US/browser/preferences/languages.ftl":"6a4d593d0178c7fe9af5b89abbe7bc0131f67b95bbd84002699f3701463a8c0a","resources/browser/en-US/browser/preferences/permissions.ftl":"b7123c46d4009d2d4f0cf9557e0324b6c3904cc327501d4fa367186e8637aa8d","resources/browser/en-US/browser/preferences/preferences.ftl":"0c67e8aa7de2ca47466a03f7ebcfdf189145d42f7c13fcc9ab4ba5f4351477f1","resources/browser/en-US/browser/preferences/selectBookmark.ftl":"edeada75825e932f44ddfe5261feae5b05c33c423530c06202c000698f701125","resources/browser/en-US/browser/preferences/siteDataSettings.ftl":"1bf8e41445c31fab1104defee087206341ecd90dd8912390e962c0b4a9fdaebc","resources/browser/en-US/browser/protectionsPanel.ftl":"9016d41a0773abcdbc4290e5a9ac5d88d71daacddfc247e643d912c030d3abfb","resources/browser/en-US/browser/sanitize.ftl":"2008c7fac2f10fcf265f5a69ffe433ec8aff48a7484fadf6cc8c2b672a2ac474","resources/browser/en-US/browser/sidebarMenu.ftl":"ff6e1bacb3fc9a46fdaac2adcdabb6d742006a6d8aa89210dcc2ba2030daceaf","resources/browser/en-US/preview/interventions.ftl":"9612d7b8d714b465df51e8ad2b4b02e6437071cd0cf1ef1339482daab7fb8307","resources/browser/pl/branding/brand.ftl":"ddf6c8480e77b53054bf7da620e9d8992dd99f766e67e6dbf7e5a6c4fb621ccc","resources/browser/pl/browser/aboutDialog.ftl":"f1ad814eb262960554770a39ac23f6d532cb153500a66624267236a0b3f92180","resources/browser/pl/browser/allTabsMenu.ftl":"fc9dbafae900bb9ec927473a07a2cbfc375b86b19c5f1c3d6193e45f3f46ea49","resources/browser/pl/browser/appmenu.ftl":"3e0d288bb82e38b57bc4010a5e1228ec64a913e1793271cec9bdbf8cddab05b6","resources/browser/pl/browser/branding/brandings.ftl":"222db172c889501db52dcf4cbb737bda272ca9891e90fd3029908a04c07fdde1","resources/browser/pl/browser/branding/sync-brand.ftl":"0deebfc761c784669e6054218f23b5ce1f2034a459d8e82931897356ff0bbfd7","resources/browser/pl/browser/browser.ftl":"46d941b1c3e15e168908e410f9f5efa9670ebe2d6369d04255f7c761c631e80d","resources/browser/pl/browser/browserContext.ftl":"a04211fd75d4237f675169024460ad8146d56a6291ad56d79fdc83f2d491a265","resources/browser/pl/browser/browserSets.ftl":"75351a196bee013fd01ce464edd412356b925e831c0e266ad056079f063aaf02","resources/browser/pl/browser/downloads.ftl":"9081edcb1eadde065ebb0016011db02e4bc0cbf7af06de702b5d391316268add","resources/browser/pl/browser/menubar.ftl":"af5512ae87a84ea76fde339d2b5efed3d2c0944af367c0cf4d880a80670fe27f","resources/browser/pl/browser/places.ftl":"a1daad73f00ee0b0ce4f436a4461c582433e368f8699d2a0923cdc721325d2ab","resources/browser/pl/browser/preferences/addEngine.ftl":"4508c0a9c125e7d46900e286b941163c628a594b6975dd18954848905e7f7c6f","resources/browser/pl/browser/preferences/blocklists.ftl":"eab3e95c754e84287d6544a98771635dc5521a090fc6f4e1ea08e94e6c8ee7f2","resources/browser/pl/browser/preferences/clearSiteData.ftl":"270620a5ce4fded5312b4b1fb82598bd487bf1d46fb4e7ce7154186d4a7813ea","resources/browser/pl/browser/preferences/colors.ftl":"215439ee9997d281a287ef47f4c5184fe9ab07619bc95dab9ccbe1bf3ff2825a","resources/browser/pl/browser/preferences/connection.ftl":"d58c098f70ceab1716cf98062a4576c006d21f903c6e761c6b1d3e4905705972","resources/browser/pl/browser/preferences/fonts.ftl":"b6d349feb79f32504831404702f59326d8e303623711d57ec0abc4a408f058c8","resources/browser/pl/browser/preferences/languages.ftl":"ecfc28954c83ba5d2753419036538ebcb83368ab68bf81eeea6ae4499f151de2","resources/browser/pl/browser/preferences/permissions.ftl":"9734f53f85b592cbab56189f988030b102a673e67dc9a8ac702bfccf81f26f39","resources/browser/pl/browser/preferences/preferences.ftl":"581f8382376d80e288090582579210bbc3b2bef3647e4777fb2fd062837d5470","resources/browser/pl/browser/preferences/selectBookmark.ftl":"c7bc41c76f3fc8942068c89cee90e446b4449822adf24b9b9d73b5f930e4e6bb","resources/browser/pl/browser/preferences/siteDataSettings.ftl":"ab2c439013646f4e853ea13f150c21ca9e6a8b6de25b82db7d85be58f6498de9","resources/browser/pl/browser/protectionsPanel.ftl":"71553e4186449b181dd1367f4a397f7cbae26e51db23548cc39110aca72fa56a","resources/browser/pl/browser/sanitize.ftl":"48e27c98f2f76c10eaa5d0da66b63dbd98f21de061adcb06ab319e66f19c843a","resources/browser/pl/browser/sidebarMenu.ftl":"010e5cdb60f72b4315c2e3b219571fc709532ffd21c408f7acadcae8fb796590","resources/empty-resource/en-US/empty/empty-all.ftl":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","resources/empty-resource/en-US/empty/empty-one.ftl":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","resources/empty-resource/pl/empty/empty-all.ftl":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","resources/empty-resource/pl/empty/empty-one.ftl":"45aaecaab59a333402073fee827ced56da5bdf7ccfc46c3e04be78c31005c55b","resources/missing-resource/pl/missing/missing-one.ftl":"5317be25f302f5a6e6b10cdf7fa5c49ce86d53f9501721d840ac582fd0842745","resources/toolkit/en-US/security/certificates/certManager.ftl":"174aa482e8f268a437de07628d42fb101af1a95dfef83fe933bd2cfb2306006c","resources/toolkit/en-US/security/certificates/deviceManager.ftl":"2dcd12266884ca9ff4b21f9d05fe83168b2b7cf1967195975d494c2512d9d385","resources/toolkit/en-US/toolkit/featuregates/features.ftl":"8b709748e4d6b4d66965d2881623ed3af193ba4a42b480337d09cc1c6883c335","resources/toolkit/en-US/toolkit/global/textActions.ftl":"1400a4d5d150d26b0b42360da5bfa59eb0a4957e51b21994ecaff65cebb830f9","resources/toolkit/en-US/toolkit/printing/printUI.ftl":"5477add8001a7cbb239a5602b11b56222b329b91764b5eea8d5cb01bd7fee6a9","resources/toolkit/en-US/toolkit/updates/history.ftl":"deb0e178779ab50460d741c7f7e1bc3507e90a98839d12682cd7ae7524e24730","resources/toolkit/pl/security/certificates/certManager.ftl":"a455c4807e68b03c4219110d1f2b6d6ae6162d8c1602b7255f612ab7ff76f7b9","resources/toolkit/pl/security/certificates/deviceManager.ftl":"5d39a2bfb74488ae9d5c19a837bcf693d645ef5df734af2207ba9b33fba7db15","resources/toolkit/pl/toolkit/featuregates/features.ftl":"cf61454864a91cfc96ba9c973d793d4c191d591f2de480a1ee8ed3c1a4c5bedb","resources/toolkit/pl/toolkit/global/textActions.ftl":"26f41ac34bd568fb92a08b1f9b1a3efb7f632bab4f128682875f58530e6868ea","resources/toolkit/pl/toolkit/printing/printUI.ftl":"b6414d5309cb616920377fbd2e1f7472f49a7107b649117f9af00d5d30bf8e3f","resources/toolkit/pl/toolkit/updates/history.ftl":"050983c0dfdc0ba7d7a5cc9b8c5dcc4d4f1dbcc671b16280fbf51158d7a495e9","src/fs.rs":"abd60ce5b792e8e082d7162359fe4ea9c1c566e051d9574f1ab50bac975667b0","src/lib.rs":"c227c853318e671954236dff017cb000dea583e07cb916814a7abdb0ce3d973a","src/scenarios/browser.rs":"c85718c78a4e73e03ec17944dff656cc30923f5ce2ea930504f2107c5272a054","src/scenarios/empty_resource_all_locales.rs":"1cf30de067c8727f78960e47522f31fdf5b4d0f0f45edf7aeb75e315e2149b73","src/scenarios/empty_resource_one_locale.rs":"0234989d252c14569798cda87b0322c5116a1c1383c662136d463291eeecebf1","src/scenarios/missing_optional_all_locales.rs":"c7a41d3a7832b56b4e7b4a11bc65ced27aa47670a54f089697e605c64b70dfc5","src/scenarios/missing_optional_one_locale.rs":"67a3ba9d1574918e71411a296e10c981444122a088fc3f3ff3de8710d3dcaa78","src/scenarios/missing_required_all_locales.rs":"0fceae5ae21290a29871589bdf7fef7792d87618b1565ebc2ee89e621abc51b9","src/scenarios/missing_required_one_locale.rs":"41353c3ef07c29601385250bc1daec0289749d2691398bf3459a03eafe70e737","src/scenarios/mod.rs":"072b026683841f653fb7637393cecd86e1a7ce2557ad07083c4aed994a79d0d1","src/scenarios/preferences.rs":"79430165be0ded10b00c010017177325fc2dd816d11b7a7aa750a654c0773505","src/scenarios/simple.rs":"b696519ceabc49e8f26997961056d77afc51e7b84ac6759688e181c3d1ecc8ca","src/scenarios/structs.rs":"a443dd97b5f5b81cc9b8f85ab24da2c1f919bd6ce9dcaf189722022f1f182ef7"},"package":"cd97ebaec22ed506c034098decf727363925b77fa265d20816f17f35c871e7f8"} \ No newline at end of file
diff --git a/third_party/rust/fluent-testing/Cargo.toml b/third_party/rust/fluent-testing/Cargo.toml
new file mode 100644
index 0000000000..0cb37eb311
--- /dev/null
+++ b/third_party/rust/fluent-testing/Cargo.toml
@@ -0,0 +1,67 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+name = "fluent-testing"
+version = "0.0.3"
+authors = [
+ "Zibi Braniecki <zibi@braniecki.net>",
+ "Erik Nordin <enordin@mozilla.com>",
+]
+include = [
+ "src/**/*",
+ "resources/**/*",
+ "Cargo.toml",
+ "README.md",
+ "LICENSE-APACHE",
+ "LICENSE-MIT",
+]
+description = """
+A collection of mock scenarios for testing fluent-rs components.
+"""
+homepage = "https://www.projectfluent.org"
+readme = "README.md"
+keywords = [
+ "localization",
+ "l10n",
+ "i18n",
+ "intl",
+ "internationalization",
+]
+categories = [
+ "localization",
+ "internationalization",
+]
+license = "Apache-2.0/MIT"
+repository = "https://github.com/projectfluent/fluent-rs"
+resolver = "1"
+
+[dependencies.fluent-bundle]
+version = "0.15.2"
+
+[dependencies.fluent-fallback]
+version = "0.7.0"
+
+[dependencies.tokio]
+version = "1.0"
+features = [
+ "fs",
+ "rt-multi-thread",
+ "macros",
+ "io-util",
+]
+optional = true
+
+[features]
+async = ["tokio"]
+default = ["sync"]
+sync = []
diff --git a/third_party/rust/fluent-testing/LICENSE-APACHE b/third_party/rust/fluent-testing/LICENSE-APACHE
new file mode 100644
index 0000000000..35582f166b
--- /dev/null
+++ b/third_party/rust/fluent-testing/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017 Mozilla
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/third_party/rust/fluent-testing/LICENSE-MIT b/third_party/rust/fluent-testing/LICENSE-MIT
new file mode 100644
index 0000000000..5655fa311c
--- /dev/null
+++ b/third_party/rust/fluent-testing/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright 2017 Mozilla
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/rust/fluent-testing/README.md b/third_party/rust/fluent-testing/README.md
new file mode 100644
index 0000000000..9eeb65899b
--- /dev/null
+++ b/third_party/rust/fluent-testing/README.md
@@ -0,0 +1,74 @@
+# Fluent
+
+`fluent-testing` is a collection of mock scenarios for testing fluent-rs components.
+
+Project Fluent keeps simple things simple and makes complex things possible.
+The syntax used for describing translations is easy to read and understand. At
+the same time it allows, when necessary, to represent complex concepts from
+natural languages like gender, plurals, conjugations, and others.
+
+[Documentation][]
+
+[Project Fluent]: https://projectfluent.org
+[Documentation]: https://docs.rs/fluent/
+
+Status
+------
+
+The implementation is in its early stages and supports only some of the Project
+Fluent's spec. Consult the [list of milestones][] for more information about
+release planning and scope.
+
+[list of milestones]: https://github.com/projectfluent/fluent-rs/milestones
+
+
+Local Development
+-----------------
+
+ cargo build
+ cargo test
+ cargo run --example simple-fallback
+
+When submitting a PR please use [`cargo fmt`][] (nightly).
+
+[`cargo fmt`]: https://github.com/rust-lang-nursery/rustfmt
+
+
+Learn the FTL syntax
+--------------------
+
+FTL is a localization file format used for describing translation resources.
+FTL stands for _Fluent Translation List_.
+
+FTL is designed to be simple to read, but at the same time allows to represent
+complex concepts from natural languages like gender, plurals, conjugations, and
+others.
+
+ hello-user = Hello, { $username }!
+
+[Read the Fluent Syntax Guide][] in order to learn more about the syntax. If
+you're a tool author you may be interested in the formal [EBNF grammar][].
+
+[Read the Fluent Syntax Guide]: https://projectfluent.org/fluent/guide/
+[EBNF grammar]: https://github.com/projectfluent/fluent/tree/master/spec
+
+
+Get Involved
+------------
+
+`fluent-rs` is open-source, licensed under the Apache License, Version 2.0. We
+encourage everyone to take a look at our code and we'll listen to your
+feedback.
+
+
+Discuss
+-------
+
+We'd love to hear your thoughts on Project Fluent! Whether you're a localizer
+looking for a better way to express yourself in your language, or a developer
+trying to make your app localizable and multilingual, or a hacker looking for
+a project to contribute to, please do get in touch on the mailing list and the
+IRC channel.
+
+ - Discourse: https://discourse.mozilla.org/c/fluent
+ - IRC channel: [irc://irc.mozilla.org/l20n](irc://irc.mozilla.org/l20n)
diff --git a/third_party/rust/fluent-testing/resources/README.md b/third_party/rust/fluent-testing/resources/README.md
new file mode 100644
index 0000000000..e699e41653
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/README.md
@@ -0,0 +1,8 @@
+This is a capture of the state of Firefox localizations from December 28 2020.
+
+It contains `en-US` locale which is complete, and `pl` locale which contains some missing
+entries.
+
+An example missing message is in file `toolkit/updates/history.ftl` and has id `history-intro`.
+
+An example of a missing file is `browser/preview/interventions.ftl`.
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/branding/brand.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/branding/brand.ftl
new file mode 100644
index 0000000000..c86ae06eb6
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/branding/brand.ftl
@@ -0,0 +1,26 @@
+# # This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## Firefox and Mozilla Brand
+##
+## Firefox and Mozilla must be treated as a brand.
+##
+## They cannot be:
+## - Transliterated.
+## - Translated.
+##
+## Declension should be avoided where possible, leaving the original
+## brand unaltered in prominent UI positions.
+##
+## For further details, consult:
+## https://mozilla-l10n.github.io/styleguides/mozilla_general/#brands-copyright-and-trademark
+
+-brand-shorter-name = Nightly
+-brand-short-name = Nightly
+-brand-full-name = Nightly
+# This brand name can be used in messages where the product name needs to
+# remain unchanged across different versions (Nightly, Beta, etc.).
+-brand-product-name = Firefox
+-vendor-short-name = Mozilla
+trademarkInfo = { " " }
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/aboutDialog.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/aboutDialog.ftl
new file mode 100644
index 0000000000..157ab9470b
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/aboutDialog.ftl
@@ -0,0 +1,61 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+aboutDialog-title =
+ .title = About { -brand-full-name }
+
+releaseNotes-link = What’s new
+
+update-checkForUpdatesButton =
+ .label = Check for updates
+ .accesskey = C
+
+update-updateButton =
+ .label = Restart to Update { -brand-shorter-name }
+ .accesskey = R
+
+update-checkingForUpdates = Checking for updates…
+update-downloading = <img data-l10n-name="icon"/>Downloading update — <label data-l10n-name="download-status"/>
+update-applying = Applying update…
+
+update-failed = Update failed. <label data-l10n-name="failed-link">Download the latest version</label>
+update-failed-main =
+ Update failed. <a data-l10n-name="failed-link-main">Download the latest version</a>
+
+update-adminDisabled = Updates disabled by your system administrator
+update-noUpdatesFound = { -brand-short-name } is up to date
+update-otherInstanceHandlingUpdates = { -brand-short-name } is being updated by another instance
+
+update-manual = Updates available at <label data-l10n-name="manual-link"/>
+
+update-unsupported = You can not perform further updates on this system. <label data-l10n-name="unsupported-link">Learn more</label>
+
+update-restarting = Restarting…
+
+channel-description = You are currently on the <label data-l10n-name="current-channel"></label> update channel.
+
+warningDesc-version = { -brand-short-name } is experimental and may be unstable.
+
+community-exp = <label data-l10n-name="community-exp-mozillaLink">{ -vendor-short-name }</label> is a <label data-l10n-name="community-exp-creditsLink">global community</label> working together to keep the Web open, public and accessible to all.
+
+community-2 = { -brand-short-name } is designed by <label data-l10n-name="community-mozillaLink">{ -vendor-short-name }</label>, a <label data-l10n-name="community-creditsLink">global community</label> working together to keep the Web open, public and accessible to all.
+
+helpus = Want to help? <label data-l10n-name="helpus-donateLink">Make a donation</label> or <label data-l10n-name="helpus-getInvolvedLink">get involved!</label>
+
+bottomLinks-license = Licensing Information
+bottomLinks-rights = End-User Rights
+bottomLinks-privacy = Privacy Policy
+
+# Example of resulting string: 66.0.1 (64-bit)
+# Variables:
+# $version (String): version of Firefox, e.g. 66.0.1
+# $bits (Number): bits of the architecture (32 or 64)
+aboutDialog-version = { $version } ({ $bits }-bit)
+
+# Example of resulting string: 66.0a1 (2019-01-16) (64-bit)
+# Variables:
+# $version (String): version of Firefox for Nightly builds, e.g. 66.0a1
+# $isodate (String): date in ISO format, e.g. 2019-01-16
+# $bits (Number): bits of the architecture (32 or 64)
+aboutDialog-version-nightly = { $version } ({ $isodate }) ({ $bits }-bit)
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/allTabsMenu.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/allTabsMenu.ftl
new file mode 100644
index 0000000000..2c5b959af5
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/allTabsMenu.ftl
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+all-tabs-menu-undo-close-tabs =
+ .label =
+ { $tabCount ->
+ [1] Undo Close Tab
+ *[other] Undo Close Tabs
+ }
+
+# "Search" is a verb, as in "Search through tabs".
+all-tabs-menu-search-tabs =
+ .label = Search Tabs
+
+all-tabs-menu-new-user-context =
+ .label = New Container Tab
+
+all-tabs-menu-hidden-tabs =
+ .label = Hidden Tabs
+
+all-tabs-menu-manage-user-context =
+ .label = Manage Containers
+ .accesskey = o
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/appmenu.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/appmenu.ftl
new file mode 100644
index 0000000000..c99eefc08d
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/appmenu.ftl
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## App Menu
+
+appmenuitem-update-banner =
+ .label-update-downloading = Downloading { -brand-shorter-name } update
+appmenuitem-protection-dashboard-title = Protections Dashboard
+appmenuitem-customize-mode =
+ .label = Customize…
+
+## Zoom Controls
+
+appmenuitem-zoom-enlarge =
+ .label = Zoom in
+appmenuitem-zoom-reduce =
+ .label = Zoom out
+
+## Firefox Account toolbar button and Sync panel in App menu.
+
+fxa-toolbar-sync-now =
+ .label = Sync Now
+
+## What's New panel in App menu.
+
+whatsnew-panel-header = What’s New
+
+# Checkbox displayed at the bottom of the What's New panel, allowing users to
+# enable/disable What's New notifications.
+whatsnew-panel-footer-checkbox =
+ .label = Notify about new features
+ .accesskey = f
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/branding/brandings.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/branding/brandings.ftl
new file mode 100644
index 0000000000..9a03796f58
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/branding/brandings.ftl
@@ -0,0 +1,25 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## The following feature names must be treated as a brand.
+##
+## They cannot be:
+## - Transliterated.
+## - Translated.
+##
+## Declension should be avoided where possible, leaving the original
+## brand unaltered in prominent UI positions.
+##
+## For further details, consult:
+## https://mozilla-l10n.github.io/styleguides/mozilla_general/#brands-copyright-and-trademark
+
+-facebook-container-brand-name = Facebook Container
+-lockwise-brand-name = Firefox Lockwise
+-lockwise-brand-short-name = Lockwise
+-monitor-brand-name = Firefox Monitor
+-monitor-brand-short-name = Monitor
+-pocket-brand-name = Pocket
+-send-brand-name = Firefox Send
+-screenshots-brand-name = Firefox Screenshots
+-mozilla-vpn-brand-name = Mozilla VPN
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/branding/sync-brand.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/branding/sync-brand.ftl
new file mode 100644
index 0000000000..3d2e27adf9
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/branding/sync-brand.ftl
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+-sync-brand-short-name = Sync
+
+# “Sync” can be localized, “Firefox” must be treated as a brand,
+# and kept in English.
+-sync-brand-name = Firefox Sync
+
+# “Account” can be localized, “Firefox” must be treated as a brand,
+# and kept in English.
+-fxaccount-brand-name = Firefox Account
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/browser.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/browser.ftl
new file mode 100644
index 0000000000..16ab4dc9a1
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/browser.ftl
@@ -0,0 +1,593 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## The main browser window's title
+
+# These are the default window titles everywhere except macOS. The first two
+# attributes are used when the web content opened has no title:
+#
+# default - "Mozilla Firefox"
+# private - "Mozilla Firefox (Private Browsing)"
+#
+# The last two are for use when there *is* a content title.
+# Variables:
+# $content-title (String): the title of the web content.
+browser-main-window =
+ .data-title-default = { -brand-full-name }
+ .data-title-private = { -brand-full-name } (Private Browsing)
+ .data-content-title-default = { $content-title } — { -brand-full-name }
+ .data-content-title-private = { $content-title } — { -brand-full-name } (Private Browsing)
+
+# These are the default window titles on macOS. The first two are for use when
+# there is no content title:
+#
+# "default" - "Mozilla Firefox"
+# "private" - "Mozilla Firefox — (Private Browsing)"
+#
+# The last two are for use when there *is* a content title.
+# Do not use the brand name in the last two attributes, as we do on non-macOS.
+#
+# Also note the other subtle difference here: we use a `-` to separate the
+# brand name from `(Private Browsing)`, which does not happen on other OSes.
+#
+# Variables:
+# $content-title (String): the title of the web content.
+browser-main-window-mac =
+ .data-title-default = { -brand-full-name }
+ .data-title-private = { -brand-full-name } — (Private Browsing)
+ .data-content-title-default = { $content-title }
+ .data-content-title-private = { $content-title } — (Private Browsing)
+
+# This gets set as the initial title, and is overridden as soon as we start
+# updating the titlebar based on loaded tabs or private browsing state.
+# This should match the `data-title-default` attribute in both
+# `browser-main-window` and `browser-main-window-mac`.
+browser-main-window-title = { -brand-full-name }
+
+##
+
+urlbar-identity-button =
+ .aria-label = View site information
+
+## Tooltips for images appearing in the address bar
+
+urlbar-services-notification-anchor =
+ .tooltiptext = Open install message panel
+urlbar-web-notification-anchor =
+ .tooltiptext = Change whether you can receive notifications from the site
+urlbar-midi-notification-anchor =
+ .tooltiptext = Open MIDI panel
+urlbar-eme-notification-anchor =
+ .tooltiptext = Manage use of DRM software
+urlbar-web-authn-anchor =
+ .tooltiptext = Open Web Authentication panel
+urlbar-canvas-notification-anchor =
+ .tooltiptext = Manage canvas extraction permission
+urlbar-web-rtc-share-microphone-notification-anchor =
+ .tooltiptext = Manage sharing your microphone with the site
+urlbar-default-notification-anchor =
+ .tooltiptext = Open message panel
+urlbar-geolocation-notification-anchor =
+ .tooltiptext = Open location request panel
+urlbar-xr-notification-anchor =
+ .tooltiptext = Open virtual reality permission panel
+urlbar-storage-access-anchor =
+ .tooltiptext = Open browsing activity permission panel
+urlbar-translate-notification-anchor =
+ .tooltiptext = Translate this page
+urlbar-web-rtc-share-screen-notification-anchor =
+ .tooltiptext = Manage sharing your windows or screen with the site
+urlbar-indexed-db-notification-anchor =
+ .tooltiptext = Open offline storage message panel
+urlbar-password-notification-anchor =
+ .tooltiptext = Open save password message panel
+urlbar-translated-notification-anchor =
+ .tooltiptext = Manage page translation
+urlbar-plugins-notification-anchor =
+ .tooltiptext = Manage plug-in use
+urlbar-web-rtc-share-devices-notification-anchor =
+ .tooltiptext = Manage sharing your camera and/or microphone with the site
+urlbar-autoplay-notification-anchor =
+ .tooltiptext = Open autoplay panel
+urlbar-persistent-storage-notification-anchor =
+ .tooltiptext = Store data in Persistent Storage
+urlbar-addons-notification-anchor =
+ .tooltiptext = Open add-on installation message panel
+urlbar-tip-help-icon =
+ .title = Get help
+urlbar-search-tips-confirm = Okay, Got It
+# Read out before Urlbar Tip text content so screenreader users know the
+# subsequent text is a tip offered by the browser. It should end in a colon or
+# localized equivalent.
+urlbar-tip-icon-description =
+ .alt = Tip:
+
+## Prompts users to use the Urlbar when they open a new tab or visit the
+## homepage of their default search engine.
+## Variables:
+## $engineName (String): The name of the user's default search engine. e.g. "Google" or "DuckDuckGo".
+urlbar-search-tips-onboard = Type less, find more: Search { $engineName } right from your address bar.
+urlbar-search-tips-redirect-2 = Start your search in the address bar to see suggestions from { $engineName } and your browsing history.
+
+# Prompts users to use the Urlbar when they are typing in the domain of a
+# search engine, e.g. google.com or amazon.com.
+urlbar-tabtosearch-onboard = Select this shortcut to find what you need faster.
+
+## Local search mode indicator labels in the urlbar
+
+urlbar-search-mode-bookmarks = Bookmarks
+urlbar-search-mode-tabs = Tabs
+urlbar-search-mode-history = History
+
+##
+
+urlbar-geolocation-blocked =
+ .tooltiptext = You have blocked location information for this website.
+urlbar-xr-blocked =
+ .tooltiptext = You have blocked virtual reality device access for this website.
+urlbar-web-notifications-blocked =
+ .tooltiptext = You have blocked notifications for this website.
+urlbar-camera-blocked =
+ .tooltiptext = You have blocked your camera for this website.
+urlbar-microphone-blocked =
+ .tooltiptext = You have blocked your microphone for this website.
+urlbar-screen-blocked =
+ .tooltiptext = You have blocked this website from sharing your screen.
+urlbar-persistent-storage-blocked =
+ .tooltiptext = You have blocked persistent storage for this website.
+urlbar-popup-blocked =
+ .tooltiptext = You have blocked pop-ups for this website.
+urlbar-autoplay-media-blocked =
+ .tooltiptext = You have blocked autoplay media with sound for this website.
+urlbar-canvas-blocked =
+ .tooltiptext = You have blocked canvas data extraction for this website.
+urlbar-midi-blocked =
+ .tooltiptext = You have blocked MIDI access for this website.
+urlbar-install-blocked =
+ .tooltiptext = You have blocked add-on installation for this website.
+
+# Variables
+# $shortcut (String) - A keyboard shortcut for the edit bookmark command.
+urlbar-star-edit-bookmark =
+ .tooltiptext = Edit this bookmark ({ $shortcut })
+
+# Variables
+# $shortcut (String) - A keyboard shortcut for the add bookmark command.
+urlbar-star-add-bookmark =
+ .tooltiptext = Bookmark this page ({ $shortcut })
+
+## Page Action Context Menu
+
+page-action-add-to-urlbar =
+ .label = Add to Address Bar
+page-action-manage-extension =
+ .label = Manage Extension…
+page-action-remove-from-urlbar =
+ .label = Remove from Address Bar
+page-action-remove-extension =
+ .label = Remove Extension
+
+## Page Action menu
+
+# Variables
+# $tabCount (integer) - Number of tabs selected
+page-action-send-tabs-panel =
+ .label = { $tabCount ->
+ [1] Send Tab to Device
+ *[other] Send { $tabCount } Tabs to Device
+ }
+page-action-send-tabs-urlbar =
+ .tooltiptext = { $tabCount ->
+ [1] Send Tab to Device
+ *[other] Send { $tabCount } Tabs to Device
+ }
+page-action-pocket-panel =
+ .label = Save Page to { -pocket-brand-name }
+page-action-copy-url-panel =
+ .label = Copy Link
+page-action-copy-url-urlbar =
+ .tooltiptext = Copy Link
+page-action-email-link-panel =
+ .label = Email Link…
+page-action-email-link-urlbar =
+ .tooltiptext = Email Link…
+page-action-share-url-panel =
+ .label = Share
+page-action-share-url-urlbar =
+ .tooltiptext = Share
+page-action-share-more-panel =
+ .label = More…
+page-action-send-tab-not-ready =
+ .label = Syncing Devices…
+# "Pin" is being used as a metaphor for expressing the fact that these tabs
+# are "pinned" to the left edge of the tabstrip. Really we just want the
+# string to express the idea that this is a lightweight and reversible
+# action that keeps your tab where you can reach it easily.
+page-action-pin-tab-panel =
+ .label = Pin Tab
+page-action-pin-tab-urlbar =
+ .tooltiptext = Pin Tab
+page-action-unpin-tab-panel =
+ .label = Unpin Tab
+page-action-unpin-tab-urlbar =
+ .tooltiptext = Unpin Tab
+
+## Auto-hide Context Menu
+
+full-screen-autohide =
+ .label = Hide Toolbars
+ .accesskey = H
+full-screen-exit =
+ .label = Exit Full Screen Mode
+ .accesskey = F
+
+## Search Engine selection buttons (one-offs)
+
+# This string prompts the user to use the list of search shortcuts in
+# the Urlbar and searchbar.
+search-one-offs-with-title = This time, search with:
+
+# This string won't wrap, so if the translated string is longer,
+# consider translating it as if it said only "Search Settings".
+search-one-offs-change-settings-button =
+ .label = Change Search Settings
+search-one-offs-change-settings-compact-button =
+ .tooltiptext = Change search settings
+
+search-one-offs-context-open-new-tab =
+ .label = Search in New Tab
+ .accesskey = T
+search-one-offs-context-set-as-default =
+ .label = Set as Default Search Engine
+ .accesskey = D
+search-one-offs-context-set-as-default-private =
+ .label = Set as Default Search Engine for Private Windows
+ .accesskey = P
+
+# Search engine one-off buttons with an @alias shortcut/keyword.
+# Variables:
+# $engineName (String): The name of the engine.
+# $alias (String): The @alias shortcut/keyword.
+search-one-offs-engine-with-alias =
+ .tooltiptext = { $engineName } ({ $alias })
+
+## Local search mode one-off buttons
+## Variables:
+## $restrict (String): The restriction token corresponding to the search mode.
+## Restriction tokens are special characters users can type in the urlbar to
+## restrict their searches to certain sources (e.g., "*" to search only
+## bookmarks).
+
+search-one-offs-bookmarks =
+ .tooltiptext = Bookmarks ({ $restrict })
+search-one-offs-tabs =
+ .tooltiptext = Tabs ({ $restrict })
+search-one-offs-history =
+ .tooltiptext = History ({ $restrict })
+
+## Bookmark Panel
+
+bookmark-panel-show-editor-checkbox =
+ .label = Show editor when saving
+ .accesskey = S
+
+bookmark-panel-done-button =
+ .label = Done
+
+# Width of the bookmark panel.
+# Should be large enough to fully display the Done and
+# Cancel/Remove Bookmark buttons.
+bookmark-panel =
+ .style = min-width: 23em
+
+## Identity Panel
+
+identity-connection-not-secure = Connection not secure
+identity-connection-secure = Connection secure
+identity-connection-internal = This is a secure { -brand-short-name } page.
+identity-connection-file = This page is stored on your computer.
+identity-extension-page = This page is loaded from an extension.
+identity-active-blocked = { -brand-short-name } has blocked parts of this page that are not secure.
+identity-custom-root = Connection verified by a certificate issuer that is not recognized by Mozilla.
+identity-passive-loaded = Parts of this page are not secure (such as images).
+identity-active-loaded = You have disabled protection on this page.
+identity-weak-encryption = This page uses weak encryption.
+identity-insecure-login-forms = Logins entered on this page could be compromised.
+
+identity-https-only-connection-upgraded = (upgraded to HTTPS)
+identity-https-only-label = HTTPS-Only Mode
+identity-https-only-dropdown-on =
+ .label = On
+identity-https-only-dropdown-off =
+ .label = Off
+identity-https-only-dropdown-off-temporarily =
+ .label = Off temporarily
+identity-https-only-info-turn-on2 = Turn on HTTPS-Only Mode for this site if you want { -brand-short-name } to upgrade the connection when possible.
+identity-https-only-info-turn-off2 = If the page seems broken, you may want to turn off HTTPS-Only Mode for this site to reload using insecure HTTP.
+identity-https-only-info-no-upgrade = Unable to upgrade connection from HTTP.
+
+identity-permissions =
+ .value = Permissions
+identity-permissions-storage-access-header = Cross-site cookies
+identity-permissions-storage-access-hint = These parties can use cross-site cookies and site data while you are on this site.
+
+identity-permissions-reload-hint = You may need to reload the page for changes to apply.
+identity-permissions-empty = You have not granted this site any special permissions.
+identity-clear-site-data =
+ .label = Clear Cookies and Site Data…
+identity-connection-not-secure-security-view = You are not securely connected to this site.
+identity-connection-verified = You are securely connected to this site.
+identity-ev-owner-label = Certificate issued to:
+identity-description-custom-root = Mozilla does not recognize this certificate issuer. It may have been added from your operating system or by an administrator. <label data-l10n-name="link">Learn More</label>
+identity-remove-cert-exception =
+ .label = Remove Exception
+ .accesskey = R
+identity-description-insecure = Your connection to this site is not private. Information you submit could be viewed by others (like passwords, messages, credit cards, etc.).
+identity-description-insecure-login-forms = The login information you enter on this page is not secure and could be compromised.
+identity-description-weak-cipher-intro = Your connection to this website uses weak encryption and is not private.
+identity-description-weak-cipher-risk = Other people can view your information or modify the website’s behavior.
+identity-description-active-blocked = { -brand-short-name } has blocked parts of this page that are not secure. <label data-l10n-name="link">Learn More</label>
+identity-description-passive-loaded = Your connection is not private and information you share with the site could be viewed by others.
+identity-description-passive-loaded-insecure = This website contains content that is not secure (such as images). <label data-l10n-name="link">Learn More</label>
+identity-description-passive-loaded-mixed = Although { -brand-short-name } has blocked some content, there is still content on the page that is not secure (such as images). <label data-l10n-name="link">Learn More</label>
+identity-description-active-loaded = This website contains content that is not secure (such as scripts) and your connection to it is not private.
+identity-description-active-loaded-insecure = Information you share with this site could be viewed by others (like passwords, messages, credit cards, etc.).
+identity-learn-more =
+ .value = Learn More
+identity-disable-mixed-content-blocking =
+ .label = Disable protection for now
+ .accesskey = D
+identity-enable-mixed-content-blocking =
+ .label = Enable protection
+ .accesskey = E
+identity-more-info-link-text =
+ .label = More Information
+
+## Window controls
+
+browser-window-minimize-button =
+ .tooltiptext = Minimize
+browser-window-maximize-button =
+ .tooltiptext = Maximize
+browser-window-restore-down-button =
+ .tooltiptext = Restore Down
+browser-window-close-button =
+ .tooltiptext = Close
+
+## Bookmarks toolbar items
+
+browser-import-button2 =
+ .label = Import bookmarks…
+ .tooltiptext = Import bookmarks from another browser to { -brand-short-name }.
+
+bookmarks-toolbar-empty-message = For quick access, place your bookmarks here on the bookmarks toolbar. <a data-l10n-name="manage-bookmarks">Manage bookmarks…</a>
+
+## WebRTC Pop-up notifications
+
+popup-select-camera =
+ .value = Camera to share:
+ .accesskey = C
+popup-select-microphone =
+ .value = Microphone to share:
+ .accesskey = M
+popup-all-windows-shared = All visible windows on your screen will be shared.
+
+popup-screen-sharing-not-now =
+ .label = Not Now
+ .accesskey = w
+
+popup-screen-sharing-never =
+ .label = Never Allow
+ .accesskey = N
+
+popup-silence-notifications-checkbox = Disable notifications from { -brand-short-name } while sharing
+popup-silence-notifications-checkbox-warning = { -brand-short-name } will not display notifications while you are sharing.
+
+## WebRTC window or screen share tab switch warning
+
+sharing-warning-window = You are sharing { -brand-short-name }. Other people can see when you switch to a new tab.
+sharing-warning-screen = You are sharing your entire screen. Other people can see when you switch to a new tab.
+sharing-warning-proceed-to-tab =
+ .label = Proceed to Tab
+sharing-warning-disable-for-session =
+ .label = Disable sharing protection for this session
+
+## DevTools F12 popup
+
+enable-devtools-popup-description = To use the F12 shortcut, first open DevTools via the Web Developer menu.
+
+
+## URL Bar
+
+# This placeholder is used when not in search mode and the user's default search
+# engine is unknown.
+urlbar-placeholder =
+ .placeholder = Search or enter address
+
+# This placeholder is used in search mode with search engines that search the
+# entire web.
+# Variables
+# $name (String): the name of a search engine that searches the entire Web
+# (e.g. Google).
+urlbar-placeholder-search-mode-web-2 =
+ .placeholder = Search the Web
+ .aria-label = Search with { $name }
+
+# This placeholder is used in search mode with search engines that search a
+# specific site (e.g., Amazon).
+# Variables
+# $name (String): the name of a search engine that searches a specific site
+# (e.g. Amazon).
+urlbar-placeholder-search-mode-other-engine =
+ .placeholder = Enter search terms
+ .aria-label = Search { $name }
+
+# This placeholder is used when searching bookmarks.
+urlbar-placeholder-search-mode-other-bookmarks =
+ .placeholder = Enter search terms
+ .aria-label = Search bookmarks
+
+# This placeholder is used when searching history.
+urlbar-placeholder-search-mode-other-history =
+ .placeholder = Enter search terms
+ .aria-label = Search history
+
+# This placeholder is used when searching open tabs.
+urlbar-placeholder-search-mode-other-tabs =
+ .placeholder = Enter search terms
+ .aria-label = Search tabs
+
+# Variables
+# $name (String): the name of the user's default search engine
+urlbar-placeholder-with-name =
+ .placeholder = Search with { $name } or enter address
+urlbar-remote-control-notification-anchor =
+ .tooltiptext = Browser is under remote control
+urlbar-permissions-granted =
+ .tooltiptext = You have granted this website additional permissions.
+urlbar-switch-to-tab =
+ .value = Switch to tab:
+
+# Used to indicate that a selected autocomplete entry is provided by an extension.
+urlbar-extension =
+ .value = Extension:
+
+urlbar-go-button =
+ .tooltiptext = Go to the address in the Location Bar
+urlbar-page-action-button =
+ .tooltiptext = Page actions
+urlbar-pocket-button =
+ .tooltiptext = Save to { -pocket-brand-name }
+
+## Action text shown in urlbar results, usually appended after the search
+## string or the url, like "result value - action text".
+
+# Used when the private browsing engine differs from the default engine.
+# The "with" format was chosen because the search engine name can end with
+# "Search", and we would like to avoid strings like "Search MSN Search".
+# Variables
+# $engine (String): the name of a search engine
+urlbar-result-action-search-in-private-w-engine = Search with { $engine } in a Private Window
+# Used when the private browsing engine is the same as the default engine.
+urlbar-result-action-search-in-private = Search in a Private Window
+# The "with" format was chosen because the search engine name can end with
+# "Search", and we would like to avoid strings like "Search MSN Search".
+# Variables
+# $engine (String): the name of a search engine
+urlbar-result-action-search-w-engine = Search with { $engine }
+urlbar-result-action-sponsored = Sponsored
+urlbar-result-action-switch-tab = Switch to Tab
+urlbar-result-action-visit = Visit
+# Directs a user to press the Tab key to perform a search with the specified
+# engine.
+# Variables
+# $engine (String): the name of a search engine that searches the entire Web
+# (e.g. Google).
+urlbar-result-action-before-tabtosearch-web = Press Tab to search with { $engine }
+# Directs a user to press the Tab key to perform a search with the specified
+# engine.
+# Variables
+# $engine (String): the name of a search engine that searches a specific site
+# (e.g. Amazon).
+urlbar-result-action-before-tabtosearch-other = Press Tab to search { $engine }
+# Variables
+# $engine (String): the name of a search engine that searches the entire Web
+# (e.g. Google).
+urlbar-result-action-tabtosearch-web = Search with { $engine } directly from the address bar
+# Variables
+# $engine (String): the name of a search engine that searches a specific site
+# (e.g. Amazon).
+urlbar-result-action-tabtosearch-other-engine = Search { $engine } directly from the address bar
+
+## Action text shown in urlbar results, usually appended after the search
+## string or the url, like "result value - action text".
+## In these actions "Search" is a verb, followed by where the search is performed.
+
+urlbar-result-action-search-bookmarks = Search Bookmarks
+urlbar-result-action-search-history = Search History
+urlbar-result-action-search-tabs = Search Tabs
+
+## Full Screen and Pointer Lock UI
+
+# Please ensure that the domain stays in the `<span data-l10n-name="domain">` markup.
+# Variables
+# $domain (String): the domain that is full screen, e.g. "mozilla.org"
+fullscreen-warning-domain = <span data-l10n-name="domain">{ $domain }</span> is now full screen
+fullscreen-warning-no-domain = This document is now full screen
+
+
+fullscreen-exit-button = Exit Full Screen (Esc)
+# "esc" is lowercase on mac keyboards, but uppercase elsewhere.
+fullscreen-exit-mac-button = Exit Full Screen (esc)
+
+# Please ensure that the domain stays in the `<span data-l10n-name="domain">` markup.
+# Variables
+# $domain (String): the domain that is using pointer-lock, e.g. "mozilla.org"
+pointerlock-warning-domain = <span data-l10n-name="domain">{ $domain }</span> has control of your pointer. Press Esc to take back control.
+pointerlock-warning-no-domain = This document has control of your pointer. Press Esc to take back control.
+
+## Bookmarks panels, menus and toolbar
+
+bookmarks-show-all-bookmarks =
+ .label = Show All Bookmarks
+bookmarks-recent-bookmarks =
+ .value = Recently Bookmarked
+bookmarks-toolbar-chevron =
+ .tooltiptext = Show more bookmarks
+bookmarks-sidebar-content =
+ .aria-label = Bookmarks
+bookmarks-menu-button =
+ .label = Bookmarks Menu
+bookmarks-other-bookmarks-menu =
+ .label = Other Bookmarks
+bookmarks-mobile-bookmarks-menu =
+ .label = Mobile Bookmarks
+bookmarks-tools-sidebar-visibility =
+ .label = { $isVisible ->
+ [true] Hide Bookmarks Sidebar
+ *[other] View Bookmarks Sidebar
+ }
+bookmarks-tools-toolbar-visibility =
+ .label = { $isVisible ->
+ [true] Hide Bookmarks Toolbar
+ *[other] View Bookmarks Toolbar
+ }
+bookmarks-tools-menu-button-visibility =
+ .label = { $isVisible ->
+ [true] Remove Bookmarks Menu from Toolbar
+ *[other] Add Bookmarks Menu to Toolbar
+ }
+bookmarks-search =
+ .label = Search Bookmarks
+bookmarks-tools =
+ .label = Bookmarking Tools
+
+# The aria-label is a spoken label that should not include the word "toolbar" or
+# such, because screen readers already know that this container is a toolbar.
+# This avoids double-speaking.
+bookmarks-toolbar =
+ .toolbarname = Bookmarks Toolbar
+ .accesskey = B
+ .aria-label = Bookmarks
+bookmarks-toolbar-menu =
+ .label = Bookmarks Toolbar
+bookmarks-toolbar-placeholder =
+ .title = Bookmarks Toolbar Items
+bookmarks-toolbar-placeholder-button =
+ .label = Bookmarks Toolbar Items
+
+## Library Panel items
+
+library-bookmarks-menu =
+ .label = Bookmarks
+library-bookmarks-bookmark-this-page =
+ .label = Bookmark This Page
+library-bookmarks-bookmark-edit =
+ .label = Edit This Bookmark
+
+## More items
+
+more-menu-go-offline =
+ .label = Work Offline
+ .accesskey = k
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/browserContext.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/browserContext.ftl
new file mode 100644
index 0000000000..bf1728615d
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/browserContext.ftl
@@ -0,0 +1,393 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+navbar-tooltip-instruction =
+ .value = { PLATFORM() ->
+ [macos] Pull down to show history
+ *[other] Right-click or pull down to show history
+ }
+
+## Back
+
+main-context-menu-back =
+ .tooltiptext = Go back one page
+ .aria-label = Back
+ .accesskey = B
+
+navbar-tooltip-back =
+ .value = { main-context-menu-back.tooltiptext }
+
+toolbar-button-back =
+ .label = { main-context-menu-back.aria-label }
+
+## Forward
+
+main-context-menu-forward =
+ .tooltiptext = Go forward one page
+ .aria-label = Forward
+ .accesskey = F
+
+navbar-tooltip-forward =
+ .value = { main-context-menu-forward.tooltiptext }
+
+toolbar-button-forward =
+ .label = { main-context-menu-forward.aria-label }
+
+## Reload
+
+main-context-menu-reload =
+ .aria-label = Reload
+ .accesskey = R
+
+toolbar-button-reload =
+ .label = { main-context-menu-reload.aria-label }
+
+## Stop
+
+main-context-menu-stop =
+ .aria-label = Stop
+ .accesskey = S
+
+toolbar-button-stop =
+ .label = { main-context-menu-stop.aria-label }
+
+## Stop-Reload Button
+
+toolbar-button-stop-reload =
+ .title = { main-context-menu-reload.aria-label }
+
+## Save Page
+
+main-context-menu-page-save =
+ .label = Save Page As…
+ .accesskey = P
+
+toolbar-button-page-save =
+ .label = { main-context-menu-page-save.label }
+
+## Simple menu items
+
+main-context-menu-bookmark-add =
+ .aria-label = Bookmark This Page
+ .accesskey = m
+ .tooltiptext = Bookmark this page
+
+# Variables
+# $shortcut (String) - A keyboard shortcut for the add bookmark command.
+main-context-menu-bookmark-add-with-shortcut =
+ .aria-label = Bookmark This Page
+ .accesskey = m
+ .tooltiptext = Bookmark this page ({ $shortcut })
+
+main-context-menu-bookmark-change =
+ .aria-label = Edit This Bookmark
+ .accesskey = m
+ .tooltiptext = Edit this bookmark
+
+# Variables
+# $shortcut (String) - A keyboard shortcut for the edit bookmark command.
+main-context-menu-bookmark-change-with-shortcut =
+ .aria-label = Edit This Bookmark
+ .accesskey = m
+ .tooltiptext = Edit this bookmark ({ $shortcut })
+
+main-context-menu-open-link =
+ .label = Open Link
+ .accesskey = O
+
+main-context-menu-open-link-new-tab =
+ .label = Open Link in New Tab
+ .accesskey = T
+
+main-context-menu-open-link-container-tab =
+ .label = Open Link in New Container Tab
+ .accesskey = b
+
+main-context-menu-open-link-new-window =
+ .label = Open Link in New Window
+ .accesskey = W
+
+main-context-menu-open-link-new-private-window =
+ .label = Open Link in New Private Window
+ .accesskey = P
+
+main-context-menu-bookmark-this-link =
+ .label = Bookmark This Link
+ .accesskey = L
+
+main-context-menu-save-link =
+ .label = Save Link As…
+ .accesskey = k
+
+main-context-menu-save-link-to-pocket =
+ .label = Save Link to { -pocket-brand-name }
+ .accesskey = o
+
+## The access keys for "Copy Link Location" and "Copy Email Address"
+## should be the same if possible; the two context menu items
+## are mutually exclusive.
+
+main-context-menu-copy-email =
+ .label = Copy Email Address
+ .accesskey = A
+
+main-context-menu-copy-link =
+ .label = Copy Link Location
+ .accesskey = a
+
+## Media (video/audio) controls
+##
+## The accesskey for "Play" and "Pause" are the
+## same because the two context-menu items are
+## mutually exclusive.
+
+main-context-menu-media-play =
+ .label = Play
+ .accesskey = P
+
+main-context-menu-media-pause =
+ .label = Pause
+ .accesskey = P
+
+##
+
+main-context-menu-media-mute =
+ .label = Mute
+ .accesskey = M
+
+main-context-menu-media-unmute =
+ .label = Unmute
+ .accesskey = m
+
+main-context-menu-media-play-speed =
+ .label = Play Speed
+ .accesskey = d
+
+main-context-menu-media-play-speed-slow =
+ .label = Slow (0.5×)
+ .accesskey = S
+
+main-context-menu-media-play-speed-normal =
+ .label = Normal
+ .accesskey = N
+
+main-context-menu-media-play-speed-fast =
+ .label = Fast (1.25×)
+ .accesskey = F
+
+main-context-menu-media-play-speed-faster =
+ .label = Faster (1.5×)
+ .accesskey = a
+
+# "Ludicrous" is a reference to the movie "Space Balls" and is meant
+# to say that this speed is very fast.
+main-context-menu-media-play-speed-fastest =
+ .label = Ludicrous (2×)
+ .accesskey = L
+
+main-context-menu-media-loop =
+ .label = Loop
+ .accesskey = L
+
+## The access keys for "Show Controls" and "Hide Controls" are the same
+## because the two context-menu items are mutually exclusive.
+
+main-context-menu-media-show-controls =
+ .label = Show Controls
+ .accesskey = C
+
+main-context-menu-media-hide-controls =
+ .label = Hide Controls
+ .accesskey = C
+
+##
+
+main-context-menu-media-video-fullscreen =
+ .label = Full Screen
+ .accesskey = F
+
+main-context-menu-media-video-leave-fullscreen =
+ .label = Exit Full Screen
+ .accesskey = u
+
+# This is used when right-clicking on a video in the
+# content area when the Picture-in-Picture feature is enabled.
+main-context-menu-media-pip =
+ .label = Picture-in-Picture
+ .accesskey = u
+
+main-context-menu-image-reload =
+ .label = Reload Image
+ .accesskey = R
+
+main-context-menu-image-view =
+ .label = View Image
+ .accesskey = I
+
+main-context-menu-video-view =
+ .label = View Video
+ .accesskey = i
+
+main-context-menu-image-copy =
+ .label = Copy Image
+ .accesskey = y
+
+main-context-menu-image-copy-location =
+ .label = Copy Image Location
+ .accesskey = o
+
+main-context-menu-video-copy-location =
+ .label = Copy Video Location
+ .accesskey = o
+
+main-context-menu-audio-copy-location =
+ .label = Copy Audio Location
+ .accesskey = o
+
+main-context-menu-image-save-as =
+ .label = Save Image As…
+ .accesskey = v
+
+main-context-menu-image-email =
+ .label = Email Image…
+ .accesskey = g
+
+main-context-menu-image-set-as-background =
+ .label = Set As Desktop Background…
+ .accesskey = S
+
+main-context-menu-image-info =
+ .label = View Image Info
+ .accesskey = f
+
+main-context-menu-image-desc =
+ .label = View Description
+ .accesskey = D
+
+main-context-menu-video-save-as =
+ .label = Save Video As…
+ .accesskey = v
+
+main-context-menu-audio-save-as =
+ .label = Save Audio As…
+ .accesskey = v
+
+main-context-menu-video-image-save-as =
+ .label = Save Snapshot As…
+ .accesskey = S
+
+main-context-menu-video-email =
+ .label = Email Video…
+ .accesskey = a
+
+main-context-menu-audio-email =
+ .label = Email Audio…
+ .accesskey = a
+
+main-context-menu-plugin-play =
+ .label = Activate this plugin
+ .accesskey = c
+
+main-context-menu-plugin-hide =
+ .label = Hide this plugin
+ .accesskey = H
+
+main-context-menu-save-to-pocket =
+ .label = Save Page to { -pocket-brand-name }
+ .accesskey = k
+
+main-context-menu-send-to-device =
+ .label = Send Page to Device
+ .accesskey = n
+
+main-context-menu-view-background-image =
+ .label = View Background Image
+ .accesskey = w
+
+main-context-menu-generate-new-password =
+ .label = Use Generated Password…
+ .accesskey = G
+
+main-context-menu-keyword =
+ .label = Add a Keyword for this Search…
+ .accesskey = K
+
+main-context-menu-link-send-to-device =
+ .label = Send Link to Device
+ .accesskey = n
+
+main-context-menu-frame =
+ .label = This Frame
+ .accesskey = h
+
+main-context-menu-frame-show-this =
+ .label = Show Only This Frame
+ .accesskey = S
+
+main-context-menu-frame-open-tab =
+ .label = Open Frame in New Tab
+ .accesskey = T
+
+main-context-menu-frame-open-window =
+ .label = Open Frame in New Window
+ .accesskey = W
+
+main-context-menu-frame-reload =
+ .label = Reload Frame
+ .accesskey = R
+
+main-context-menu-frame-bookmark =
+ .label = Bookmark This Frame
+ .accesskey = m
+
+main-context-menu-frame-save-as =
+ .label = Save Frame As…
+ .accesskey = F
+
+main-context-menu-frame-print =
+ .label = Print Frame…
+ .accesskey = P
+
+main-context-menu-frame-view-source =
+ .label = View Frame Source
+ .accesskey = V
+
+main-context-menu-frame-view-info =
+ .label = View Frame Info
+ .accesskey = I
+
+main-context-menu-print-selection =
+ .label = Print Selection
+ .accesskey = r
+
+main-context-menu-view-selection-source =
+ .label = View Selection Source
+ .accesskey = e
+
+main-context-menu-view-page-source =
+ .label = View Page Source
+ .accesskey = V
+
+main-context-menu-view-page-info =
+ .label = View Page Info
+ .accesskey = I
+
+main-context-menu-bidi-switch-text =
+ .label = Switch Text Direction
+ .accesskey = w
+
+main-context-menu-bidi-switch-page =
+ .label = Switch Page Direction
+ .accesskey = D
+
+main-context-menu-inspect-element =
+ .label = Inspect Element
+ .accesskey = Q
+
+main-context-menu-inspect-a11y-properties =
+ .label = Inspect Accessibility Properties
+
+main-context-menu-eme-learn-more =
+ .label = Learn more about DRM…
+ .accesskey = D
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/browserSets.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/browserSets.ftl
new file mode 100644
index 0000000000..42cce2cab9
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/browserSets.ftl
@@ -0,0 +1,215 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+window-minimize-command =
+ .label = Minimize
+
+window-zoom-command =
+ .label = Zoom
+
+window-new-shortcut =
+ .key = N
+
+window-minimize-shortcut =
+ .key = M
+
+close-shortcut =
+ .key = W
+
+tab-new-shortcut =
+ .key = T
+
+location-open-shortcut =
+ .key = L
+
+location-open-shortcut-alt =
+ .key = D
+
+search-focus-shortcut =
+ .key = K
+
+# This shortcut is used in two contexts:
+# - web search
+# - find in page
+find-shortcut =
+ .key = F
+
+search-find-again-shortcut =
+ .key = G
+
+search-find-again-shortcut-alt =
+ .keycode = VK_F3
+
+search-find-selection-shortcut =
+ .key = E
+
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+search-focus-shortcut-alt =
+ .key = { PLATFORM() ->
+ [linux] J
+ *[other] E
+ }
+
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+downloads-shortcut =
+ .key = { PLATFORM() ->
+ [linux] Y
+ *[other] J
+ }
+
+addons-shortcut =
+ .key = A
+
+file-open-shortcut =
+ .key = O
+
+save-page-shortcut =
+ .key = S
+
+page-source-shortcut =
+ .key = U
+
+# This should match the Option+Command keyboard shortcut letter that Safari
+# and Chrome use for "View Source" on macOS. `page-source-shortcut` above
+# is Firefox's official keyboard shortcut shown in the GUI.
+# Safari variant is an alias provided for the convenience of Safari and Chrome
+# users on macOS. See bug 1398988.
+page-source-shortcut-safari =
+ .key = U
+
+page-info-shortcut =
+ .key = I
+
+print-shortcut =
+ .key = P
+
+mute-toggle-shortcut =
+ .key = M
+
+nav-back-shortcut-alt =
+ .key = [
+
+nav-fwd-shortcut-alt =
+ .key = ]
+
+nav-reload-shortcut =
+ .key = R
+
+# Shortcut available only on macOS.
+nav-stop-shortcut =
+ .key = .
+
+history-show-all-shortcut =
+ .key = H
+
+history-show-all-shortcut-mac =
+ .key = Y
+
+history-sidebar-shortcut =
+ .key = H
+
+full-screen-shortcut =
+ .key = F
+
+reader-mode-toggle-shortcut-windows =
+ .keycode = VK_F9
+
+reader-mode-toggle-shortcut-other =
+ .key = R
+
+picture-in-picture-toggle-shortcut-mac =
+ .key = ]
+
+# Pick the key that is commonly present
+# in your locale keyboards above the
+# `picture-in-picture-toggle-shortcut-mac` key.
+picture-in-picture-toggle-shortcut-mac-alt =
+ .key = {"}"}
+
+picture-in-picture-toggle-shortcut =
+ .key = ]
+
+# Pick the key that is commonly present
+# in your locale keyboards above the
+# `picture-in-picture-toggle-shortcut` key.
+picture-in-picture-toggle-shortcut-alt =
+ .key = {"}"}
+
+bookmark-this-page-shortcut =
+ .key = D
+
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+bookmark-show-library-shortcut =
+ .key = O
+
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+bookmark-show-sidebar-shortcut =
+ .key = B
+
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+bookmark-show-toolbar-shortcut =
+ .key = B
+
+## All `-alt*` messages are alternative acceleration keys for zoom.
+## If shift key is needed with your locale popular keyboard for them,
+## you can use these alternative items. Otherwise, their values should be empty.
+
+full-zoom-reduce-shortcut =
+ .key = -
+
+full-zoom-reduce-shortcut-alt-a =
+ .key = _
+
+full-zoom-reduce-shortcut-alt-b =
+ .key = {""}
+
+full-zoom-enlarge-shortcut =
+ .key = +
+
+full-zoom-enlarge-shortcut-alt =
+ .key = =
+
+full-zoom-enlarge-shortcut-alt2 =
+ .key = {""}
+
+full-zoom-reset-shortcut =
+ .key = 0
+
+full-zoom-reset-shortcut-alt =
+ .key = {""}
+##
+
+bidi-switch-direction-shortcut =
+ .key = X
+
+private-browsing-shortcut =
+ .key = P
+
+## The shortcuts below are for Mac specific
+## global menu.
+
+quit-app-shortcut =
+ .key = Q
+
+help-shortcut =
+ .key = ?
+
+preferences-shortcut =
+ .key = ,
+
+hide-app-shortcut =
+ .key = H
+
+hide-other-apps-shortcut =
+ .key = H
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/downloads.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/downloads.ftl
new file mode 100644
index 0000000000..6ed10d0be1
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/downloads.ftl
@@ -0,0 +1,163 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+## The title and aria-label attributes are used by screen readers to describe
+## the Downloads Panel.
+
+downloads-window =
+ .title = Downloads
+downloads-panel =
+ .aria-label = Downloads
+
+##
+
+# The style attribute has the width of the Downloads Panel expressed using
+# a CSS unit. The longest labels that should fit are usually those of
+# in-progress and blocked downloads.
+downloads-panel-list =
+ .style = width: 70ch
+
+downloads-cmd-pause =
+ .label = Pause
+ .accesskey = P
+downloads-cmd-resume =
+ .label = Resume
+ .accesskey = R
+downloads-cmd-cancel =
+ .tooltiptext = Cancel
+downloads-cmd-cancel-panel =
+ .aria-label = Cancel
+
+# This message is only displayed on Windows and Linux devices
+downloads-cmd-show-menuitem =
+ .label = Open Containing Folder
+ .accesskey = F
+
+# This message is only displayed on macOS devices
+downloads-cmd-show-menuitem-mac =
+ .label = Show In Finder
+ .accesskey = F
+
+downloads-cmd-use-system-default =
+ .label = Open In System Viewer
+ .accesskey = V
+
+downloads-cmd-always-use-system-default =
+ .label = Always Open In System Viewer
+ .accesskey = w
+
+downloads-cmd-show-button =
+ .tooltiptext = { PLATFORM() ->
+ [macos] Show In Finder
+ *[other] Open Containing Folder
+ }
+
+downloads-cmd-show-panel =
+ .aria-label = { PLATFORM() ->
+ [macos] Show In Finder
+ *[other] Open Containing Folder
+ }
+downloads-cmd-show-description =
+ .value = { PLATFORM() ->
+ [macos] Show In Finder
+ *[other] Open Containing Folder
+ }
+
+downloads-cmd-show-downloads =
+ .label = Show Downloads Folder
+downloads-cmd-retry =
+ .tooltiptext = Retry
+downloads-cmd-retry-panel =
+ .aria-label = Retry
+downloads-cmd-go-to-download-page =
+ .label = Go To Download Page
+ .accesskey = G
+downloads-cmd-copy-download-link =
+ .label = Copy Download Link
+ .accesskey = L
+downloads-cmd-remove-from-history =
+ .label = Remove From History
+ .accesskey = e
+downloads-cmd-clear-list =
+ .label = Clear Preview Panel
+ .accesskey = a
+downloads-cmd-clear-downloads =
+ .label = Clear Downloads
+ .accesskey = D
+
+# This command is shown in the context menu when downloads are blocked.
+downloads-cmd-unblock =
+ .label = Allow Download
+ .accesskey = o
+
+# This is the tooltip of the action button shown when malware is blocked.
+downloads-cmd-remove-file =
+ .tooltiptext = Remove File
+
+downloads-cmd-remove-file-panel =
+ .aria-label = Remove File
+
+# This is the tooltip of the action button shown when potentially unwanted
+# downloads are blocked. This opens a dialog where the user can choose
+# whether to unblock or remove the download. Removing is the default option.
+downloads-cmd-choose-unblock =
+ .tooltiptext = Remove File or Allow Download
+
+downloads-cmd-choose-unblock-panel =
+ .aria-label = Remove File or Allow Download
+
+# This is the tooltip of the action button shown when uncommon downloads are
+# blocked.This opens a dialog where the user can choose whether to open the
+# file or remove the download. Opening is the default option.
+downloads-cmd-choose-open =
+ .tooltiptext = Open or Remove File
+
+downloads-cmd-choose-open-panel =
+ .aria-label = Open or Remove File
+
+# Displayed when hovering a blocked download, indicates that it's possible to
+# show more information for user to take the next action.
+downloads-show-more-information =
+ .value = Show more information
+
+# Displayed when hovering a complete download, indicates that it's possible to
+# open the file using an app available in the system.
+downloads-open-file =
+ .value = Open File
+
+# Displayed when hovering a download which is able to be retried by users,
+# indicates that it's possible to download this file again.
+downloads-retry-download =
+ .value = Retry Download
+
+# Displayed when hovering a download which is able to be cancelled by users,
+# indicates that it's possible to cancel and stop the download.
+downloads-cancel-download =
+ .value = Cancel Download
+
+# This string is shown at the bottom of the Downloads Panel when all the
+# downloads fit in the available space, or when there are no downloads in
+# the panel at all.
+downloads-history =
+ .label = Show All Downloads
+ .accesskey = S
+
+# This string is shown at the top of the Download Details Panel, to indicate
+# that we are showing the details of a single download.
+downloads-details =
+ .title = Download Details
+
+downloads-clear-downloads-button =
+ .label = Clear Downloads
+ .tooltiptext = Clears completed, canceled and failed downloads
+
+# This string is shown when there are no items in the Downloads view, when it
+# is displayed inside a browser tab.
+downloads-list-empty =
+ .value = There are no downloads.
+
+# This string is shown when there are no items in the Downloads Panel.
+downloads-panel-empty =
+ .value = No downloads for this session.
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/menubar.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/menubar.ftl
new file mode 100644
index 0000000000..b24af086eb
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/menubar.ftl
@@ -0,0 +1,278 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# NOTE: For English locales, strings in this file should be in APA-style Title Case.
+# See https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case
+#
+# NOTE: For Engineers, please don't re-use these strings outside of the menubar.
+
+## File Menu
+
+menu-file =
+ .label = File
+ .accesskey = F
+menu-file-new-tab =
+ .label = New Tab
+ .accesskey = T
+menu-file-new-container-tab =
+ .label = New Container Tab
+ .accesskey = b
+menu-file-new-window =
+ .label = New Window
+ .accesskey = N
+menu-file-new-private-window =
+ .label = New Private Window
+ .accesskey = W
+# "Open Location" is only displayed on macOS, and only on windows
+# that aren't main browser windows, or when there are no windows
+# but Firefox is still running.
+menu-file-open-location =
+ .label = Open Location…
+menu-file-open-file =
+ .label = Open File…
+ .accesskey = O
+menu-file-close =
+ .label = Close
+ .accesskey = C
+menu-file-close-window =
+ .label = Close Window
+ .accesskey = d
+menu-file-save-page =
+ .label = Save Page As…
+ .accesskey = A
+menu-file-email-link =
+ .label = Email Link…
+ .accesskey = E
+menu-file-print-setup =
+ .label = Page Setup…
+ .accesskey = u
+menu-file-print-preview =
+ .label = Print Preview
+ .accesskey = v
+menu-file-print =
+ .label = Print…
+ .accesskey = P
+menu-file-import-from-another-browser =
+ .label = Import From Another Browser…
+ .accesskey = I
+menu-file-go-offline =
+ .label = Work Offline
+ .accesskey = k
+
+## Edit Menu
+
+menu-edit =
+ .label = Edit
+ .accesskey = E
+menu-edit-find-on =
+ .label = Find in This Page…
+ .accesskey = F
+menu-edit-find-again =
+ .label = Find Again
+ .accesskey = g
+menu-edit-bidi-switch-text-direction =
+ .label = Switch Text Direction
+ .accesskey = w
+
+## View Menu
+
+menu-view =
+ .label = View
+ .accesskey = V
+menu-view-toolbars-menu =
+ .label = Toolbars
+ .accesskey = T
+menu-view-customize-toolbar =
+ .label = Customize…
+ .accesskey = C
+menu-view-sidebar =
+ .label = Sidebar
+ .accesskey = e
+menu-view-bookmarks =
+ .label = Bookmarks
+menu-view-history-button =
+ .label = History
+menu-view-synced-tabs-sidebar =
+ .label = Synced Tabs
+menu-view-full-zoom =
+ .label = Zoom
+ .accesskey = Z
+menu-view-full-zoom-enlarge =
+ .label = Zoom In
+ .accesskey = I
+menu-view-full-zoom-reduce =
+ .label = Zoom Out
+ .accesskey = O
+menu-view-full-zoom-actual-size =
+ .label = Actual Size
+ .accesskey = A
+menu-view-full-zoom-toggle =
+ .label = Zoom Text Only
+ .accesskey = T
+menu-view-page-style-menu =
+ .label = Page Style
+ .accesskey = y
+menu-view-page-style-no-style =
+ .label = No Style
+ .accesskey = n
+menu-view-page-basic-style =
+ .label = Basic Page Style
+ .accesskey = B
+menu-view-charset =
+ .label = Text Encoding
+ .accesskey = c
+
+## These should match what Safari and other Apple applications
+## use on macOS.
+
+menu-view-enter-full-screen =
+ .label = Enter Full Screen
+ .accesskey = F
+menu-view-exit-full-screen =
+ .label = Exit Full Screen
+ .accesskey = F
+menu-view-full-screen =
+ .label = Full Screen
+ .accesskey = F
+
+##
+
+menu-view-show-all-tabs =
+ .label = Show All Tabs
+ .accesskey = A
+menu-view-bidi-switch-page-direction =
+ .label = Switch Page Direction
+ .accesskey = D
+
+## History Menu
+
+menu-history =
+ .label = History
+ .accesskey = s
+menu-history-show-all-history =
+ .label = Show All History
+menu-history-clear-recent-history =
+ .label = Clear Recent History…
+menu-history-synced-tabs =
+ .label = Synced Tabs
+menu-history-restore-last-session =
+ .label = Restore Previous Session
+menu-history-hidden-tabs =
+ .label = Hidden Tabs
+menu-history-undo-menu =
+ .label = Recently Closed Tabs
+menu-history-undo-window-menu =
+ .label = Recently Closed Windows
+
+## Bookmarks Menu
+
+menu-bookmarks-menu =
+ .label = Bookmarks
+ .accesskey = B
+menu-bookmarks-show-all =
+ .label = Show All Bookmarks
+menu-bookmark-this-page =
+ .label = Bookmark This Page
+menu-bookmark-edit =
+ .label = Edit This Bookmark
+menu-bookmarks-all-tabs =
+ .label = Bookmark All Tabs…
+menu-bookmarks-toolbar =
+ .label = Bookmarks Toolbar
+menu-bookmarks-other =
+ .label = Other Bookmarks
+menu-bookmarks-mobile =
+ .label = Mobile Bookmarks
+
+## Tools Menu
+
+menu-tools =
+ .label = Tools
+ .accesskey = T
+menu-tools-downloads =
+ .label = Downloads
+ .accesskey = D
+menu-tools-addons =
+ .label = Add-ons
+ .accesskey = A
+menu-tools-fxa-sign-in =
+ .label = Sign In To { -brand-product-name }…
+ .accesskey = g
+menu-tools-turn-on-sync =
+ .label = Turn on { -sync-brand-short-name }…
+ .accesskey = n
+menu-tools-sync-now =
+ .label = Sync Now
+ .accesskey = S
+menu-tools-fxa-re-auth =
+ .label = Reconnect to { -brand-product-name }…
+ .accesskey = R
+menu-tools-web-developer =
+ .label = Web Developer
+ .accesskey = W
+menu-tools-page-source =
+ .label = Page Source
+ .accesskey = o
+menu-tools-page-info =
+ .label = Page Info
+ .accesskey = I
+menu-preferences =
+ .label =
+ { PLATFORM() ->
+ [windows] Options
+ *[other] Preferences
+ }
+ .accesskey =
+ { PLATFORM() ->
+ [windows] O
+ *[other] n
+ }
+menu-tools-layout-debugger =
+ .label = Layout Debugger
+ .accesskey = L
+
+## Window Menu
+
+menu-window-menu =
+ .label = Window
+menu-window-bring-all-to-front =
+ .label = Bring All to Front
+
+## Help Menu
+
+menu-help =
+ .label = Help
+ .accesskey = H
+menu-help-product =
+ .label = { -brand-shorter-name } Help
+ .accesskey = H
+menu-help-show-tour =
+ .label = { -brand-shorter-name } Tour
+ .accesskey = o
+menu-help-import-from-another-browser =
+ .label = Import From Another Browser…
+ .accesskey = I
+menu-help-keyboard-shortcuts =
+ .label = Keyboard Shortcuts
+ .accesskey = K
+menu-help-troubleshooting-info =
+ .label = Troubleshooting Information
+ .accesskey = T
+menu-help-feedback-page =
+ .label = Submit Feedback…
+ .accesskey = S
+menu-help-safe-mode-without-addons =
+ .label = Restart With Add-ons Disabled…
+ .accesskey = R
+menu-help-safe-mode-with-addons =
+ .label = Restart With Add-ons Enabled
+ .accesskey = R
+# Label of the Help menu item. Either this or
+# menu-help-notdeceptive is shown.
+menu-help-report-deceptive-site =
+ .label = Report Deceptive Site…
+ .accesskey = D
+menu-help-not-deceptive =
+ .label = This Isn’t a Deceptive Site…
+ .accesskey = D
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/places.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/places.ftl
new file mode 100644
index 0000000000..7879940380
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/places.ftl
@@ -0,0 +1,77 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+places-open =
+ .label = Open
+ .accesskey = O
+places-open-tab =
+ .label = Open in a New Tab
+ .accesskey = w
+places-open-all-in-tabs =
+ .label = Open All in Tabs
+ .accesskey = O
+places-open-window =
+ .label = Open in a New Window
+ .accesskey = N
+places-open-private-window =
+ .label = Open in a New Private Window
+ .accesskey = P
+
+places-new-bookmark =
+ .label = New Bookmark…
+ .accesskey = B
+places-new-folder-contextmenu =
+ .label = New Folder…
+ .accesskey = F
+places-new-folder =
+ .label = New Folder…
+ .accesskey = o
+places-new-separator =
+ .label = New Separator
+ .accesskey = S
+
+places-view =
+ .label = View
+ .accesskey = w
+places-by-date =
+ .label = By Date
+ .accesskey = D
+places-by-site =
+ .label = By Site
+ .accesskey = S
+places-by-most-visited =
+ .label = By Most Visited
+ .accesskey = V
+places-by-last-visited =
+ .label = By Last Visited
+ .accesskey = L
+places-by-day-and-site =
+ .label = By Date and Site
+ .accesskey = t
+
+places-history-search =
+ .placeholder = Search history
+places-bookmarks-search =
+ .placeholder = Search bookmarks
+
+places-delete-domain-data =
+ .label = Forget About This Site
+ .accesskey = F
+places-sortby-name =
+ .label = Sort By Name
+ .accesskey = r
+places-properties =
+ .label = Properties
+ .accesskey = i
+
+# Managed bookmarks are created by an administrator and cannot be changed by the user.
+managed-bookmarks =
+ .label = Managed bookmarks
+# This label is used when a managed bookmarks folder doesn't have a name.
+managed-bookmarks-subfolder =
+ .label = Subfolder
+
+# This label is used for the "Other Bookmarks" folder that appears in the bookmarks toolbar.
+other-bookmarks-folder =
+ .label = Other Bookmarks
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/addEngine.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/addEngine.ftl
new file mode 100644
index 0000000000..c16f7f7db2
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/addEngine.ftl
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+add-engine-window =
+ .title = Add Search Engine
+ .style = width: 32em;
+
+add-engine-button = Add Custom Engine
+
+add-engine-name = Search engine name
+
+add-engine-alias = Alias
+
+add-engine-url = Engine URL, use %s in place of the search term
+
+add-engine-dialog=
+ .buttonlabelaccept = Add Engine
+ .buttonaccesskeyaccept = A
+
+engine-name-exists = An engine with that name already exists
+engine-alias-exists = An engine with that alias already exists
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/blocklists.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/blocklists.ftl
new file mode 100644
index 0000000000..cd5f7fe82d
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/blocklists.ftl
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+blocklist-window =
+ .title = Block Lists
+ .style = width: 55em
+
+blocklist-description = Choose the list { -brand-short-name } uses to block online trackers. Lists provided by <a data-l10n-name="disconnect-link" title="Disconnect">Disconnect</a>.
+blocklist-close-key =
+ .key = w
+
+blocklist-treehead-list =
+ .label = List
+
+blocklist-dialog=
+ .buttonlabelaccept = Save Changes
+ .buttonaccesskeyaccept = S
+
+
+# This template constructs the name of the block list in the block lists dialog.
+# It combines the list name and description.
+# e.g. "Standard (Recommended). This list does a pretty good job."
+#
+# Variables:
+# $listName {string, "Standard (Recommended)."} - List name.
+# $description {string, "This list does a pretty good job."} - Description of the list.
+blocklist-item-list-template = { $listName } { $description }
+
+blocklist-item-moz-std-listName = Level 1 block list (Recommended).
+blocklist-item-moz-std-description = Allows some trackers so fewer websites break.
+blocklist-item-moz-full-listName = Level 2 block list.
+blocklist-item-moz-full-description = Blocks all detected trackers. Some websites or content may not load properly.
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/clearSiteData.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/clearSiteData.ftl
new file mode 100644
index 0000000000..2d0f29e4ce
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/clearSiteData.ftl
@@ -0,0 +1,56 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+clear-site-data-window =
+ .title = Clear Data
+ .style = width: 35em
+
+clear-site-data-description = Clearing all cookies and site data stored by { -brand-short-name } may sign you out of websites and remove offline web content. Clearing cache data will not affect your logins.
+
+clear-site-data-close-key =
+ .key = w
+
+# The parameters in parentheses in this string describe disk usage
+# in the format ($amount $unit), e.g. "Cookies and Site Data (24 KB)"
+# Variables:
+# $amount (Number) - Amount of site data currently stored on disk
+# $unit (String) - Abbreviation of the unit that $amount is in, e.g. "MB"
+clear-site-data-cookies-with-data =
+ .label = Cookies and Site Data ({ $amount } { $unit })
+ .accesskey = S
+
+# This string is a placeholder for while the data used to fill
+# clear-site-data-cookies-with-data is loading. This placeholder is usually
+# only shown for a very short time (< 1s), so it should be very similar
+# or the same as clear-site-data-cookies-with-data (except the amount and unit),
+# to avoid flickering.
+clear-site-data-cookies-empty =
+ .label = Cookies and Site Data
+ .accesskey = S
+
+clear-site-data-cookies-info = You may get signed out of websites if cleared
+
+# The parameters in parentheses in this string describe disk usage
+# in the format ($amount $unit), e.g. "Cached Web Content (24 KB)"
+# Variables:
+# $amount (Number) - Amount of cache currently stored on disk
+# $unit (String) - Abbreviation of the unit that $amount is in, e.g. "MB"
+clear-site-data-cache-with-data =
+ .label = Cached Web Content ({ $amount } { $unit })
+ .accesskey = W
+
+# This string is a placeholder for while the data used to fill
+# clear-site-data-cache-with-data is loading. This placeholder is usually
+# only shown for a very short time (< 1s), so it should be very similar
+# or the same as clear-site-data-cache-with-data (except the amount and unit),
+# to avoid flickering.
+clear-site-data-cache-empty =
+ .label = Cached Web Content
+ .accesskey = W
+
+clear-site-data-cache-info = Will require websites to reload images and data
+
+clear-site-data-dialog =
+ .buttonlabelaccept = Clear
+ .buttonaccesskeyaccept = l
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/colors.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/colors.ftl
new file mode 100644
index 0000000000..55dc0d0b54
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/colors.ftl
@@ -0,0 +1,48 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+colors-window =
+ .title = Colors
+ .style =
+ { PLATFORM() ->
+ [macos] width: 41em
+ *[other] width: 38em
+ }
+
+colors-close-key =
+ .key = w
+
+colors-page-override = Override the colors specified by the page with your selections above
+ .accesskey = O
+
+colors-page-override-option-always =
+ .label = Always
+colors-page-override-option-auto =
+ .label = Only with High Contrast themes
+colors-page-override-option-never =
+ .label = Never
+
+colors-text-and-background = Text and Background
+
+colors-text-header = Text
+ .accesskey = T
+
+colors-background = Background
+ .accesskey = B
+
+colors-use-system =
+ .label = Use system colors
+ .accesskey = s
+
+colors-underline-links =
+ .label = Underline links
+ .accesskey = U
+
+colors-links-header = Link Colors
+
+colors-unvisited-links = Unvisited Links
+ .accesskey = L
+
+colors-visited-links = Visited Links
+ .accesskey = V
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/connection.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/connection.ftl
new file mode 100644
index 0000000000..c16dd44d9a
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/connection.ftl
@@ -0,0 +1,106 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+connection-window =
+ .title = Connection Settings
+ .style =
+ { PLATFORM() ->
+ [macos] width: 44em
+ *[other] width: 49em
+ }
+
+connection-close-key =
+ .key = w
+
+connection-disable-extension =
+ .label = Disable Extension
+
+connection-proxy-configure = Configure Proxy Access to the Internet
+
+connection-proxy-option-no =
+ .label = No proxy
+ .accesskey = y
+connection-proxy-option-system =
+ .label = Use system proxy settings
+ .accesskey = U
+connection-proxy-option-auto =
+ .label = Auto-detect proxy settings for this network
+ .accesskey = w
+connection-proxy-option-manual =
+ .label = Manual proxy configuration
+ .accesskey = M
+
+connection-proxy-http = HTTP Proxy
+ .accesskey = x
+connection-proxy-http-port = Port
+ .accesskey = P
+connection-proxy-http-sharing =
+ .label = Also use this proxy for FTP and HTTPS
+ .accesskey = s
+
+connection-proxy-https = HTTPS Proxy
+ .accesskey = H
+connection-proxy-ssl-port = Port
+ .accesskey = o
+
+connection-proxy-ftp = FTP Proxy
+ .accesskey = F
+connection-proxy-ftp-port = Port
+ .accesskey = r
+
+connection-proxy-socks = SOCKS Host
+ .accesskey = C
+connection-proxy-socks-port = Port
+ .accesskey = t
+
+connection-proxy-socks4 =
+ .label = SOCKS v4
+ .accesskey = K
+connection-proxy-socks5 =
+ .label = SOCKS v5
+ .accesskey = v
+connection-proxy-noproxy = No proxy for
+ .accesskey = N
+
+connection-proxy-noproxy-desc = Example: .mozilla.org, .net.nz, 192.168.1.0/24
+
+# Do not translate localhost, 127.0.0.1 and ::1.
+connection-proxy-noproxy-localhost-desc = Connections to localhost, 127.0.0.1, and ::1 are never proxied.
+
+connection-proxy-autotype =
+ .label = Automatic proxy configuration URL
+ .accesskey = A
+
+connection-proxy-reload =
+ .label = Reload
+ .accesskey = e
+
+connection-proxy-autologin =
+ .label = Do not prompt for authentication if password is saved
+ .accesskey = i
+ .tooltip = This option silently authenticates you to proxies when you have saved credentials for them. You will be prompted if authentication fails.
+
+connection-proxy-socks-remote-dns =
+ .label = Proxy DNS when using SOCKS v5
+ .accesskey = D
+
+connection-dns-over-https =
+ .label = Enable DNS over HTTPS
+ .accesskey = b
+
+connection-dns-over-https-url-resolver = Use Provider
+ .accesskey = P
+
+# Variables:
+# $name (String) - Display name or URL for the DNS over HTTPS provider
+connection-dns-over-https-url-item-default =
+ .label = { $name } (Default)
+ .tooltiptext = Use the default URL for resolving DNS over HTTPS
+
+connection-dns-over-https-url-custom =
+ .label = Custom
+ .accesskey = C
+ .tooltiptext = Enter your preferred URL for resolving DNS over HTTPS
+
+connection-dns-over-https-custom-label = Custom
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/fonts.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/fonts.ftl
new file mode 100644
index 0000000000..0f4269e8be
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/fonts.ftl
@@ -0,0 +1,116 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+fonts-window =
+ .title = Fonts
+
+fonts-window-close =
+ .key = w
+
+## Font groups by language
+
+fonts-langgroup-header = Fonts for
+ .accesskey = F
+
+fonts-langgroup-arabic =
+ .label = Arabic
+fonts-langgroup-armenian =
+ .label = Armenian
+fonts-langgroup-bengali =
+ .label = Bengali
+fonts-langgroup-simpl-chinese =
+ .label = Simplified Chinese
+fonts-langgroup-trad-chinese-hk =
+ .label = Traditional Chinese (Hong Kong)
+fonts-langgroup-trad-chinese =
+ .label = Traditional Chinese (Taiwan)
+fonts-langgroup-cyrillic =
+ .label = Cyrillic
+fonts-langgroup-devanagari =
+ .label = Devanagari
+fonts-langgroup-ethiopic =
+ .label = Ethiopic
+fonts-langgroup-georgian =
+ .label = Georgian
+fonts-langgroup-el =
+ .label = Greek
+fonts-langgroup-gujarati =
+ .label = Gujarati
+fonts-langgroup-gurmukhi =
+ .label = Gurmukhi
+fonts-langgroup-japanese =
+ .label = Japanese
+fonts-langgroup-hebrew =
+ .label = Hebrew
+fonts-langgroup-kannada =
+ .label = Kannada
+fonts-langgroup-khmer =
+ .label = Khmer
+fonts-langgroup-korean =
+ .label = Korean
+# Translate "Latin" as the name of Latin (Roman) script, not as the name of the Latin language.
+fonts-langgroup-latin =
+ .label = Latin
+fonts-langgroup-malayalam =
+ .label = Malayalam
+fonts-langgroup-math =
+ .label = Mathematics
+fonts-langgroup-odia =
+ .label = Odia
+fonts-langgroup-sinhala =
+ .label = Sinhala
+fonts-langgroup-tamil =
+ .label = Tamil
+fonts-langgroup-telugu =
+ .label = Telugu
+fonts-langgroup-thai =
+ .label = Thai
+fonts-langgroup-tibetan =
+ .label = Tibetan
+fonts-langgroup-canadian =
+ .label = Unified Canadian Syllabary
+fonts-langgroup-other =
+ .label = Other Writing Systems
+
+## Default fonts and their sizes
+
+fonts-proportional-header = Proportional
+ .accesskey = P
+
+fonts-default-serif =
+ .label = Serif
+fonts-default-sans-serif =
+ .label = Sans Serif
+
+fonts-proportional-size = Size
+ .accesskey = z
+
+fonts-serif = Serif
+ .accesskey = S
+
+fonts-sans-serif = Sans-serif
+ .accesskey = n
+
+fonts-monospace = Monospace
+ .accesskey = M
+
+fonts-monospace-size = Size
+ .accesskey = e
+
+fonts-minsize = Minimum font size
+ .accesskey = o
+
+fonts-minsize-none =
+ .label = None
+
+fonts-allow-own =
+ .label = Allow pages to choose their own fonts, instead of your selections above
+ .accesskey = A
+
+# Variables:
+# $name {string, "Arial"} - Name of the default font
+fonts-label-default =
+ .label = Default ({ $name })
+fonts-label-default-unnamed =
+ .label = Default
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/languages.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/languages.ftl
new file mode 100644
index 0000000000..23d94ba08c
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/languages.ftl
@@ -0,0 +1,73 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+webpage-languages-window =
+ .title = Webpage Language Settings
+ .style = width: 40em
+
+languages-close-key =
+ .key = w
+
+languages-description = Web pages are sometimes offered in more than one language. Choose languages for displaying these web pages, in order of preference
+
+languages-customize-spoof-english =
+ .label = Request English versions of web pages for enhanced privacy
+
+languages-customize-moveup =
+ .label = Move Up
+ .accesskey = U
+
+languages-customize-movedown =
+ .label = Move Down
+ .accesskey = D
+
+languages-customize-remove =
+ .label = Remove
+ .accesskey = R
+
+languages-customize-select-language =
+ .placeholder = Select a language to add…
+
+languages-customize-add =
+ .label = Add
+ .accesskey = A
+
+# The pattern used to generate strings presented to the user in the
+# locale selection list.
+#
+# Example:
+# Icelandic [is]
+# Spanish (Chile) [es-CL]
+#
+# Variables:
+# $locale (String) - A name of the locale (for example: "Icelandic", "Spanish (Chile)")
+# $code (String) - Locale code of the locale (for example: "is", "es-CL")
+languages-code-format =
+ .label = { $locale } [{ $code }]
+
+languages-active-code-format =
+ .value = { languages-code-format.label }
+
+browser-languages-window =
+ .title = { -brand-short-name } Language Settings
+ .style = width: 40em
+
+browser-languages-description = { -brand-short-name } will display the first language as your default and will display alternate languages if necessary in the order they appear.
+
+browser-languages-search = Search for more languages…
+
+browser-languages-searching =
+ .label = Searching for languages…
+
+browser-languages-downloading =
+ .label = Downloading…
+
+browser-languages-select-language =
+ .label = Select a language to add…
+ .placeholder = Select a language to add…
+
+browser-languages-installed-label = Installed languages
+browser-languages-available-label = Available languages
+
+browser-languages-error = { -brand-short-name } can’t update your languages right now. Check that you are connected to the internet or try again.
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/permissions.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/permissions.ftl
new file mode 100644
index 0000000000..be1a4f74cb
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/permissions.ftl
@@ -0,0 +1,166 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+permissions-window =
+ .title = Exceptions
+ .style = width: 45em
+
+permissions-close-key =
+ .key = w
+
+permissions-address = Address of website
+ .accesskey = d
+
+permissions-block =
+ .label = Block
+ .accesskey = B
+
+permissions-session =
+ .label = Allow for Session
+ .accesskey = S
+
+permissions-allow =
+ .label = Allow
+ .accesskey = A
+
+permissions-site-name =
+ .label = Website
+
+permissions-status =
+ .label = Status
+
+permissions-remove =
+ .label = Remove Website
+ .accesskey = R
+
+permissions-remove-all =
+ .label = Remove All Websites
+ .accesskey = e
+
+permission-dialog =
+ .buttonlabelaccept = Save Changes
+ .buttonaccesskeyaccept = S
+
+permissions-autoplay-menu = Default for all websites:
+
+permissions-searchbox =
+ .placeholder = Search Website
+
+permissions-capabilities-autoplay-allow =
+ .label = Allow Audio and Video
+permissions-capabilities-autoplay-block =
+ .label = Block Audio
+permissions-capabilities-autoplay-blockall =
+ .label = Block Audio and Video
+
+permissions-capabilities-allow =
+ .label = Allow
+permissions-capabilities-block =
+ .label = Block
+permissions-capabilities-prompt =
+ .label = Always Ask
+
+permissions-capabilities-listitem-allow =
+ .value = Allow
+permissions-capabilities-listitem-block =
+ .value = Block
+permissions-capabilities-listitem-allow-session =
+ .value = Allow for Session
+
+## Invalid Hostname Dialog
+
+permissions-invalid-uri-title = Invalid Hostname Entered
+permissions-invalid-uri-label = Please enter a valid hostname
+
+## Exceptions - Tracking Protection
+
+permissions-exceptions-etp-window =
+ .title = Exceptions for Enhanced Tracking Protection
+ .style = { permissions-window.style }
+permissions-exceptions-etp-desc = You’ve turned off protections on these websites.
+
+## Exceptions - Cookies
+
+permissions-exceptions-cookie-window =
+ .title = Exceptions - Cookies and Site Data
+ .style = { permissions-window.style }
+permissions-exceptions-cookie-desc = You can specify which websites are always or never allowed to use cookies and site data. Type the exact address of the site you want to manage and then click Block, Allow for Session, or Allow.
+
+## Exceptions - Pop-ups
+
+permissions-exceptions-popup-window =
+ .title = Allowed Websites - Pop-ups
+ .style = { permissions-window.style }
+permissions-exceptions-popup-desc = You can specify which websites are allowed to open pop-up windows. Type the exact address of the site you want to allow and then click Allow.
+
+## Exceptions - Saved Logins
+
+permissions-exceptions-saved-logins-window =
+ .title = Exceptions - Saved Logins
+ .style = { permissions-window.style }
+permissions-exceptions-saved-logins-desc = Logins for the following websites will not be saved
+
+## Exceptions - Add-ons
+
+permissions-exceptions-addons-window =
+ .title = Allowed Websites - Add-ons Installation
+ .style = { permissions-window.style }
+permissions-exceptions-addons-desc = You can specify which websites are allowed to install add-ons. Type the exact address of the site you want to allow and then click Allow.
+
+## Site Permissions - Autoplay
+
+permissions-site-autoplay-window =
+ .title = Settings - Autoplay
+ .style = { permissions-window.style }
+permissions-site-autoplay-desc = You can manage the sites that do not follow your default autoplay settings here.
+
+## Site Permissions - Notifications
+
+permissions-site-notification-window =
+ .title = Settings - Notification Permissions
+ .style = { permissions-window.style }
+permissions-site-notification-desc = The following websites have requested to send you notifications. You can specify which websites are allowed to send you notifications. You can also block new requests asking to allow notifications.
+permissions-site-notification-disable-label =
+ .label = Block new requests asking to allow notifications
+permissions-site-notification-disable-desc = This will prevent any websites not listed above from requesting permission to send notifications. Blocking notifications may break some website features.
+
+## Site Permissions - Location
+
+permissions-site-location-window =
+ .title = Settings - Location Permissions
+ .style = { permissions-window.style }
+permissions-site-location-desc = The following websites have requested to access your location. You can specify which websites are allowed to access your location. You can also block new requests asking to access your location.
+permissions-site-location-disable-label =
+ .label = Block new requests asking to access your location
+permissions-site-location-disable-desc = This will prevent any websites not listed above from requesting permission to access your location. Blocking access to your location may break some website features.
+
+## Site Permissions - Virtual Reality
+
+permissions-site-xr-window =
+ .title = Settings - Virtual Reality Permissions
+ .style = { permissions-window.style }
+permissions-site-xr-desc = The following websites have requested to access your virtual reality devices. You can specify which websites are allowed to access your virtual reality devices. You can also block new requests asking to access your virtual reality devices.
+permissions-site-xr-disable-label =
+ .label = Block new requests asking to access your virtual reality devices
+permissions-site-xr-disable-desc = This will prevent any websites not listed above from requesting permission to access your virtual reality devices. Blocking access to your virtual reality devices may break some website features.
+
+## Site Permissions - Camera
+
+permissions-site-camera-window =
+ .title = Settings - Camera Permissions
+ .style = { permissions-window.style }
+permissions-site-camera-desc = The following websites have requested to access your camera. You can specify which websites are allowed to access your camera. You can also block new requests asking to access your camera.
+permissions-site-camera-disable-label =
+ .label = Block new requests asking to access your camera
+permissions-site-camera-disable-desc = This will prevent any websites not listed above from requesting permission to access your camera. Blocking access to your camera may break some website features.
+
+## Site Permissions - Microphone
+
+permissions-site-microphone-window =
+ .title = Settings - Microphone Permissions
+ .style = { permissions-window.style }
+permissions-site-microphone-desc = The following websites have requested to access your microphone. You can specify which websites are allowed to access your microphone. You can also block new requests asking to access your microphone.
+permissions-site-microphone-disable-label =
+ .label = Block new requests asking to access your microphone
+permissions-site-microphone-disable-desc = This will prevent any websites not listed above from requesting permission to access your microphone. Blocking access to your microphone may break some website features.
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/preferences.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/preferences.ftl
new file mode 100644
index 0000000000..d90823fa6b
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/preferences.ftl
@@ -0,0 +1,1389 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+do-not-track-description = Send websites a “Do Not Track” signal that you don’t want to be tracked
+do-not-track-learn-more = Learn more
+do-not-track-option-default-content-blocking-known =
+ .label = Only when { -brand-short-name } is set to block known trackers
+do-not-track-option-always =
+ .label = Always
+
+pref-page-title =
+ { PLATFORM() ->
+ [windows] Options
+ *[other] Preferences
+ }
+
+# This is used to determine the width of the search field in about:preferences,
+# in order to make the entire placeholder string visible
+#
+# Please keep the placeholder string short to avoid truncation.
+#
+# Notice: The value of the `.style` attribute is a CSS string, and the `width`
+# is the name of the CSS property. It is intended only to adjust the element's width.
+# Do not translate.
+search-input-box =
+ .style = width: 15.4em
+ .placeholder =
+ { PLATFORM() ->
+ [windows] Find in Options
+ *[other] Find in Preferences
+ }
+
+managed-notice = Your browser is being managed by your organization.
+
+category-list =
+ .aria-label = Categories
+
+pane-general-title = General
+category-general =
+ .tooltiptext = { pane-general-title }
+
+pane-home-title = Home
+category-home =
+ .tooltiptext = { pane-home-title }
+
+pane-search-title = Search
+category-search =
+ .tooltiptext = { pane-search-title }
+
+pane-privacy-title = Privacy & Security
+category-privacy =
+ .tooltiptext = { pane-privacy-title }
+
+pane-sync-title2 = { -sync-brand-short-name }
+category-sync2 =
+ .tooltiptext = { pane-sync-title2 }
+
+pane-experimental-title = { -brand-short-name } Experiments
+category-experimental =
+ .tooltiptext = { -brand-short-name } Experiments
+pane-experimental-subtitle = Proceed with Caution
+pane-experimental-search-results-header = { -brand-short-name } Experiments: Proceed with Caution
+pane-experimental-description = Changing advanced configuration preferences can impact { -brand-short-name } performance or security.
+
+pane-experimental-reset =
+ .label = Restore Defaults
+ .accesskey = R
+
+help-button-label = { -brand-short-name } Support
+addons-button-label = Extensions & Themes
+
+focus-search =
+ .key = f
+
+close-button =
+ .aria-label = Close
+
+## Browser Restart Dialog
+
+feature-enable-requires-restart = { -brand-short-name } must restart to enable this feature.
+feature-disable-requires-restart = { -brand-short-name } must restart to disable this feature.
+should-restart-title = Restart { -brand-short-name }
+should-restart-ok = Restart { -brand-short-name } now
+cancel-no-restart-button = Cancel
+restart-later = Restart Later
+
+## Extension Control Notifications
+##
+## These strings are used to inform the user
+## about changes made by extensions to browser settings.
+##
+## <img data-l10n-name="icon"/> is going to be replaced by the extension icon.
+##
+## Variables:
+## $name (String): name of the extension
+
+# This string is shown to notify the user that the password manager setting
+# is being controlled by an extension
+extension-controlled-password-saving = An extension, <img data-l10n-name="icon"/> { $name }, is controlling this setting.
+
+# This string is shown to notify the user that their notifications permission
+# is being controlled by an extension.
+extension-controlled-web-notifications= An extension, <img data-l10n-name="icon"/> { $name }, is controlling this setting.
+
+# This string is shown to notify the user that Container Tabs
+# are being enabled by an extension.
+extension-controlled-privacy-containers = An extension, <img data-l10n-name="icon"/> { $name }, requires Container Tabs.
+
+# This string is shown to notify the user that their content blocking "All Detected Trackers"
+# preferences are being controlled by an extension.
+extension-controlled-websites-content-blocking-all-trackers = An extension, <img data-l10n-name="icon"/> { $name }, is controlling this setting.
+
+# This string is shown to notify the user that their proxy configuration preferences
+# are being controlled by an extension.
+extension-controlled-proxy-config = An extension, <img data-l10n-name="icon"/> { $name }, is controlling how { -brand-short-name } connects to the internet.
+
+# This string is shown after the user disables an extension to notify the user
+# how to enable an extension that they disabled.
+#
+# <img data-l10n-name="addons-icon"/> will be replaced with Add-ons icon
+# <img data-l10n-name="menu-icon"/> will be replaced with Menu icon
+extension-controlled-enable = To enable the extension go to <img data-l10n-name="addons-icon"/> Add-ons in the <img data-l10n-name="menu-icon"/> menu.
+
+## Preferences UI Search Results
+
+search-results-header = Search Results
+
+# `<span data-l10n-name="query"></span>` will be replaced by the search term.
+search-results-empty-message =
+ { PLATFORM() ->
+ [windows] Sorry! There are no results in Options for “<span data-l10n-name="query"></span>”.
+ *[other] Sorry! There are no results in Preferences for “<span data-l10n-name="query"></span>”.
+ }
+
+search-results-help-link = Need help? Visit <a data-l10n-name="url">{ -brand-short-name } Support</a>
+
+## General Section
+
+startup-header = Startup
+
+# { -brand-short-name } will be 'Firefox Developer Edition',
+# since this setting is only exposed in Firefox Developer Edition
+separate-profile-mode =
+ .label = Allow { -brand-short-name } and Firefox to run at the same time
+use-firefox-sync = Tip: This uses separate profiles. Use { -sync-brand-short-name } to share data between them.
+get-started-not-logged-in = Sign in to { -sync-brand-short-name }…
+get-started-configured = Open { -sync-brand-short-name } preferences
+
+always-check-default =
+ .label = Always check if { -brand-short-name } is your default browser
+ .accesskey = y
+
+is-default = { -brand-short-name } is currently your default browser
+is-not-default = { -brand-short-name } is not your default browser
+
+set-as-my-default-browser =
+ .label = Make Default…
+ .accesskey = D
+
+startup-restore-previous-session =
+ .label = Restore previous session
+ .accesskey = s
+
+startup-restore-warn-on-quit =
+ .label = Warn you when quitting the browser
+
+disable-extension =
+ .label = Disable Extension
+
+tabs-group-header = Tabs
+
+ctrl-tab-recently-used-order =
+ .label = Ctrl+Tab cycles through tabs in recently used order
+ .accesskey = T
+
+open-new-link-as-tabs =
+ .label = Open links in tabs instead of new windows
+ .accesskey = w
+
+warn-on-close-multiple-tabs =
+ .label = Warn you when closing multiple tabs
+ .accesskey = m
+
+warn-on-open-many-tabs =
+ .label = Warn you when opening multiple tabs might slow down { -brand-short-name }
+ .accesskey = d
+
+switch-links-to-new-tabs =
+ .label = When you open a link in a new tab, switch to it immediately
+ .accesskey = h
+
+show-tabs-in-taskbar =
+ .label = Show tab previews in the Windows taskbar
+ .accesskey = k
+
+browser-containers-enabled =
+ .label = Enable Container Tabs
+ .accesskey = n
+
+browser-containers-learn-more = Learn more
+
+browser-containers-settings =
+ .label = Settings…
+ .accesskey = i
+
+containers-disable-alert-title = Close All Container Tabs?
+containers-disable-alert-desc =
+ { $tabCount ->
+ [one] If you disable Container Tabs now, { $tabCount } container tab will be closed. Are you sure you want to disable Container Tabs?
+ *[other] If you disable Container Tabs now, { $tabCount } container tabs will be closed. Are you sure you want to disable Container Tabs?
+ }
+
+containers-disable-alert-ok-button =
+ { $tabCount ->
+ [one] Close { $tabCount } Container Tab
+ *[other] Close { $tabCount } Container Tabs
+ }
+containers-disable-alert-cancel-button = Keep enabled
+
+containers-remove-alert-title = Remove This Container?
+
+# Variables:
+# $count (Number) - Number of tabs that will be closed.
+containers-remove-alert-msg =
+ { $count ->
+ [one] If you remove this Container now, { $count } container tab will be closed. Are you sure you want to remove this Container?
+ *[other] If you remove this Container now, { $count } container tabs will be closed. Are you sure you want to remove this Container?
+ }
+
+containers-remove-ok-button = Remove this Container
+containers-remove-cancel-button = Don’t remove this Container
+
+
+## General Section - Language & Appearance
+
+language-and-appearance-header = Language and Appearance
+
+fonts-and-colors-header = Fonts and Colors
+
+default-font = Default font
+ .accesskey = D
+default-font-size = Size
+ .accesskey = S
+
+advanced-fonts =
+ .label = Advanced…
+ .accesskey = A
+
+colors-settings =
+ .label = Colors…
+ .accesskey = C
+
+# Zoom is a noun, and the message is used as header for a group of options
+preferences-zoom-header = Zoom
+
+preferences-default-zoom = Default zoom
+ .accesskey = z
+
+preferences-default-zoom-value =
+ .label = { $percentage }%
+
+preferences-zoom-text-only =
+ .label = Zoom text only
+ .accesskey = t
+
+language-header = Language
+
+choose-language-description = Choose your preferred language for displaying pages
+
+choose-button =
+ .label = Choose…
+ .accesskey = o
+
+choose-browser-language-description = Choose the languages used to display menus, messages, and notifications from { -brand-short-name }.
+manage-browser-languages-button =
+ .label = Set Alternatives…
+ .accesskey = l
+confirm-browser-language-change-description = Restart { -brand-short-name } to apply these changes
+confirm-browser-language-change-button = Apply and Restart
+
+translate-web-pages =
+ .label = Translate web content
+ .accesskey = T
+
+# The <img> element is replaced by the logo of the provider
+# used to provide machine translations for web pages.
+translate-attribution = Translations by <img data-l10n-name="logo"/>
+
+translate-exceptions =
+ .label = Exceptions…
+ .accesskey = x
+
+# Variables:
+# $localeName (string) - Localized name of the locale to be used.
+use-system-locale =
+ .label = Use your operating system settings for “{ $localeName }” to format dates, times, numbers, and measurements.
+
+check-user-spelling =
+ .label = Check your spelling as you type
+ .accesskey = t
+
+## General Section - Files and Applications
+
+files-and-applications-title = Files and Applications
+
+download-header = Downloads
+
+download-save-to =
+ .label = Save files to
+ .accesskey = v
+
+download-choose-folder =
+ .label =
+ { PLATFORM() ->
+ [macos] Choose…
+ *[other] Browse…
+ }
+ .accesskey =
+ { PLATFORM() ->
+ [macos] e
+ *[other] o
+ }
+
+download-always-ask-where =
+ .label = Always ask you where to save files
+ .accesskey = A
+
+applications-header = Applications
+
+applications-description = Choose how { -brand-short-name } handles the files you download from the web or the applications you use while browsing.
+
+applications-filter =
+ .placeholder = Search file types or applications
+
+applications-type-column =
+ .label = Content Type
+ .accesskey = T
+
+applications-action-column =
+ .label = Action
+ .accesskey = A
+
+# Variables:
+# $extension (String) - file extension (e.g .TXT)
+applications-file-ending = { $extension } file
+applications-action-save =
+ .label = Save File
+
+# Variables:
+# $app-name (String) - Name of an application (e.g Adobe Acrobat)
+applications-use-app =
+ .label = Use { $app-name }
+
+# Variables:
+# $app-name (String) - Name of an application (e.g Adobe Acrobat)
+applications-use-app-default =
+ .label = Use { $app-name } (default)
+
+applications-use-os-default =
+ .label =
+ { PLATFORM() ->
+ [macos] Use macOS default application
+ [windows] Use Windows default application
+ *[other] Use system default application
+ }
+
+applications-use-other =
+ .label = Use other…
+applications-select-helper = Select Helper Application
+
+applications-manage-app =
+ .label = Application Details…
+applications-always-ask =
+ .label = Always ask
+
+# Variables:
+# $type-description (String) - Description of the type (e.g "Portable Document Format")
+# $type (String) - the MIME type (e.g application/binary)
+applications-type-description-with-type = { $type-description } ({ $type })
+
+# Variables:
+# $extension (String) - file extension (e.g .TXT)
+# $type (String) - the MIME type (e.g application/binary)
+applications-file-ending-with-type = { applications-file-ending } ({ $type })
+
+# Variables:
+# $plugin-name (String) - Name of a plugin (e.g Adobe Flash)
+applications-use-plugin-in =
+ .label = Use { $plugin-name } (in { -brand-short-name })
+applications-open-inapp =
+ .label = Open in { -brand-short-name }
+
+## The strings in this group are used to populate
+## selected label element based on the string from
+## the selected menu item.
+
+applications-use-plugin-in-label =
+ .value = { applications-use-plugin-in.label }
+
+applications-action-save-label =
+ .value = { applications-action-save.label }
+
+applications-use-app-label =
+ .value = { applications-use-app.label }
+
+applications-open-inapp-label =
+ .value = { applications-open-inapp.label }
+
+applications-always-ask-label =
+ .value = { applications-always-ask.label }
+
+applications-use-app-default-label =
+ .value = { applications-use-app-default.label }
+
+applications-use-other-label =
+ .value = { applications-use-other.label }
+
+applications-use-os-default-label =
+ .value = { applications-use-os-default.label }
+
+##
+
+drm-content-header = Digital Rights Management (DRM) Content
+
+play-drm-content =
+ .label = Play DRM-controlled content
+ .accesskey = P
+
+play-drm-content-learn-more = Learn more
+
+update-application-title = { -brand-short-name } Updates
+
+update-application-description = Keep { -brand-short-name } up to date for the best performance, stability, and security.
+
+update-application-version = Version { $version } <a data-l10n-name="learn-more">What’s new</a>
+
+update-history =
+ .label = Show Update History…
+ .accesskey = p
+
+update-application-allow-description = Allow { -brand-short-name } to
+
+update-application-auto =
+ .label = Automatically install updates (recommended)
+ .accesskey = A
+
+update-application-check-choose =
+ .label = Check for updates but let you choose to install them
+ .accesskey = C
+
+update-application-manual =
+ .label = Never check for updates (not recommended)
+ .accesskey = N
+
+update-application-warning-cross-user-setting = This setting will apply to all Windows accounts and { -brand-short-name } profiles using this installation of { -brand-short-name }.
+
+update-application-use-service =
+ .label = Use a background service to install updates
+ .accesskey = b
+
+update-setting-write-failure-title = Error saving Update preferences
+
+# Variables:
+# $path (String) - Path to the configuration file
+# The newlines between the main text and the line containing the path is
+# intentional so the path is easier to identify.
+update-setting-write-failure-message =
+ { -brand-short-name } encountered an error and didn’t save this change. Note that setting this update preference requires permission to write to the file below. You or a system administrator may be able resolve the error by granting the Users group full control to this file.
+
+ Could not write to file: { $path }
+
+update-in-progress-title = Update In Progress
+
+update-in-progress-message = Do you want { -brand-short-name } to continue with this update?
+
+update-in-progress-ok-button = &Discard
+# Continue is the cancel button so pressing escape or using a platform standard
+# method of closing the UI will not discard the update.
+update-in-progress-cancel-button = &Continue
+
+## General Section - Performance
+
+performance-title = Performance
+
+performance-use-recommended-settings-checkbox =
+ .label = Use recommended performance settings
+ .accesskey = U
+
+performance-use-recommended-settings-desc = These settings are tailored to your computer’s hardware and operating system.
+
+performance-settings-learn-more = Learn more
+
+performance-allow-hw-accel =
+ .label = Use hardware acceleration when available
+ .accesskey = r
+
+performance-limit-content-process-option = Content process limit
+ .accesskey = l
+
+performance-limit-content-process-enabled-desc = Additional content processes can improve performance when using multiple tabs, but will also use more memory.
+performance-limit-content-process-blocked-desc = Modifying the number of content processes is only possible with multiprocess { -brand-short-name }. <a data-l10n-name="learn-more">Learn how to check if multiprocess is enabled</a>
+
+# Variables:
+# $num - default value of the `dom.ipc.processCount` pref.
+performance-default-content-process-count =
+ .label = { $num } (default)
+
+## General Section - Browsing
+
+browsing-title = Browsing
+
+browsing-use-autoscroll =
+ .label = Use autoscrolling
+ .accesskey = a
+
+browsing-use-smooth-scrolling =
+ .label = Use smooth scrolling
+ .accesskey = m
+
+browsing-use-onscreen-keyboard =
+ .label = Show a touch keyboard when necessary
+ .accesskey = c
+
+browsing-use-cursor-navigation =
+ .label = Always use the cursor keys to navigate within pages
+ .accesskey = k
+
+browsing-search-on-start-typing =
+ .label = Search for text when you start typing
+ .accesskey = x
+
+browsing-picture-in-picture-toggle-enabled =
+ .label = Enable picture-in-picture video controls
+ .accesskey = E
+
+browsing-picture-in-picture-learn-more = Learn more
+
+browsing-media-control =
+ .label = Control media via keyboard, headset, or virtual interface
+ .accesskey = v
+
+browsing-media-control-learn-more = Learn more
+
+browsing-cfr-recommendations =
+ .label = Recommend extensions as you browse
+ .accesskey = R
+browsing-cfr-features =
+ .label = Recommend features as you browse
+ .accesskey = f
+
+browsing-cfr-recommendations-learn-more = Learn more
+
+## General Section - Proxy
+
+network-settings-title = Network Settings
+
+network-proxy-connection-description = Configure how { -brand-short-name } connects to the internet.
+
+network-proxy-connection-learn-more = Learn more
+
+network-proxy-connection-settings =
+ .label = Settings…
+ .accesskey = e
+
+## Home Section
+
+home-new-windows-tabs-header = New Windows and Tabs
+
+home-new-windows-tabs-description2 = Choose what you see when you open your homepage, new windows, and new tabs.
+
+## Home Section - Home Page Customization
+
+home-homepage-mode-label = Homepage and new windows
+
+home-newtabs-mode-label = New tabs
+
+home-restore-defaults =
+ .label = Restore Defaults
+ .accesskey = R
+
+# "Firefox" should be treated as a brand and kept in English,
+# while "Home" and "(Default)" can be localized.
+home-mode-choice-default =
+ .label = Firefox Home (Default)
+
+home-mode-choice-custom =
+ .label = Custom URLs…
+
+home-mode-choice-blank =
+ .label = Blank Page
+
+home-homepage-custom-url =
+ .placeholder = Paste a URL…
+
+# This string has a special case for '1' and [other] (default). If necessary for
+# your language, you can add {$tabCount} to your translations and use the
+# standard CLDR forms, or only use the form for [other] if both strings should
+# be identical.
+use-current-pages =
+ .label =
+ { $tabCount ->
+ [1] Use Current Page
+ *[other] Use Current Pages
+ }
+ .accesskey = C
+
+choose-bookmark =
+ .label = Use Bookmark…
+ .accesskey = B
+
+## Home Section - Firefox Home Content Customization
+
+home-prefs-content-header = Firefox Home Content
+home-prefs-content-description = Choose what content you want on your Firefox Home screen.
+
+home-prefs-search-header =
+ .label = Web Search
+home-prefs-topsites-header =
+ .label = Top Sites
+home-prefs-topsites-description = The sites you visit most
+home-prefs-topsites-by-option-sponsored =
+ .label = Sponsored Top Sites
+home-prefs-shortcuts-header =
+ .label = Shortcuts
+home-prefs-shortcuts-description = Sites you save or visit
+home-prefs-shortcuts-by-option-sponsored =
+ .label = Sponsored shortcuts
+
+## Variables:
+## $provider (String): Name of the corresponding content provider, e.g "Pocket".
+
+home-prefs-recommended-by-header =
+ .label = Recommended by { $provider }
+home-prefs-recommended-by-description-update = Exceptional content from across the web, curated by { $provider }
+home-prefs-recommended-by-description-new = Exceptional content curated by { $provider }, part of the { -brand-product-name } family
+##
+
+home-prefs-recommended-by-learn-more = How it works
+home-prefs-recommended-by-option-sponsored-stories =
+ .label = Sponsored Stories
+
+home-prefs-highlights-header =
+ .label = Highlights
+home-prefs-highlights-description = A selection of sites that you’ve saved or visited
+home-prefs-highlights-option-visited-pages =
+ .label = Visited Pages
+home-prefs-highlights-options-bookmarks =
+ .label = Bookmarks
+home-prefs-highlights-option-most-recent-download =
+ .label = Most Recent Download
+home-prefs-highlights-option-saved-to-pocket =
+ .label = Pages Saved to { -pocket-brand-name }
+
+home-prefs-recent-activity-header =
+ .label = Recent activity
+home-prefs-recent-activity-description = A selection of recent sites and content
+
+# For the "Snippets" feature traditionally on about:home.
+# Alternative translation options: "Small Note" or something that
+# expresses the idea of "a small message, shortened from something else,
+# and non-essential but also not entirely trivial and useless.
+home-prefs-snippets-header =
+ .label = Snippets
+home-prefs-snippets-description = Updates from { -vendor-short-name } and { -brand-product-name }
+
+home-prefs-snippets-description-new = Tips and news from { -vendor-short-name } and { -brand-product-name }
+
+home-prefs-sections-rows-option =
+ .label =
+ { $num ->
+ [one] { $num } row
+ *[other] { $num } rows
+ }
+
+## Search Section
+
+search-bar-header = Search Bar
+search-bar-hidden =
+ .label = Use the address bar for search and navigation
+search-bar-shown =
+ .label = Add search bar in toolbar
+
+search-engine-default-header = Default Search Engine
+search-engine-default-desc-2 = This is your default search engine in the address bar and search bar. You can switch it at any time.
+search-engine-default-private-desc-2 = Choose a different default search engine for Private Windows only
+search-separate-default-engine =
+ .label = Use this search engine in Private Windows
+ .accesskey = U
+
+search-suggestions-header = Search Suggestions
+search-suggestions-desc = Choose how suggestions from search engines appear.
+
+search-suggestions-option =
+ .label = Provide search suggestions
+ .accesskey = s
+
+search-show-suggestions-url-bar-option =
+ .label = Show search suggestions in address bar results
+ .accesskey = l
+
+# This string describes what the user will observe when the system
+# prioritizes search suggestions over browsing history in the results
+# that extend down from the address bar. In the original English string,
+# "ahead" refers to location (appearing most proximate to), not time
+# (appearing before).
+search-show-suggestions-above-history-option =
+ .label = Show search suggestions ahead of browsing history in address bar results
+
+search-show-suggestions-private-windows =
+ .label = Show search suggestions in Private Windows
+
+suggestions-addressbar-settings-generic = Change preferences for other address bar suggestions
+
+search-suggestions-cant-show = Search suggestions will not be shown in location bar results because you have configured { -brand-short-name } to never remember history.
+
+search-one-click-header2 = Search Shortcuts
+
+search-one-click-desc = Choose the alternative search engines that appear below the address bar and search bar when you start to enter a keyword.
+
+search-choose-engine-column =
+ .label = Search Engine
+search-choose-keyword-column =
+ .label = Keyword
+
+search-restore-default =
+ .label = Restore Default Search Engines
+ .accesskey = D
+
+search-remove-engine =
+ .label = Remove
+ .accesskey = R
+search-add-engine =
+ .label = Add
+ .accesskey = A
+
+search-find-more-link = Find more search engines
+
+# This warning is displayed when the chosen keyword is already in use
+# ('Duplicate' is an adjective)
+search-keyword-warning-title = Duplicate Keyword
+# Variables:
+# $name (String) - Name of a search engine.
+search-keyword-warning-engine = You have chosen a keyword that is currently in use by “{ $name }”. Please select another.
+search-keyword-warning-bookmark = You have chosen a keyword that is currently in use by a bookmark. Please select another.
+
+## Containers Section
+
+containers-back-button =
+ .aria-label =
+ { PLATFORM() ->
+ [windows] Back to Options
+ *[other] Back to Preferences
+ }
+containers-header = Container Tabs
+containers-add-button =
+ .label = Add New Container
+ .accesskey = A
+
+containers-new-tab-check =
+ .label = Select a container for each new tab
+ .accesskey = S
+
+containers-preferences-button =
+ .label = Preferences
+containers-remove-button =
+ .label = Remove
+
+## Firefox Account - Signed out. Note that "Sync" and "Firefox Account" are now
+## more discrete ("signed in" no longer means "and sync is connected").
+
+sync-signedout-caption = Take Your Web With You
+sync-signedout-description = Synchronize your bookmarks, history, tabs, passwords, add-ons, and preferences across all your devices.
+
+sync-signedout-account-signin2 =
+ .label = Sign in to { -sync-brand-short-name }…
+ .accesskey = i
+
+# This message contains two links and two icon images.
+# `<img data-l10n-name="android-icon"/>` - Android logo icon
+# `<a data-l10n-name="android-link">` - Link to Android Download
+# `<img data-l10n-name="ios-icon">` - iOS logo icon
+# `<a data-l10n-name="ios-link">` - Link to iOS Download
+#
+# They can be moved within the sentence as needed to adapt
+# to your language, but should not be changed or translated.
+sync-mobile-promo = Download Firefox for <img data-l10n-name="android-icon"/> <a data-l10n-name="android-link">Android</a> or <img data-l10n-name="ios-icon"/> <a data-l10n-name="ios-link">iOS</a> to sync with your mobile device.
+
+## Firefox Account - Signed in
+
+sync-profile-picture =
+ .tooltiptext = Change profile picture
+
+sync-sign-out =
+ .label = Sign Out…
+ .accesskey = g
+
+sync-manage-account = Manage account
+ .accesskey = o
+
+sync-signedin-unverified = { $email } is not verified.
+sync-signedin-login-failure = Please sign in to reconnect { $email }
+
+sync-resend-verification =
+ .label = Resend Verification
+ .accesskey = d
+
+sync-remove-account =
+ .label = Remove Account
+ .accesskey = R
+
+sync-sign-in =
+ .label = Sign in
+ .accesskey = g
+
+## Sync section - enabling or disabling sync.
+
+prefs-syncing-on = Syncing: ON
+
+prefs-syncing-off = Syncing: OFF
+
+prefs-sync-setup =
+ .label = Set Up { -sync-brand-short-name }…
+ .accesskey = S
+
+prefs-sync-offer-setup-label = Synchronize your bookmarks, history, tabs, passwords, add-ons, and preferences across all your devices.
+
+prefs-sync-now =
+ .labelnotsyncing = Sync Now
+ .accesskeynotsyncing = N
+ .labelsyncing = Syncing…
+
+## The list of things currently syncing.
+
+sync-currently-syncing-heading = You are currently syncing these items:
+
+sync-currently-syncing-bookmarks = Bookmarks
+sync-currently-syncing-history = History
+sync-currently-syncing-tabs = Open tabs
+sync-currently-syncing-logins-passwords = Logins and passwords
+sync-currently-syncing-addresses = Addresses
+sync-currently-syncing-creditcards = Credit cards
+sync-currently-syncing-addons = Add-ons
+sync-currently-syncing-prefs =
+ { PLATFORM() ->
+ [windows] Options
+ *[other] Preferences
+ }
+
+sync-change-options =
+ .label = Change…
+ .accesskey = C
+
+## The "Choose what to sync" dialog.
+
+sync-choose-what-to-sync-dialog =
+ .title = Choose What To Sync
+ .style = width: 36em; min-height: 35em;
+ .buttonlabelaccept = Save Changes
+ .buttonaccesskeyaccept = S
+ .buttonlabelextra2 = Disconnect…
+ .buttonaccesskeyextra2 = D
+
+sync-engine-bookmarks =
+ .label = Bookmarks
+ .accesskey = m
+
+sync-engine-history =
+ .label = History
+ .accesskey = r
+
+sync-engine-tabs =
+ .label = Open tabs
+ .tooltiptext = A list of what’s open on all synced devices
+ .accesskey = t
+
+sync-engine-logins-passwords =
+ .label = Logins and passwords
+ .tooltiptext = Usernames and passwords you’ve saved
+ .accesskey = L
+
+sync-engine-addresses =
+ .label = Addresses
+ .tooltiptext = Postal addresses you’ve saved (desktop only)
+ .accesskey = e
+
+sync-engine-creditcards =
+ .label = Credit cards
+ .tooltiptext = Names, numbers and expiry dates (desktop only)
+ .accesskey = C
+
+sync-engine-addons =
+ .label = Add-ons
+ .tooltiptext = Extensions and themes for Firefox desktop
+ .accesskey = A
+
+sync-engine-prefs =
+ .label =
+ { PLATFORM() ->
+ [windows] Options
+ *[other] Preferences
+ }
+ .tooltiptext = General, Privacy, and Security settings you’ve changed
+ .accesskey = s
+
+## The device name controls.
+
+sync-device-name-header = Device Name
+
+sync-device-name-change =
+ .label = Change Device Name…
+ .accesskey = h
+
+sync-device-name-cancel =
+ .label = Cancel
+ .accesskey = n
+
+sync-device-name-save =
+ .label = Save
+ .accesskey = v
+
+sync-connect-another-device = Connect another device
+
+## Privacy Section
+
+privacy-header = Browser Privacy
+
+## Privacy Section - Logins and Passwords
+
+# The search keyword isn't shown to users but is used to find relevant settings in about:preferences.
+pane-privacy-logins-and-passwords-header = Logins and Passwords
+ .searchkeywords = { -lockwise-brand-short-name }
+
+# Checkbox to control whether UI is shown to users to save or fill logins/passwords.
+forms-ask-to-save-logins =
+ .label = Ask to save logins and passwords for websites
+ .accesskey = r
+forms-exceptions =
+ .label = Exceptions…
+ .accesskey = x
+forms-generate-passwords =
+ .label = Suggest and generate strong passwords
+ .accesskey = u
+forms-breach-alerts =
+ .label = Show alerts about passwords for breached websites
+ .accesskey = b
+forms-breach-alerts-learn-more-link = Learn more
+
+# Checkbox which controls filling saved logins into fields automatically when they appear, in some cases without user interaction.
+forms-fill-logins-and-passwords =
+ .label = Autofill logins and passwords
+ .accesskey = i
+forms-saved-logins =
+ .label = Saved Logins…
+ .accesskey = L
+forms-primary-pw-use =
+ .label = Use a Primary Password
+ .accesskey = U
+forms-primary-pw-learn-more-link = Learn more
+# This string uses the former name of the Primary Password feature
+# ("Master Password" in English) so that the preferences can be found
+# when searching for the old name. The accesskey is unused.
+forms-master-pw-change =
+ .label = Change Master Password…
+ .accesskey = M
+forms-primary-pw-change =
+ .label = Change Primary Password…
+ .accesskey = P
+# Leave this message empty if the translation for "Primary Password" matches
+# "Master Password" in your language. If you're editing the FTL file directly,
+# use { "" } as the value.
+forms-primary-pw-former-name = Formerly known as Master Password
+
+forms-primary-pw-fips-title = You are currently in FIPS mode. FIPS requires a non-empty Primary Password.
+forms-master-pw-fips-desc = Password Change Failed
+
+## OS Authentication dialog
+
+# This message can be seen by trying to add a Primary Password.
+primary-password-os-auth-dialog-message-win = To create a Primary Password, enter your Windows login credentials. This helps protect the security of your accounts.
+
+# This message can be seen by trying to add a Primary Password.
+# The macOS strings are preceded by the operating system with "Firefox is trying to "
+# and includes subtitle of "Enter password for the user "xxx" to allow this." These
+# notes are only valid for English. Please test in your locale.
+primary-password-os-auth-dialog-message-macosx = create a Primary Password
+master-password-os-auth-dialog-caption = { -brand-full-name }
+
+## Privacy Section - History
+
+history-header = History
+
+# This label is followed, on the same line, by a dropdown list of options
+# (Remember history, etc.).
+# In English it visually creates a full sentence, e.g.
+# "Firefox will" + "Remember history".
+#
+# If this doesn't work for your language, you can translate this message:
+# - Simply as "Firefox", moving the verb into each option.
+# This will result in "Firefox" + "Will remember history", etc.
+# - As a stand-alone message, for example "Firefox history settings:".
+history-remember-label = { -brand-short-name } will
+ .accesskey = w
+
+history-remember-option-all =
+ .label = Remember history
+history-remember-option-never =
+ .label = Never remember history
+history-remember-option-custom =
+ .label = Use custom settings for history
+
+history-remember-description = { -brand-short-name } will remember your browsing, download, form, and search history.
+history-dontremember-description = { -brand-short-name } will use the same settings as private browsing, and will not remember any history as you browse the Web.
+
+history-private-browsing-permanent =
+ .label = Always use private browsing mode
+ .accesskey = p
+
+history-remember-browser-option =
+ .label = Remember browsing and download history
+ .accesskey = b
+
+history-remember-search-option =
+ .label = Remember search and form history
+ .accesskey = f
+
+history-clear-on-close-option =
+ .label = Clear history when { -brand-short-name } closes
+ .accesskey = r
+
+history-clear-on-close-settings =
+ .label = Settings…
+ .accesskey = t
+
+history-clear-button =
+ .label = Clear History…
+ .accesskey = s
+
+## Privacy Section - Site Data
+
+sitedata-header = Cookies and Site Data
+
+sitedata-total-size-calculating = Calculating site data and cache size…
+
+# Variables:
+# $value (Number) - Value of the unit (for example: 4.6, 500)
+# $unit (String) - Name of the unit (for example: "bytes", "KB")
+sitedata-total-size = Your stored cookies, site data, and cache are currently using { $value } { $unit } of disk space.
+
+sitedata-learn-more = Learn more
+
+sitedata-delete-on-close =
+ .label = Delete cookies and site data when { -brand-short-name } is closed
+ .accesskey = c
+
+sitedata-delete-on-close-private-browsing = In permanent private browsing mode, cookies and site data will always be cleared when { -brand-short-name } is closed.
+
+sitedata-allow-cookies-option =
+ .label = Accept cookies and site data
+ .accesskey = A
+
+sitedata-disallow-cookies-option =
+ .label = Block cookies and site data
+ .accesskey = B
+
+# This label means 'type of content that is blocked', and is followed by a drop-down list with content types below.
+# The list items are the strings named sitedata-block-*-option*.
+sitedata-block-desc = Type blocked
+ .accesskey = T
+
+sitedata-option-block-cross-site-trackers =
+ .label = Cross-site trackers
+sitedata-option-block-cross-site-and-social-media-trackers =
+ .label = Cross-site and social media trackers
+sitedata-option-block-cross-site-tracking-cookies-including-social-media =
+ .label = Cross-site tracking cookies — includes social media cookies
+sitedata-option-block-cross-site-cookies-including-social-media =
+ .label = Cross-site cookies — includes social media cookies
+sitedata-option-block-cross-site-and-social-media-trackers-plus-isolate =
+ .label = Cross-site and social media trackers, and isolate remaining cookies
+sitedata-option-block-unvisited =
+ .label = Cookies from unvisited websites
+sitedata-option-block-all-third-party =
+ .label = All third-party cookies (may cause websites to break)
+sitedata-option-block-all =
+ .label = All cookies (will cause websites to break)
+
+sitedata-clear =
+ .label = Clear Data…
+ .accesskey = l
+
+sitedata-settings =
+ .label = Manage Data…
+ .accesskey = M
+
+sitedata-cookies-exceptions =
+ .label = Manage Exceptions…
+ .accesskey = x
+
+## Privacy Section - Address Bar
+
+addressbar-header = Address Bar
+
+addressbar-suggest = When using the address bar, suggest
+
+addressbar-locbar-history-option =
+ .label = Browsing history
+ .accesskey = h
+addressbar-locbar-bookmarks-option =
+ .label = Bookmarks
+ .accesskey = k
+addressbar-locbar-openpage-option =
+ .label = Open tabs
+ .accesskey = O
+addressbar-locbar-topsites-option =
+ .label = Top sites
+ .accesskey = T
+addressbar-locbar-engines-option =
+ .label = Search engines
+ .accesskey = a
+
+addressbar-suggestions-settings = Change preferences for search engine suggestions
+
+## Privacy Section - Content Blocking
+
+content-blocking-enhanced-tracking-protection = Enhanced Tracking Protection
+
+content-blocking-section-top-level-description = Trackers follow you around online to collect information about your browsing habits and interests. { -brand-short-name } blocks many of these trackers and other malicious scripts.
+
+content-blocking-learn-more = Learn more
+
+content-blocking-fpi-incompatibility-warning = You are using First Party Isolation (FPI), which overrides some of { -brand-short-name }’s cookie settings.
+
+## These strings are used to define the different levels of
+## Enhanced Tracking Protection.
+
+# "Standard" in this case is an adjective, meaning "default" or "normal".
+enhanced-tracking-protection-setting-standard =
+ .label = Standard
+ .accesskey = d
+enhanced-tracking-protection-setting-strict =
+ .label = Strict
+ .accesskey = r
+enhanced-tracking-protection-setting-custom =
+ .label = Custom
+ .accesskey = C
+##
+
+content-blocking-etp-standard-desc = Balanced for protection and performance. Pages will load normally.
+content-blocking-etp-strict-desc = Stronger protection, but may cause some sites or content to break.
+content-blocking-etp-custom-desc = Choose which trackers and scripts to block.
+
+content-blocking-private-windows = Tracking content in Private Windows
+content-blocking-cross-site-cookies = Cross-site cookies
+content-blocking-cross-site-tracking-cookies = Cross-site tracking cookies
+content-blocking-cross-site-tracking-cookies-plus-isolate = Cross-site tracking cookies, and isolate remaining cookies
+content-blocking-social-media-trackers = Social media trackers
+content-blocking-all-cookies = All cookies
+content-blocking-unvisited-cookies = Cookies from unvisited sites
+content-blocking-all-windows-tracking-content = Tracking content in all windows
+content-blocking-all-third-party-cookies = All third-party cookies
+content-blocking-cryptominers = Cryptominers
+content-blocking-fingerprinters = Fingerprinters
+
+content-blocking-warning-title = Heads up!
+content-blocking-and-isolating-etp-warning-description = Blocking trackers and isolating cookies could impact the functionality of some sites. Reload a page with trackers to load all content.
+content-blocking-and-isolating-etp-warning-description-2 = This setting may cause some websites to not display content or work correctly. If a site seems broken, you may want to turn off tracking protection for that site to load all content.
+content-blocking-warning-learn-how = Learn how
+
+content-blocking-reload-description = You will need to reload your tabs to apply these changes.
+content-blocking-reload-tabs-button =
+ .label = Reload All Tabs
+ .accesskey = R
+
+content-blocking-tracking-content-label =
+ .label = Tracking content
+ .accesskey = T
+content-blocking-tracking-protection-option-all-windows =
+ .label = In all windows
+ .accesskey = A
+content-blocking-option-private =
+ .label = Only in Private Windows
+ .accesskey = p
+content-blocking-tracking-protection-change-block-list = Change block list
+
+content-blocking-cookies-label =
+ .label = Cookies
+ .accesskey = C
+
+content-blocking-expand-section =
+ .tooltiptext = More information
+
+# Cryptomining refers to using scripts on websites that can use a computer’s resources to mine cryptocurrency without a user’s knowledge.
+content-blocking-cryptominers-label =
+ .label = Cryptominers
+ .accesskey = y
+
+# Browser fingerprinting is a method of tracking users by the configuration and settings information (their "digital fingerprint")
+# that is visible to websites they browse, rather than traditional tracking methods such as IP addresses and unique cookies.
+content-blocking-fingerprinters-label =
+ .label = Fingerprinters
+ .accesskey = F
+
+## Privacy Section - Tracking
+
+tracking-manage-exceptions =
+ .label = Manage Exceptions…
+ .accesskey = x
+
+## Privacy Section - Permissions
+
+permissions-header = Permissions
+
+permissions-location = Location
+permissions-location-settings =
+ .label = Settings…
+ .accesskey = t
+
+permissions-xr = Virtual Reality
+permissions-xr-settings =
+ .label = Settings…
+ .accesskey = t
+
+permissions-camera = Camera
+permissions-camera-settings =
+ .label = Settings…
+ .accesskey = t
+
+permissions-microphone = Microphone
+permissions-microphone-settings =
+ .label = Settings…
+ .accesskey = t
+
+permissions-notification = Notifications
+permissions-notification-settings =
+ .label = Settings…
+ .accesskey = t
+permissions-notification-link = Learn more
+
+permissions-notification-pause =
+ .label = Pause notifications until { -brand-short-name } restarts
+ .accesskey = n
+
+permissions-autoplay = Autoplay
+
+permissions-autoplay-settings =
+ .label = Settings…
+ .accesskey = t
+
+permissions-block-popups =
+ .label = Block pop-up windows
+ .accesskey = B
+
+permissions-block-popups-exceptions =
+ .label = Exceptions…
+ .accesskey = E
+
+permissions-addon-install-warning =
+ .label = Warn you when websites try to install add-ons
+ .accesskey = W
+
+permissions-addon-exceptions =
+ .label = Exceptions…
+ .accesskey = E
+
+## Privacy Section - Data Collection
+
+collection-header = { -brand-short-name } Data Collection and Use
+
+collection-description = We strive to provide you with choices and collect only what we need to provide and improve { -brand-short-name } for everyone. We always ask permission before receiving personal information.
+collection-privacy-notice = Privacy Notice
+
+collection-health-report-telemetry-disabled = You’re no longer allowing { -vendor-short-name } to capture technical and interaction data. All past data will be deleted within 30 days.
+collection-health-report-telemetry-disabled-link = Learn more
+
+collection-health-report =
+ .label = Allow { -brand-short-name } to send technical and interaction data to { -vendor-short-name }
+ .accesskey = r
+collection-health-report-link = Learn more
+
+collection-studies =
+ .label = Allow { -brand-short-name } to install and run studies
+collection-studies-link = View { -brand-short-name } studies
+
+addon-recommendations =
+ .label = Allow { -brand-short-name } to make personalized extension recommendations
+addon-recommendations-link = Learn more
+
+# This message is displayed above disabled data sharing options in developer builds
+# or builds with no Telemetry support available.
+collection-health-report-disabled = Data reporting is disabled for this build configuration
+
+collection-backlogged-crash-reports =
+ .label = Allow { -brand-short-name } to send backlogged crash reports on your behalf
+ .accesskey = c
+collection-backlogged-crash-reports-link = Learn more
+
+## Privacy Section - Security
+##
+## It is important that wording follows the guidelines outlined on this page:
+## https://developers.google.com/safe-browsing/developers_guide_v2#AcceptableUsage
+
+security-header = Security
+
+security-browsing-protection = Deceptive Content and Dangerous Software Protection
+
+security-enable-safe-browsing =
+ .label = Block dangerous and deceptive content
+ .accesskey = B
+security-enable-safe-browsing-link = Learn more
+
+security-block-downloads =
+ .label = Block dangerous downloads
+ .accesskey = d
+
+security-block-uncommon-software =
+ .label = Warn you about unwanted and uncommon software
+ .accesskey = c
+
+## Privacy Section - Certificates
+
+certs-header = Certificates
+
+certs-enable-ocsp =
+ .label = Query OCSP responder servers to confirm the current validity of certificates
+ .accesskey = Q
+
+certs-view =
+ .label = View Certificates…
+ .accesskey = C
+
+certs-devices =
+ .label = Security Devices…
+ .accesskey = D
+
+space-alert-learn-more-button =
+ .label = Learn More
+ .accesskey = L
+
+space-alert-over-5gb-pref-button =
+ .label =
+ { PLATFORM() ->
+ [windows] Open Options
+ *[other] Open Preferences
+ }
+ .accesskey =
+ { PLATFORM() ->
+ [windows] O
+ *[other] O
+ }
+
+space-alert-over-5gb-message =
+ { PLATFORM() ->
+ [windows] { -brand-short-name } is running out of disk space. Website contents may not display properly. You can clear stored data in Options > Privacy & Security > Cookies and Site Data.
+ *[other] { -brand-short-name } is running out of disk space. Website contents may not display properly. You can clear stored data in Preferences > Privacy & Security > Cookies and Site Data.
+ }
+
+space-alert-under-5gb-ok-button =
+ .label = OK, Got it
+ .accesskey = K
+
+space-alert-under-5gb-message = { -brand-short-name } is running out of disk space. Website contents may not display properly. Visit “Learn More” to optimize your disk usage for better browsing experience.
+
+## Privacy Section - HTTPS-Only
+
+httpsonly-header = HTTPS-Only Mode
+
+httpsonly-description = HTTPS provides a secure, encrypted connection between { -brand-short-name } and the websites you visit. Most websites support HTTPS, and if HTTPS-Only Mode is enabled, then { -brand-short-name } will upgrade all connections to HTTPS.
+
+httpsonly-learn-more = Learn more
+
+httpsonly-radio-enabled =
+ .label = Enable HTTPS-Only Mode in all windows
+
+httpsonly-radio-enabled-pbm =
+ .label = Enable HTTPS-Only Mode in private windows only
+
+httpsonly-radio-disabled =
+ .label = Don’t enable HTTPS-Only Mode
+
+## The following strings are used in the Download section of settings
+desktop-folder-name = Desktop
+downloads-folder-name = Downloads
+choose-download-folder-title = Choose Download Folder:
+
+# Variables:
+# $service-name (String) - Name of a cloud storage provider like Dropbox, Google Drive, etc...
+save-files-to-cloud-storage =
+ .label = Save files to { $service-name }
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/selectBookmark.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/selectBookmark.ftl
new file mode 100644
index 0000000000..c88abfcd54
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/selectBookmark.ftl
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+select-bookmark-window =
+ .title = Set Home Page
+ .style = width: 32em;
+
+select-bookmark-desc = Choose a Bookmark to be your Home Page. If you choose a folder, the Bookmarks in that folder will be opened in Tabs.
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/siteDataSettings.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/siteDataSettings.ftl
new file mode 100644
index 0000000000..19e25b8b16
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/preferences/siteDataSettings.ftl
@@ -0,0 +1,63 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+## Settings
+
+site-data-settings-window =
+ .title = Manage Cookies and Site Data
+
+site-data-settings-description = The following websites store cookies and site data on your computer. { -brand-short-name } keeps data from websites with persistent storage until you delete it, and deletes data from websites with non-persistent storage as space is needed.
+
+site-data-search-textbox =
+ .placeholder = Search websites
+ .accesskey = S
+
+site-data-column-host =
+ .label = Site
+site-data-column-cookies =
+ .label = Cookies
+site-data-column-storage =
+ .label = Storage
+site-data-column-last-used =
+ .label = Last Used
+
+# This label is used in the "Host" column for local files, which have no host.
+site-data-local-file-host = (local file)
+
+site-data-remove-selected =
+ .label = Remove Selected
+ .accesskey = R
+
+site-data-settings-dialog =
+ .buttonlabelaccept = Save Changes
+ .buttonaccesskeyaccept = a
+
+# Variables:
+# $value (Number) - Value of the unit (for example: 4.6, 500)
+# $unit (String) - Name of the unit (for example: "bytes", "KB")
+site-storage-usage =
+ .value = { $value } { $unit }
+site-storage-persistent =
+ .value = { site-storage-usage.value } (Persistent)
+
+site-data-remove-all =
+ .label = Remove All
+ .accesskey = e
+
+site-data-remove-shown =
+ .label = Remove All Shown
+ .accesskey = e
+
+## Removing
+
+site-data-removing-dialog =
+ .title = { site-data-removing-header }
+ .buttonlabelaccept = Remove
+
+site-data-removing-header = Removing Cookies and Site Data
+
+site-data-removing-desc = Removing cookies and site data may log you out of websites. Are you sure you want to make the changes?
+
+site-data-removing-table = Cookies and site data for the following websites will be removed
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/protectionsPanel.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/protectionsPanel.ftl
new file mode 100644
index 0000000000..d7c33a53da
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/protectionsPanel.ftl
@@ -0,0 +1,107 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+protections-panel-sendreportview-error = There was an error sending the report. Please try again later.
+
+# A link shown when ETP is disabled for a site. Opens the breakage report subview when clicked.
+protections-panel-sitefixedsendreport-label = Site fixed? Send report
+
+## These strings are used to define the different levels of
+## Enhanced Tracking Protection.
+
+protections-popup-footer-protection-label-strict = Strict
+ .label = Strict
+protections-popup-footer-protection-label-custom = Custom
+ .label = Custom
+protections-popup-footer-protection-label-standard = Standard
+ .label = Standard
+
+##
+
+# The text a screen reader speaks when focused on the info button.
+protections-panel-etp-more-info =
+ .aria-label = More information about Enhanced Tracking Protection
+
+protections-panel-etp-on-header = Enhanced Tracking Protection is ON for this site
+protections-panel-etp-off-header = Enhanced Tracking Protection is OFF for this site
+
+# The link to be clicked to open the sub-panel view
+protections-panel-site-not-working = Site not working?
+
+# The heading/title of the sub-panel view
+protections-panel-site-not-working-view =
+ .title = Site Not Working?
+
+## The "Allowed" header also includes a "Why?" link that, when hovered, shows
+## a tooltip explaining why these items were not blocked in the page.
+
+protections-panel-not-blocking-why-label = Why?
+protections-panel-not-blocking-why-etp-on-tooltip = Blocking these could break elements of some websites. Without trackers, some buttons, forms, and login fields might not work.
+protections-panel-not-blocking-why-etp-off-tooltip = All trackers on this site have been loaded because protections are turned off.
+
+##
+
+protections-panel-no-trackers-found = No trackers known to { -brand-short-name } were detected on this page.
+
+protections-panel-content-blocking-tracking-protection = Tracking Content
+
+protections-panel-content-blocking-socialblock = Social Media Trackers
+protections-panel-content-blocking-cryptominers-label = Cryptominers
+protections-panel-content-blocking-fingerprinters-label = Fingerprinters
+
+## In the protections panel, Content Blocking category items are in three sections:
+## "Blocked" for categories being blocked in the current page,
+## "Allowed" for categories detected but not blocked in the current page, and
+## "None Detected" for categories not detected in the current page.
+## These strings are used in the header labels of each of these sections.
+protections-panel-blocking-label = Blocked
+protections-panel-not-blocking-label = Allowed
+protections-panel-not-found-label = None Detected
+
+##
+
+protections-panel-settings-label = Protection Settings
+# This should match the "appmenuitem-protection-dashboard-title" string in browser/appmenu.ftl.
+protections-panel-protectionsdashboard-label = Protections Dashboard
+
+## In the Site Not Working? view, we suggest turning off protections if
+## the user is experiencing issues with any of a variety of functionality.
+
+# The header of the list
+protections-panel-site-not-working-view-header = Turn off protections if you’re having issues with:
+
+# The list items, shown in a <ul>
+protections-panel-site-not-working-view-issue-list-login-fields = Login fields
+protections-panel-site-not-working-view-issue-list-forms = Forms
+protections-panel-site-not-working-view-issue-list-payments = Payments
+protections-panel-site-not-working-view-issue-list-comments = Comments
+protections-panel-site-not-working-view-issue-list-videos = Videos
+
+protections-panel-site-not-working-view-send-report = Send a report
+
+##
+
+protections-panel-cross-site-tracking-cookies = These cookies follow you from site to site to gather data about what you do online. They are set by third parties such as advertisers and analytics companies.
+protections-panel-cryptominers = Cryptominers use your system’s computing power to mine digital money. Cryptomining scripts drain your battery, slow down your computer, and can increase your energy bill.
+protections-panel-fingerprinters = Fingerprinters collect settings from your browser and computer to create a profile of you. Using this digital fingerprint, they can track you across different websites.
+protections-panel-tracking-content = Websites may load external ads, videos, and other content with tracking code. Blocking tracking content can help sites load faster, but some buttons, forms, and login fields might not work.
+protections-panel-social-media-trackers = Social networks place trackers on other websites to follow what you do, see, and watch online. This allows social media companies to learn more about you beyond what you share on your social media profiles.
+
+protections-panel-content-blocking-manage-settings =
+ .label = Manage Protection Settings
+ .accesskey = M
+
+protections-panel-content-blocking-breakage-report-view =
+ .title = Report a Broken Site
+protections-panel-content-blocking-breakage-report-view-description = Blocking certain trackers can cause problems with some websites. Reporting these problems helps make { -brand-short-name } better for everyone. Sending this report will send a URL and information about your browser settings to Mozilla. <label data-l10n-name="learn-more">Learn more</label>
+protections-panel-content-blocking-breakage-report-view-collection-url = URL
+protections-panel-content-blocking-breakage-report-view-collection-url-label =
+ .aria-label = URL
+protections-panel-content-blocking-breakage-report-view-collection-comments = Optional: Describe the problem
+protections-panel-content-blocking-breakage-report-view-collection-comments-label =
+ .aria-label = Optional: Describe the problem
+protections-panel-content-blocking-breakage-report-view-cancel =
+ .label = Cancel
+protections-panel-content-blocking-breakage-report-view-send-report =
+ .label = Send Report
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/sanitize.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/sanitize.ftl
new file mode 100644
index 0000000000..3df2c0f519
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/sanitize.ftl
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+sanitize-prefs =
+ .title = Settings for Clearing History
+ .style = width: 34em
+
+sanitize-prefs-style =
+ .style = width: 17em
+
+dialog-title =
+ .title = Clear Recent History
+ .style = width: 34em
+
+# When "Time range to clear" is set to "Everything", this message is used for the
+# title instead of dialog-title.
+dialog-title-everything =
+ .title = Clear All History
+ .style = width: 34em
+
+clear-data-settings-label = When closed, { -brand-short-name } should automatically clear all
+
+## clear-time-duration-prefix is followed by a dropdown list, with
+## values localized using clear-time-duration-value-* messages.
+## clear-time-duration-suffix is left empty in English, but can be
+## used in other languages to change the structure of the message.
+##
+## This results in English:
+## Time range to clear: (Last Hour, Today, etc.)
+
+clear-time-duration-prefix =
+ .value = Time range to clear:{ " " }
+ .accesskey = T
+
+clear-time-duration-value-last-hour =
+ .label = Last Hour
+
+clear-time-duration-value-last-2-hours =
+ .label = Last Two Hours
+
+clear-time-duration-value-last-4-hours =
+ .label = Last Four Hours
+
+clear-time-duration-value-today =
+ .label = Today
+
+clear-time-duration-value-everything =
+ .label = Everything
+
+clear-time-duration-suffix =
+ .value = { "" }
+
+## These strings are used as section comments and checkboxes
+## to select the items to remove
+
+history-section-label = History
+
+item-history-and-downloads =
+ .label = Browsing & Download History
+ .accesskey = B
+
+item-cookies =
+ .label = Cookies
+ .accesskey = C
+
+item-active-logins =
+ .label = Active Logins
+ .accesskey = L
+
+item-cache =
+ .label = Cache
+ .accesskey = a
+
+item-form-search-history =
+ .label = Form & Search History
+ .accesskey = F
+
+data-section-label = Data
+
+item-site-preferences =
+ .label = Site Preferences
+ .accesskey = S
+
+item-offline-apps =
+ .label = Offline Website Data
+ .accesskey = O
+
+sanitize-everything-undo-warning = This action cannot be undone.
+
+window-close =
+ .key = w
+
+sanitize-button-ok =
+ .label = Clear Now
+
+# The label for the default button between the user clicking it and the window
+# closing. Indicates the items are being cleared.
+sanitize-button-clearing =
+ .label = Clearing
+
+# Warning that appears when "Time range to clear" is set to "Everything" in Clear
+# Recent History dialog, provided that the user has not modified the default set
+# of history items to clear.
+sanitize-everything-warning = All history will be cleared.
+
+# Warning that appears when "Time range to clear" is set to "Everything" in Clear
+# Recent History dialog, provided that the user has modified the default set of
+# history items to clear.
+sanitize-selected-warning = All selected items will be cleared.
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/browser/sidebarMenu.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/browser/sidebarMenu.ftl
new file mode 100644
index 0000000000..ab2cea2d39
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/browser/sidebarMenu.ftl
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+sidebar-menu-bookmarks =
+ .label = Bookmarks
+
+sidebar-menu-history =
+ .label = History
+
+sidebar-menu-synced-tabs =
+ .label = Synced Tabs
+
+sidebar-menu-close =
+ .label = Close Sidebar
diff --git a/third_party/rust/fluent-testing/resources/browser/en-US/preview/interventions.ftl b/third_party/rust/fluent-testing/resources/browser/en-US/preview/interventions.ftl
new file mode 100644
index 0000000000..f17a833a00
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/en-US/preview/interventions.ftl
@@ -0,0 +1,40 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+### These strings appear in Urlbar Interventions. Interventions appear in the
+### Urlbar in response to the user's query. For example, if we detect that the
+### user is searching how to clear their history, we show the Intervention
+### described by clear-data.
+
+intervention-clear-data = Clear your cache, cookies, history and more.
+intervention-clear-data-confirm = Choose What to Clear…
+intervention-refresh-profile = Restore default settings and remove old add-ons for optimal performance.
+intervention-refresh-profile-confirm = Refresh { -brand-short-name }…
+
+## These strings describe Interventions helping the user with the Firefox update
+## process.
+
+## Shown when an update is available to download.
+
+intervention-update-ask = A new version of { -brand-short-name } is available.
+intervention-update-ask-confirm = Install and Restart to Update
+
+## Shown when Firefox does not need to update so instead we offer to refresh
+## the user's profile.
+
+intervention-update-refresh = { -brand-short-name } is up to date. Trying to fix a problem? Restore default settings and remove old add-ons for optimal performance.
+intervention-update-refresh-confirm = Refresh { -brand-short-name }…
+
+## Shown when an update is downloaded and Firefox is ready to install it.
+
+intervention-update-restart = The latest { -brand-short-name } is downloaded and ready to install.
+intervention-update-restart-confirm = Restart to Update
+
+## Shown when Firefox cannot update itself. The button will open the download
+## page on the Firefox website.
+
+intervention-update-web = Get the latest { -brand-short-name } browser.
+intervention-update-web-confirm = Download Now
+
+## \ No newline at end of file
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/branding/brand.ftl b/third_party/rust/fluent-testing/resources/browser/pl/branding/brand.ftl
new file mode 100644
index 0000000000..7f3ed74d06
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/branding/brand.ftl
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+## Firefox Brand
+##
+## Firefox must be treated as a brand, and kept in English.
+## It cannot be:
+## - Declined to adapt to grammatical case.
+## - Transliterated.
+## - Translated.
+##
+## Reference: https://www.mozilla.org/styleguide/communications/translation/
+
+## Firefox and Mozilla Brand
+##
+## Firefox and Mozilla must be treated as a brand.
+##
+## They cannot be:
+## - Transliterated.
+## - Translated.
+##
+## Declension should be avoided where possible, leaving the original
+## brand unaltered in prominent UI positions.
+##
+## For further details, consult:
+## https://mozilla-l10n.github.io/styleguides/mozilla_general/#brands-copyright-and-trademark
+
+-brand-shorter-name = Firefox
+-brand-short-name = Firefox
+-brand-full-name = Mozilla Firefox
+# This brand name can be used in messages where the product name needs to
+# remain unchanged across different versions (Nightly, Beta, etc.).
+-brand-product-name = Firefox
+-vendor-short-name = Mozilla
+trademarkInfo = Firefox oraz logotyp Firefox to znaki towarowe Mozilla Foundation.
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/aboutDialog.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/aboutDialog.ftl
new file mode 100644
index 0000000000..16bde25ba7
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/aboutDialog.ftl
@@ -0,0 +1,60 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+aboutDialog-title =
+ .title = O programie { -brand-full-name }
+
+releaseNotes-link = Informacje o wydaniu
+
+update-checkForUpdatesButton =
+ .label = Sprawdź dostępność aktualizacji
+ .accesskey = S
+
+update-updateButton =
+ .label = Uruchom ponownie, aby uaktualnić przeglądarkę { -brand-shorter-name }
+ .accesskey = U
+
+update-checkingForUpdates = Poszukiwanie aktualizacji…
+update-downloading = <img data-l10n-name="icon"/> Pobieranie aktualizacji — <label data-l10n-name="download-status"/>
+update-applying = Instalowanie aktualizacji…
+
+update-failed = Aktualizacja się nie powiodła. <label data-l10n-name="failed-link">Pobierz najnowszą wersję</label>.
+update-failed-main = Aktualizacja się nie powiodła. <a data-l10n-name="failed-link-main">Pobierz najnowszą wersję</a>.
+
+update-adminDisabled = Aktualizacje zablokowane przez administratora komputera.
+update-noUpdatesFound = { -brand-short-name } jest aktualny.
+update-otherInstanceHandlingUpdates = Inna instancja właśnie aktualizuje program { -brand-short-name }.
+
+update-manual = Aktualizacje dostępne na <label data-l10n-name="manual-link"/>.
+
+update-unsupported = Dalsze aktualizacje na tym systemie nie są możliwe. <label data-l10n-name="unsupported-link">Więcej informacji</label>.
+
+update-restarting = Ponowne uruchamianie…
+
+channel-description = Obecnie korzystasz z kanału aktualizacji „<label data-l10n-name="current-channel"></label>”.
+
+warningDesc-version = { -brand-short-name } jest wersją rozwojową programu i może być niestabilny.
+
+community-exp = <label data-l10n-name="community-exp-mozillaLink">{ -vendor-short-name }</label> jest <label data-l10n-name="community-exp-creditsLink">globalną społecznością</label>, starającą się zapewnić, by Internet pozostał otwarty, publiczny i dostępny dla wszystkich.
+
+community-2 = { -brand-short-name } został opracowany przez <label data-l10n-name="community-mozillaLink">organizację { -vendor-short-name }</label>, która jest <label data-l10n-name="community-creditsLink">globalną społecznością</label>, starającą się zapewnić, by Internet pozostał otwarty, publiczny i dostępny dla wszystkich.
+
+helpus = Chcesz pomóc? <label data-l10n-name="helpus-donateLink">Przekaż datek</label> lub <label data-l10n-name="helpus-getInvolvedLink">dołącz do nas</label>.
+
+bottomLinks-license = Informacje licencyjne
+bottomLinks-rights = Prawa użytkownika
+bottomLinks-privacy = Zasady ochrony prywatności
+
+# Example of resulting string: 66.0.1 (64-bit)
+# Variables:
+# $version (String): version of Firefox, e.g. 66.0.1
+# $bits (Number): bits of the architecture (32 or 64)
+aboutDialog-version = { $version } ({ $bits } bity)
+
+# Example of resulting string: 66.0a1 (2019-01-16) (64-bit)
+# Variables:
+# $version (String): version of Firefox for Nightly builds, e.g. 66.0a1
+# $isodate (String): date in ISO format, e.g. 2019-01-16
+# $bits (Number): bits of the architecture (32 or 64)
+aboutDialog-version-nightly = { $version } ({ $isodate }) ({ $bits } bity)
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/allTabsMenu.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/allTabsMenu.ftl
new file mode 100644
index 0000000000..b1e5cc9c34
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/allTabsMenu.ftl
@@ -0,0 +1,26 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+all-tabs-menu-undo-close-tabs =
+ .label =
+ { $tabCount ->
+ [1] Przywróć zamkniętą kartę
+ [one] Przywróć zamkniętą kartę
+ [few] Przywróć zamknięte karty
+ *[many] Przywróć zamknięte karty
+ }
+
+# "Search" is a verb, as in "Search through tabs".
+all-tabs-menu-search-tabs =
+ .label = Przeszukaj karty
+
+all-tabs-menu-new-user-context =
+ .label = Nowa karta z kontekstem
+
+all-tabs-menu-hidden-tabs =
+ .label = Ukryte karty
+
+all-tabs-menu-manage-user-context =
+ .label = Zarządzaj kontekstami
+ .accesskey = Z
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/appmenu.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/appmenu.ftl
new file mode 100644
index 0000000000..56f23a1075
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/appmenu.ftl
@@ -0,0 +1,34 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+## App Menu
+
+appmenuitem-update-banner =
+ .label-update-downloading = Pobieranie aktualizacji programu { -brand-shorter-name }
+appmenuitem-protection-dashboard-title = Panel ochrony
+appmenuitem-customize-mode =
+ .label = Dostosuj…
+
+## Zoom Controls
+
+appmenuitem-zoom-enlarge =
+ .label = Powiększ
+appmenuitem-zoom-reduce =
+ .label = Pomniejsz
+
+## Firefox Account toolbar button and Sync panel in App menu.
+
+fxa-toolbar-sync-now =
+ .label = Synchronizuj teraz
+
+## What's New panel in App menu.
+
+whatsnew-panel-header = Co nowego
+
+# Checkbox displayed at the bottom of the What's New panel, allowing users to
+# enable/disable What's New notifications.
+whatsnew-panel-footer-checkbox =
+ .label = Powiadamiaj o nowych funkcjach
+ .accesskey = P
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/branding/brandings.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/branding/brandings.ftl
new file mode 100644
index 0000000000..8bcd96f832
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/branding/brandings.ftl
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## The following feature names must be treated as a brand, and kept in English.
+## They cannot be:
+## - Declined to adapt to grammatical case.
+## - Transliterated.
+## - Translated.
+
+## The following feature names must be treated as a brand.
+##
+## They cannot be:
+## - Transliterated.
+## - Translated.
+##
+## Declension should be avoided where possible, leaving the original
+## brand unaltered in prominent UI positions.
+##
+## For further details, consult:
+## https://mozilla-l10n.github.io/styleguides/mozilla_general/#brands-copyright-and-trademark
+
+-facebook-container-brand-name = Facebook Container
+-lockwise-brand-name = Firefox Lockwise
+-lockwise-brand-short-name = Lockwise
+-monitor-brand-name = Firefox Monitor
+-monitor-brand-short-name = Monitor
+-pocket-brand-name = Pocket
+-send-brand-name = Firefox Send
+-screenshots-brand-name = Firefox Screenshots
+-mozilla-vpn-brand-name = Mozilla VPN
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/branding/sync-brand.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/branding/sync-brand.ftl
new file mode 100644
index 0000000000..bdcdb77388
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/branding/sync-brand.ftl
@@ -0,0 +1,107 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+-sync-brand-short-name =
+ { $case ->
+ *[nom]
+ { $capitalization ->
+ *[upper] Synchronizacja
+ [lower] synchronizacja
+ }
+ [gen]
+ { $capitalization ->
+ *[upper] Synchronizacji
+ [lower] synchronizacji
+ }
+ [dat]
+ { $capitalization ->
+ *[upper] Synchronizacji
+ [lower] synchronizacji
+ }
+ [acc]
+ { $capitalization ->
+ *[upper] Synchronizację
+ [lower] synchronizację
+ }
+ [ins]
+ { $capitalization ->
+ *[upper] Synchronizacją
+ [lower] synchronizacją
+ }
+ [loc]
+ { $capitalization ->
+ *[upper] Synchronizacji
+ [lower] synchronizacji
+ }
+ }
+# “Sync” can be localized, “Firefox” must be treated as a brand,
+# and kept in English.
+-sync-brand-name =
+ { $case ->
+ *[nom]
+ { $capitalization ->
+ *[upper] Synchronizacja Firefoksa
+ [lower] synchronizacja Firefoksa
+ }
+ [gen]
+ { $capitalization ->
+ *[upper] Synchronizacji Firefoksa
+ [lower] synchronizacji Firefoksa
+ }
+ [dat]
+ { $capitalization ->
+ *[upper] Synchronizacji Firefoksa
+ [lower] synchronizacji Firefoksa
+ }
+ [acc]
+ { $capitalization ->
+ *[upper] Synchronizację Firefoksa
+ [lower] synchronizację Firefoksa
+ }
+ [ins]
+ { $capitalization ->
+ *[upper] Synchronizacją Firefoksa
+ [lower] synchronizacją Firefoksa
+ }
+ [loc]
+ { $capitalization ->
+ *[upper] Synchronizacji Firefoksa
+ [lower] synchronizacji Firefoksa
+ }
+ }
+# “Account” can be localized, “Firefox” must be treated as a brand,
+# and kept in English.
+-fxaccount-brand-name =
+ { $case ->
+ *[nom]
+ { $capitalization ->
+ *[upper] Konto Firefoksa
+ [lower] konto Firefoksa
+ }
+ [gen]
+ { $capitalization ->
+ *[upper] Konta Firefoksa
+ [lower] konta Firefoksa
+ }
+ [dat]
+ { $capitalization ->
+ *[upper] Kontu Firefoksa
+ [lower] kontu Firefoksa
+ }
+ [acc]
+ { $capitalization ->
+ *[upper] Konto Firefoksa
+ [lower] konto Firefoksa
+ }
+ [ins]
+ { $capitalization ->
+ *[upper] Kontem Firefoksa
+ [lower] kontem Firefoksa
+ }
+ [loc]
+ { $capitalization ->
+ *[upper] Koncie Firefoksa
+ [lower] koncie Firefoksa
+ }
+ }
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/browser.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/browser.ftl
new file mode 100644
index 0000000000..7d4f5c68e6
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/browser.ftl
@@ -0,0 +1,574 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+## The main browser window's title
+
+# These are the default window titles everywhere except macOS. The first two
+# attributes are used when the web content opened has no title:
+#
+# default - "Mozilla Firefox"
+# private - "Mozilla Firefox (Private Browsing)"
+#
+# The last two are for use when there *is* a content title.
+# Variables:
+# $content-title (String): the title of the web content.
+browser-main-window =
+ .data-title-default = { -brand-full-name }
+ .data-title-private = { -brand-full-name } (tryb prywatny)
+ .data-content-title-default = { $content-title } — { -brand-full-name }
+ .data-content-title-private = { $content-title } — { -brand-full-name } (tryb prywatny)
+# These are the default window titles on macOS. The first two are for use when
+# there is no content title:
+#
+# "default" - "Mozilla Firefox"
+# "private" - "Mozilla Firefox — (Private Browsing)"
+#
+# The last two are for use when there *is* a content title.
+# Do not use the brand name in the last two attributes, as we do on non-macOS.
+#
+# Also note the other subtle difference here: we use a `-` to separate the
+# brand name from `(Private Browsing)`, which does not happen on other OSes.
+#
+# Variables:
+# $content-title (String): the title of the web content.
+browser-main-window-mac =
+ .data-title-default = { -brand-full-name }
+ .data-title-private = { -brand-full-name } — (tryb prywatny)
+ .data-content-title-default = { $content-title }
+ .data-content-title-private = { $content-title } — (tryb prywatny)
+# This gets set as the initial title, and is overridden as soon as we start
+# updating the titlebar based on loaded tabs or private browsing state.
+# This should match the `data-title-default` attribute in both
+# `browser-main-window` and `browser-main-window-mac`.
+browser-main-window-title = { -brand-full-name }
+
+##
+
+urlbar-identity-button =
+ .aria-label = Wyświetl informacje o stronie
+
+## Tooltips for images appearing in the address bar
+
+urlbar-services-notification-anchor =
+ .tooltiptext = Wyświetl zapytanie instalacji usługi
+urlbar-web-notification-anchor =
+ .tooltiptext = Określ, czy witryna ma prawo wyświetlać powiadomienia
+urlbar-midi-notification-anchor =
+ .tooltiptext = Otwórz panel MIDI
+urlbar-eme-notification-anchor =
+ .tooltiptext = Zarządzaj ustawieniami DRM
+urlbar-web-authn-anchor =
+ .tooltiptext = Otwórz panel Web Authentication
+urlbar-canvas-notification-anchor =
+ .tooltiptext = Zarządzaj uprawnieniami odczytu danych canvas
+urlbar-web-rtc-share-microphone-notification-anchor =
+ .tooltiptext = Zarządzaj udostępnianiem mikrofonu tej witrynie
+urlbar-default-notification-anchor =
+ .tooltiptext = Wyświetl powiadomienie
+urlbar-geolocation-notification-anchor =
+ .tooltiptext = Wyświetl zapytanie o położenie
+urlbar-xr-notification-anchor =
+ .tooltiptext = Zarządzaj uprawnieniami rzeczywistości wirtualnej
+urlbar-storage-access-anchor =
+ .tooltiptext = Zarządzaj uprawnieniami śledzenia aktywności przeglądania
+urlbar-translate-notification-anchor =
+ .tooltiptext = Przetłumacz tę stronę
+urlbar-web-rtc-share-screen-notification-anchor =
+ .tooltiptext = Zarządzaj udostępnianiem tej witrynie okien i ekranu
+urlbar-indexed-db-notification-anchor =
+ .tooltiptext = Wyświetl zapytanie o przechowywanie danych offline
+urlbar-password-notification-anchor =
+ .tooltiptext = Określ, czy zachować hasło
+urlbar-translated-notification-anchor =
+ .tooltiptext = Zarządzaj ustawieniami tłumaczenia
+urlbar-plugins-notification-anchor =
+ .tooltiptext = Zarządzaj wtyczkami używanymi na tej stronie
+urlbar-web-rtc-share-devices-notification-anchor =
+ .tooltiptext = Zarządzaj udostępnianiem tej witrynie kamery i mikrofonu
+urlbar-autoplay-notification-anchor =
+ .tooltiptext = Wyświetl zapytanie o automatyczne odtwarzanie
+urlbar-persistent-storage-notification-anchor =
+ .tooltiptext = Przechowywanie danych na komputerze
+urlbar-addons-notification-anchor =
+ .tooltiptext = Wyświetl zapytanie o instalację dodatków
+urlbar-tip-help-icon =
+ .title = Pomoc
+urlbar-search-tips-confirm = OK
+# Read out before Urlbar Tip text content so screenreader users know the
+# subsequent text is a tip offered by the browser. It should end in a colon or
+# localized equivalent.
+urlbar-tip-icon-description =
+ .alt = Wskazówka:
+
+## Prompts users to use the Urlbar when they open a new tab or visit the
+## homepage of their default search engine.
+## Variables:
+## $engineName (String): The name of the user's default search engine. e.g. "Google" or "DuckDuckGo".
+
+urlbar-search-tips-onboard = Pisz mniej, wyszukuj więcej: szukaj w { $engineName } prosto z paska adresu
+urlbar-search-tips-redirect-2 = Zacznij szukać na pasku adresu, aby uzyskać podpowiedzi od wyszukiwarki { $engineName } i wyniki na podstawie historii przeglądania
+# Prompts users to use the Urlbar when they are typing in the domain of a
+# search engine, e.g. google.com or amazon.com.
+urlbar-tabtosearch-onboard = Kliknij ten skrót, aby szybciej znaleźć to, czego potrzebujesz
+
+## Local search mode indicator labels in the urlbar
+
+urlbar-search-mode-bookmarks = Zakładki
+urlbar-search-mode-tabs = Karty
+urlbar-search-mode-history = Historia
+
+##
+
+urlbar-geolocation-blocked =
+ .tooltiptext = Udostępnianie położenia tej witrynie zostało zablokowane
+urlbar-xr-blocked =
+ .tooltiptext = Dostęp do urządzenia rzeczywistości wirtualnej dla tej witryny został zablokowany
+urlbar-web-notifications-blocked =
+ .tooltiptext = Powiadomienia z tej witryny zostały zablokowane
+urlbar-camera-blocked =
+ .tooltiptext = Udostępnianie kamery tej witrynie zostało zablokowane
+urlbar-microphone-blocked =
+ .tooltiptext = Udostępnianie mikrofonu tej witrynie zostało zablokowane
+urlbar-screen-blocked =
+ .tooltiptext = Udostępnianie obrazu ekranu tej witrynie zostało zablokowane
+urlbar-persistent-storage-blocked =
+ .tooltiptext = Przechowywanie danych na komputerze przez tę witrynę zostało zablokowane
+urlbar-popup-blocked =
+ .tooltiptext = Wyskakujące okna na tej witrynie są blokowane
+urlbar-autoplay-media-blocked =
+ .tooltiptext = Automatyczne odtwarzanie treści z dźwiękiem przez tę witrynę zostało zablokowane
+urlbar-canvas-blocked =
+ .tooltiptext = Odczytywanie danych canvas przez witrynę zostało zablokowane
+urlbar-midi-blocked =
+ .tooltiptext = Dostęp do urządzeń MIDI dla tej witryny został zablokowany
+urlbar-install-blocked =
+ .tooltiptext = Instalacja dodatków przez tę witrynę została zablokowana
+# Variables
+# $shortcut (String) - A keyboard shortcut for the edit bookmark command.
+urlbar-star-edit-bookmark =
+ .tooltiptext = Edytuj zakładkę ({ $shortcut })
+# Variables
+# $shortcut (String) - A keyboard shortcut for the add bookmark command.
+urlbar-star-add-bookmark =
+ .tooltiptext = Dodaj zakładkę do tej strony ({ $shortcut })
+
+## Page Action Context Menu
+
+page-action-add-to-urlbar =
+ .label = Dodaj do paska adresu
+page-action-manage-extension =
+ .label = Zarządzaj rozszerzeniem…
+page-action-remove-from-urlbar =
+ .label = Usuń z paska adresu
+page-action-remove-extension =
+ .label = Usuń rozszerzenie
+
+## Page Action menu
+
+# Variables
+# $tabCount (integer) - Number of tabs selected
+page-action-send-tabs-panel =
+ .label =
+ { $tabCount ->
+ [one] Wyślij stronę na urządzenie
+ [few] Wyślij { $tabCount } strony na urządzenie
+ *[many] Wyślij { $tabCount } stron na urządzenie
+ }
+page-action-send-tabs-urlbar =
+ .tooltiptext =
+ { $tabCount ->
+ [one] Wyślij stronę na urządzenie
+ [few] Wyślij { $tabCount } strony na urządzenie
+ *[many] Wyślij { $tabCount } stron na urządzenie
+ }
+page-action-pocket-panel =
+ .label = Wyślij stronę do { -pocket-brand-name }
+page-action-copy-url-panel =
+ .label = Kopiuj odnośnik
+page-action-copy-url-urlbar =
+ .tooltiptext = Kopiuj odnośnik
+page-action-email-link-panel =
+ .label = Wyślij odnośnik…
+page-action-email-link-urlbar =
+ .tooltiptext = Wyślij odnośnik…
+page-action-share-url-panel =
+ .label = Udostępnij
+page-action-share-url-urlbar =
+ .tooltiptext = Udostępnij
+page-action-share-more-panel =
+ .label = Więcej…
+page-action-send-tab-not-ready =
+ .label = Synchronizowanie urządzeń…
+# "Pin" is being used as a metaphor for expressing the fact that these tabs
+# are "pinned" to the left edge of the tabstrip. Really we just want the
+# string to express the idea that this is a lightweight and reversible
+# action that keeps your tab where you can reach it easily.
+page-action-pin-tab-panel =
+ .label = Przypnij kartę
+page-action-pin-tab-urlbar =
+ .tooltiptext = Przypnij kartę
+page-action-unpin-tab-panel =
+ .label = Odepnij kartę
+page-action-unpin-tab-urlbar =
+ .tooltiptext = Odepnij kartę
+
+## Auto-hide Context Menu
+
+full-screen-autohide =
+ .label = Ukryj paski narzędzi
+ .accesskey = U
+full-screen-exit =
+ .label = Opuść tryb pełnoekranowy
+ .accesskey = O
+
+## Search Engine selection buttons (one-offs)
+
+# This string prompts the user to use the list of search shortcuts in
+# the Urlbar and searchbar.
+search-one-offs-with-title = Tym razem szukaj w:
+# This string won't wrap, so if the translated string is longer,
+# consider translating it as if it said only "Search Settings".
+search-one-offs-change-settings-button =
+ .label = Ustawienia wyszukiwania
+search-one-offs-change-settings-compact-button =
+ .tooltiptext = Zmień ustawienia wyszukiwania
+search-one-offs-context-open-new-tab =
+ .label = Szukaj w nowej karcie
+ .accesskey = S
+search-one-offs-context-set-as-default =
+ .label = Ustaw jako domyślną wyszukiwarkę
+ .accesskey = U
+search-one-offs-context-set-as-default-private =
+ .label = Ustaw jako domyślną wyszukiwarkę w prywatnych oknach
+ .accesskey = w
+# Search engine one-off buttons with an @alias shortcut/keyword.
+# Variables:
+# $engineName (String): The name of the engine.
+# $alias (String): The @alias shortcut/keyword.
+search-one-offs-engine-with-alias =
+ .tooltiptext = { $engineName } ({ $alias })
+
+## Local search mode one-off buttons
+## Variables:
+## $restrict (String): The restriction token corresponding to the search mode.
+## Restriction tokens are special characters users can type in the urlbar to
+## restrict their searches to certain sources (e.g., "*" to search only
+## bookmarks).
+
+search-one-offs-bookmarks =
+ .tooltiptext = Zakładki ({ $restrict })
+search-one-offs-tabs =
+ .tooltiptext = Karty ({ $restrict })
+search-one-offs-history =
+ .tooltiptext = Historia ({ $restrict })
+
+## Bookmark Panel
+
+bookmark-panel-show-editor-checkbox =
+ .label = Wyświetlanie tego okna podczas dodawania
+ .accesskey = W
+bookmark-panel-done-button =
+ .label = Gotowe
+# Width of the bookmark panel.
+# Should be large enough to fully display the Done and
+# Cancel/Remove Bookmark buttons.
+bookmark-panel =
+ .style = min-width: 25em
+
+## Identity Panel
+
+identity-connection-not-secure = Niezabezpieczone połączenie
+identity-connection-secure = Zabezpieczone połączenie
+identity-connection-internal = To jest strona programu { -brand-short-name }.
+identity-connection-file = Strona wczytana z tego komputera.
+identity-extension-page = Ta strona została wczytana przez rozszerzenie.
+identity-active-blocked = { -brand-short-name } zablokował elementy tej strony, które nie były przesłane w sposób bezpieczny.
+identity-custom-root = Połączenie zweryfikowane przez wystawcę certyfikatu, który nie jest rozpoznawany przez Mozillę.
+identity-passive-loaded = Niektóre elementy tej strony (np. obrazy) nie były przesłane w sposób bezpieczny.
+identity-active-loaded = Ochrona na tej stronie została wyłączona przez użytkownika.
+identity-weak-encryption = Strona używa słabego szyfrowania.
+identity-insecure-login-forms = Dane logowania wprowadzone na tej stronie nie są chronione.
+identity-https-only-connection-upgraded = (przełączono na protokół HTTPS)
+identity-https-only-label = Tryb używania wyłącznie protokołu HTTPS
+identity-https-only-dropdown-on =
+ .label = Włączony
+identity-https-only-dropdown-off =
+ .label = Wyłączony
+identity-https-only-dropdown-off-temporarily =
+ .label = Tymczasowo wyłączony
+identity-https-only-info-turn-on2 = Włącz tryb używania wyłącznie protokołu HTTPS dla tej witryny, jeśli chcesz, aby { -brand-short-name } przełączał na zabezpieczone połączenie, kiedy to możliwe.
+identity-https-only-info-turn-off2 = Jeśli strona wydaje się niepoprawnie działać, możesz wyłączyć tryb używania wyłącznie protokołu HTTPS dla tej witryny, aby odświeżyć ją za pomocą niezabezpieczonego protokołu HTTP.
+identity-https-only-info-no-upgrade = Nie można przełączyć połączenia z protokołu HTTP.
+identity-permissions =
+ .value = Uprawnienia
+identity-permissions-storage-access-header = Ciasteczka między witrynami
+identity-permissions-storage-access-hint = Te strony mogą używać ciasteczek i danych między witrynami, kiedy jesteś na tej witrynie.
+identity-permissions-reload-hint = Ponowne wczytanie strony może być konieczne, aby wprowadzone zmiany przyniosły skutek.
+identity-permissions-empty = Witryna korzysta z domyślnych uprawnień.
+identity-clear-site-data =
+ .label = Wyczyść ciasteczka i dane stron…
+identity-connection-not-secure-security-view = Połączenie z tą witryną nie jest zabezpieczone.
+identity-connection-verified = Połączenie z tą witryną jest zabezpieczone.
+identity-ev-owner-label = Certyfikat wystawiony dla:
+identity-description-custom-root = Mozilla nie rozpoznaje tego wystawcy certyfikatu. Mógł zostać dodany przez system operacyjny lub administratora. <label data-l10n-name="link">Więcej informacji</label>
+identity-remove-cert-exception =
+ .label = Usuń wyjątek
+ .accesskey = U
+identity-description-insecure = Prywatność podczas łączenia się z tą witryną nie jest chroniona. Przesyłane informacje (np. hasła, wiadomości, numery kart) mogą być dostępne dla innych.
+identity-description-insecure-login-forms = Dane logowania wprowadzone na tej stronie nie są bezpieczne i mogą być dostępne dla innych.
+identity-description-weak-cipher-intro = Połączenie z tą witryną nie zapewnia prywatności, ponieważ szyfrowanie nie jest wystarczające.
+identity-description-weak-cipher-risk = Informacje na witrynie mogą być dostępne dla innych, a jej działanie modyfikowane.
+identity-description-active-blocked = { -brand-short-name } zablokował elementy tej strony, które nie były przesłane w sposób bezpieczny. <label data-l10n-name="link">Więcej informacji</label>
+identity-description-passive-loaded = Połączenie z tą witryną nie zapewnia prywatności, a przesyłane informacje mogą być dostępne dla innych.
+identity-description-passive-loaded-insecure = Niektóre elementy tej witryny (np. obrazy) nie były przesłane w sposób bezpieczny. <label data-l10n-name="link">Więcej informacji</label>
+identity-description-passive-loaded-mixed = { -brand-short-name } zablokował niektóre elementy strony, mimo to nie wszystkie pozostałe elementy były przesłane w sposób bezpieczny (np. obrazy). <label data-l10n-name="link">Więcej informacji</label>
+identity-description-active-loaded = Witryna zawiera elementy, które nie były przesłane w sposób bezpieczny (np. skrypty) i połączenie z nią nie zapewnia prywatności.
+identity-description-active-loaded-insecure = Przesyłane informacje (np. hasła, wiadomości, numery kart) mogą być dostępne dla innych.
+identity-learn-more =
+ .value = Więcej informacji
+identity-disable-mixed-content-blocking =
+ .label = Tymczasowo wyłącz ochronę
+ .accesskey = T
+identity-enable-mixed-content-blocking =
+ .label = Włącz ochronę
+ .accesskey = W
+identity-more-info-link-text =
+ .label = Więcej informacji…
+
+## Window controls
+
+browser-window-minimize-button =
+ .tooltiptext = Minimalizuj
+browser-window-maximize-button =
+ .tooltiptext = Maksymalizuj
+browser-window-restore-down-button =
+ .tooltiptext = Przywróć w dół
+browser-window-close-button =
+ .tooltiptext = Zamknij
+
+## Bookmarks toolbar items
+
+browser-import-button2 =
+ .label = Importuj zakładki…
+ .tooltiptext = Zaimportuj zakładki z innej przeglądarki do przeglądarki { -brand-short-name }
+bookmarks-toolbar-empty-message = Umieść swoje zakładki na tym pasku zakładek, aby mieć do nich szybki dostęp. <a data-l10n-name="manage-bookmarks">Zarządzaj zakładkami…</a>
+
+## WebRTC Pop-up notifications
+
+popup-select-camera =
+ .value = Kamera do udostępnienia:
+ .accesskey = K
+popup-select-microphone =
+ .value = Mikrofon do udostępnienia:
+ .accesskey = M
+popup-all-windows-shared = Wszystkie widoczne na ekranie okna zostaną udostępnione.
+popup-screen-sharing-not-now =
+ .label = Nie teraz
+ .accesskey = N
+popup-screen-sharing-never =
+ .label = Nigdy nie pozwalaj
+ .accesskey = d
+popup-silence-notifications-checkbox = Wyłącz powiadomienia przeglądarki { -brand-short-name } podczas udostępniania
+popup-silence-notifications-checkbox-warning = { -brand-short-name } nie będzie wyświetlał powiadomień w trakcie udostępniania.
+
+## WebRTC window or screen share tab switch warning
+
+sharing-warning-window = { -brand-short-name } jest udostępniany. Inni będą widzieć, że przechodzisz do nowej karty.
+sharing-warning-screen = Cały ekran jest udostępniany. Inni będą widzieć, że przechodzisz do nowej karty.
+sharing-warning-proceed-to-tab =
+ .label = Przejdź do karty
+sharing-warning-disable-for-session =
+ .label = Wyłącz ochronę udostępniania na czas tej sesji
+
+## DevTools F12 popup
+
+enable-devtools-popup-description = Aby móc użyć skrótu F12, najpierw otwórz narzędzia dla programistów w menu „Dla twórców witryn”.
+
+## URL Bar
+
+urlbar-default-placeholder =
+ .defaultPlaceholder = Wprowadź adres lub szukaj
+# This placeholder is used when not in search mode and the user's default search
+# engine is unknown.
+urlbar-placeholder =
+ .placeholder = Wprowadź adres lub szukaj
+# This placeholder is used in search mode with search engines that search the
+# entire web.
+# Variables
+# $name (String): the name of a search engine that searches the entire Web
+# (e.g. Google).
+urlbar-placeholder-search-mode-web-2 =
+ .placeholder = Szukaj w Internecie
+ .aria-label = Szukaj w { $name }
+# This placeholder is used in search mode with search engines that search a
+# specific site (e.g., Amazon).
+# Variables
+# $name (String): the name of a search engine that searches a specific site
+# (e.g. Amazon).
+urlbar-placeholder-search-mode-other-engine =
+ .placeholder = Szukaj
+ .aria-label = Szukaj na witrynie { $name }
+# This placeholder is used when searching bookmarks.
+urlbar-placeholder-search-mode-other-bookmarks =
+ .placeholder = Szukaj
+ .aria-label = Szukaj zakładek
+# This placeholder is used when searching history.
+urlbar-placeholder-search-mode-other-history =
+ .placeholder = Szukaj
+ .aria-label = Szukaj w historii
+# This placeholder is used when searching open tabs.
+urlbar-placeholder-search-mode-other-tabs =
+ .placeholder = Szukaj
+ .aria-label = Szukaj kart
+# Variables
+# $name (String): the name of the user's default search engine
+urlbar-placeholder-with-name =
+ .placeholder = Wprowadź adres lub szukaj w { $name }
+urlbar-remote-control-notification-anchor =
+ .tooltiptext = Przeglądarka jest zdalnie zarządzana
+urlbar-permissions-granted =
+ .tooltiptext = Witryna korzysta z dodatkowych uprawnień.
+urlbar-switch-to-tab =
+ .value = Przełącz na kartę:
+# Used to indicate that a selected autocomplete entry is provided by an extension.
+urlbar-extension =
+ .value = Rozszerzenie:
+urlbar-go-button =
+ .tooltiptext = Przejdź do strony o podanym adresie
+urlbar-page-action-button =
+ .tooltiptext = Interakcje
+urlbar-pocket-button =
+ .tooltiptext = Wyślij do { -pocket-brand-name }
+
+## Action text shown in urlbar results, usually appended after the search
+## string or the url, like "result value - action text".
+
+# Used when the private browsing engine differs from the default engine.
+# The "with" format was chosen because the search engine name can end with
+# "Search", and we would like to avoid strings like "Search MSN Search".
+# Variables
+# $engine (String): the name of a search engine
+urlbar-result-action-search-in-private-w-engine = szukaj w { $engine } w prywatnym oknie
+# Used when the private browsing engine is the same as the default engine.
+urlbar-result-action-search-in-private = szukaj w prywatnym oknie
+# The "with" format was chosen because the search engine name can end with
+# "Search", and we would like to avoid strings like "Search MSN Search".
+# Variables
+# $engine (String): the name of a search engine
+urlbar-result-action-search-w-engine = szukaj w { $engine }
+urlbar-result-action-sponsored = sponsorowane
+urlbar-result-action-switch-tab = przełącz na kartę
+urlbar-result-action-visit = otwórz stronę
+# Directs a user to press the Tab key to perform a search with the specified
+# engine.
+# Variables
+# $engine (String): the name of a search engine that searches the entire Web
+# (e.g. Google).
+urlbar-result-action-before-tabtosearch-web = naciśnij Tab, aby szukać w { $engine }
+# Directs a user to press the Tab key to perform a search with the specified
+# engine.
+# Variables
+# $engine (String): the name of a search engine that searches a specific site
+# (e.g. Amazon).
+urlbar-result-action-before-tabtosearch-other = naciśnij Tab, aby szukać na witrynie { $engine }
+# Variables
+# $engine (String): the name of a search engine that searches the entire Web
+# (e.g. Google).
+urlbar-result-action-tabtosearch-web = szukaj w { $engine } prosto z paska adresu
+# Variables
+# $engine (String): the name of a search engine that searches a specific site
+# (e.g. Amazon).
+urlbar-result-action-tabtosearch-other-engine = szukaj na witrynie { $engine } prosto z paska adresu
+
+## Action text shown in urlbar results, usually appended after the search
+## string or the url, like "result value - action text".
+## In these actions "Search" is a verb, followed by where the search is performed.
+
+urlbar-result-action-search-bookmarks = szukaj w zakładkach
+urlbar-result-action-search-history = szukaj w historii
+urlbar-result-action-search-tabs = szukaj w kartach
+
+## Full Screen and Pointer Lock UI
+
+# Please ensure that the domain stays in the `<span data-l10n-name="domain">` markup.
+# Variables
+# $domain (String): the domain that is full screen, e.g. "mozilla.org"
+fullscreen-warning-domain = <span data-l10n-name="domain">{ $domain }</span> jest teraz w trybie pełnoekranowym
+fullscreen-warning-no-domain = Dokument jest teraz wyświetlany w trybie pełnoekranowym
+fullscreen-exit-button = Opuść tryb pełnoekranowy (Esc)
+# "esc" is lowercase on mac keyboards, but uppercase elsewhere.
+fullscreen-exit-mac-button = Opuść tryb pełnoekranowy (esc)
+# Please ensure that the domain stays in the `<span data-l10n-name="domain">` markup.
+# Variables
+# $domain (String): the domain that is using pointer-lock, e.g. "mozilla.org"
+pointerlock-warning-domain = <span data-l10n-name="domain">{ $domain }</span> kontroluje teraz kursor. Naciśnij klawisz Esc, aby przejąć nad nim kontrolę.
+pointerlock-warning-no-domain = Dokument kontroluje teraz kursor. Naciśnij klawisz Esc, aby przejąć nad nim kontrolę.
+
+## Bookmarks panels, menus and toolbar
+
+bookmarks-show-all-bookmarks =
+ .label = Wyświetl wszystkie zakładki
+bookmarks-recent-bookmarks =
+ .value = Ostatnio dodane
+bookmarks-toolbar-chevron =
+ .tooltiptext = Wyświetl więcej zakładek
+bookmarks-sidebar-content =
+ .aria-label = Zakładki
+bookmarks-menu-button =
+ .label = Menu zakładki
+bookmarks-other-bookmarks-menu =
+ .label = Pozostałe zakładki
+bookmarks-mobile-bookmarks-menu =
+ .label = Zakładki z telefonu
+bookmarks-tools-sidebar-visibility =
+ .label =
+ { $isVisible ->
+ [true] Ukryj panel zakładek
+ *[other] Wyświetl panel zakładek
+ }
+bookmarks-tools-toolbar-visibility =
+ .label =
+ { $isVisible ->
+ [true] Ukryj pasek zakładek
+ *[other] Wyświetl pasek zakładek
+ }
+bookmarks-tools-menu-button-visibility =
+ .label =
+ { $isVisible ->
+ [true] Usuń menu Zakładki z paska narzędzi
+ *[other] Dodaj menu Zakładki do paska narzędzi
+ }
+bookmarks-search =
+ .label = Szukaj w zakładkach
+bookmarks-tools =
+ .label = Narzędzia zakładek
+# The aria-label is a spoken label that should not include the word "toolbar" or
+# such, because screen readers already know that this container is a toolbar.
+# This avoids double-speaking.
+bookmarks-toolbar =
+ .toolbarname = Pasek zakładek
+ .accesskey = z
+ .aria-label = Zakładki
+bookmarks-toolbar-menu =
+ .label = Pasek zakładek
+bookmarks-toolbar-placeholder =
+ .title = Elementy paska zakładek
+bookmarks-toolbar-placeholder-button =
+ .label = Elementy paska zakładek
+
+## Library Panel items
+
+library-bookmarks-menu =
+ .label = Zakładki
+library-bookmarks-bookmark-this-page =
+ .label = Dodaj zakładkę
+library-bookmarks-bookmark-edit =
+ .label = Edytuj zakładkę
+
+## More items
+
+more-menu-go-offline =
+ .label = Pracuj w trybie offline
+ .accesskey = c
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/browserContext.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/browserContext.ftl
new file mode 100644
index 0000000000..f1e4773a13
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/browserContext.ftl
@@ -0,0 +1,319 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+navbar-tooltip-instruction =
+ .value =
+ { PLATFORM() ->
+ [macos] Rozwiń, by wyświetlić historię
+ *[other] Kliknij prawym przyciskiem lub rozwiń, by wyświetlić historię
+ }
+
+## Back
+
+main-context-menu-back =
+ .tooltiptext = Przejdź do poprzedniej strony
+ .aria-label = Wstecz
+ .accesskey = W
+navbar-tooltip-back =
+ .value = { main-context-menu-back.tooltiptext }
+toolbar-button-back =
+ .label = { main-context-menu-back.aria-label }
+
+## Forward
+
+main-context-menu-forward =
+ .tooltiptext = Przejdź do następnej strony
+ .aria-label = Do przodu
+ .accesskey = D
+navbar-tooltip-forward =
+ .value = { main-context-menu-forward.tooltiptext }
+toolbar-button-forward =
+ .label = { main-context-menu-forward.aria-label }
+
+## Reload
+
+main-context-menu-reload =
+ .aria-label = Odśwież
+ .accesskey = O
+toolbar-button-reload =
+ .label = { main-context-menu-reload.aria-label }
+
+## Stop
+
+main-context-menu-stop =
+ .aria-label = Zatrzymaj
+ .accesskey = Z
+toolbar-button-stop =
+ .label = { main-context-menu-stop.aria-label }
+
+## Stop-Reload Button
+
+toolbar-button-stop-reload =
+ .title = { main-context-menu-reload.aria-label }
+
+## Save Page
+
+main-context-menu-page-save =
+ .label = Zapisz stronę jako…
+ .accesskey = s
+toolbar-button-page-save =
+ .label = { main-context-menu-page-save.label }
+
+## Simple menu items
+
+main-context-menu-bookmark-add =
+ .aria-label = Dodaj zakładkę do tej strony
+ .accesskey = D
+ .tooltiptext = Dodaj zakładkę do tej strony
+# Variables
+# $shortcut (String) - A keyboard shortcut for the add bookmark command.
+main-context-menu-bookmark-add-with-shortcut =
+ .aria-label = Dodaj zakładkę do tej strony
+ .accesskey = D
+ .tooltiptext = Dodaj zakładkę do tej strony ({ $shortcut })
+main-context-menu-bookmark-change =
+ .aria-label = Edytuj zakładkę
+ .accesskey = D
+ .tooltiptext = Edytuj zakładkę
+# Variables
+# $shortcut (String) - A keyboard shortcut for the edit bookmark command.
+main-context-menu-bookmark-change-with-shortcut =
+ .aria-label = Edytuj zakładkę
+ .accesskey = D
+ .tooltiptext = Edytuj zakładkę ({ $shortcut })
+main-context-menu-open-link =
+ .label = Otwórz odnośnik
+ .accesskey = O
+main-context-menu-open-link-new-tab =
+ .label = Otwórz odnośnik w nowej karcie
+ .accesskey = j
+main-context-menu-open-link-container-tab =
+ .label = Otwórz odnośnik w nowej karcie w nowym kontekście
+ .accesskey = k
+main-context-menu-open-link-new-window =
+ .label = Otwórz odnośnik w nowym oknie
+ .accesskey = n
+main-context-menu-open-link-new-private-window =
+ .label = Otwórz odnośnik w nowym oknie w trybie prywatnym
+ .accesskey = w
+main-context-menu-bookmark-this-link =
+ .label = Dodaj zakładkę do odnośnika
+ .accesskey = D
+main-context-menu-save-link =
+ .label = Zapisz element docelowy jako…
+ .accesskey = s
+main-context-menu-save-link-to-pocket =
+ .label = Wyślij odnośnik do { -pocket-brand-name }
+ .accesskey = o
+
+## The access keys for "Copy Link Location" and "Copy Email Address"
+## should be the same if possible; the two context menu items
+## are mutually exclusive.
+
+main-context-menu-copy-email =
+ .label = Kopiuj adres e-mail
+ .accesskey = a
+main-context-menu-copy-link =
+ .label = Kopiuj adres odnośnika
+ .accesskey = a
+
+## Media (video/audio) controls
+##
+## The accesskey for "Play" and "Pause" are the
+## same because the two context-menu items are
+## mutually exclusive.
+
+main-context-menu-media-play =
+ .label = Odtwórz
+ .accesskey = z
+main-context-menu-media-pause =
+ .label = Wstrzymaj
+ .accesskey = W
+
+##
+
+main-context-menu-media-mute =
+ .label = Wycisz
+ .accesskey = c
+main-context-menu-media-unmute =
+ .label = Włącz dźwięk
+ .accesskey = c
+main-context-menu-media-play-speed =
+ .label = Szybkość odtwarzania
+ .accesskey = S
+main-context-menu-media-play-speed-slow =
+ .label = Zmniejszona (0,5×)
+ .accesskey = Z
+main-context-menu-media-play-speed-normal =
+ .label = Normalna
+ .accesskey = N
+main-context-menu-media-play-speed-fast =
+ .label = Zwiększona (1,25×)
+ .accesskey = k
+main-context-menu-media-play-speed-faster =
+ .label = Wysoka (1,5×)
+ .accesskey = W
+# "Ludicrous" is a reference to the movie "Space Balls" and is meant
+# to say that this speed is very fast.
+main-context-menu-media-play-speed-fastest =
+ .label = Absurdalna (2×)
+ .accesskey = A
+main-context-menu-media-loop =
+ .label = Zapętl
+ .accesskey = Z
+
+## The access keys for "Show Controls" and "Hide Controls" are the same
+## because the two context-menu items are mutually exclusive.
+
+main-context-menu-media-show-controls =
+ .label = Wyświetl elementy sterujące
+ .accesskey = e
+main-context-menu-media-hide-controls =
+ .label = Ukryj elementy sterujące
+ .accesskey = e
+
+##
+
+main-context-menu-media-video-fullscreen =
+ .label = Tryb pełnoekranowy
+ .accesskey = n
+main-context-menu-media-video-leave-fullscreen =
+ .label = Opuść tryb pełnoekranowy
+ .accesskey = u
+# This is used when right-clicking on a video in the
+# content area when the Picture-in-Picture feature is enabled.
+main-context-menu-media-pip =
+ .label = Obraz w obrazie
+ .accesskey = O
+main-context-menu-image-reload =
+ .label = Odśwież obraz
+ .accesskey = O
+main-context-menu-image-view =
+ .label = Pokaż obraz
+ .accesskey = P
+main-context-menu-video-view =
+ .label = Pokaż wideo
+ .accesskey = k
+main-context-menu-image-copy =
+ .label = Kopiuj obraz
+ .accesskey = r
+main-context-menu-image-copy-location =
+ .label = Kopiuj adres obrazu
+ .accesskey = b
+main-context-menu-video-copy-location =
+ .label = Kopiuj adres wideo
+ .accesskey = u
+main-context-menu-audio-copy-location =
+ .label = Kopiuj adres dźwięku
+ .accesskey = u
+main-context-menu-image-save-as =
+ .label = Zapisz obraz jako…
+ .accesskey = Z
+main-context-menu-image-email =
+ .label = Wyślij obraz…
+ .accesskey = o
+main-context-menu-image-set-as-background =
+ .label = Ustaw jako tapetę…
+ .accesskey = t
+main-context-menu-image-info =
+ .label = Pokaż informacje o obrazie
+ .accesskey = f
+main-context-menu-image-desc =
+ .label = Pokaż opis
+ .accesskey = s
+main-context-menu-video-save-as =
+ .label = Zapisz wideo jako…
+ .accesskey = s
+main-context-menu-audio-save-as =
+ .label = Zapisz dźwięk jako…
+ .accesskey = s
+main-context-menu-video-image-save-as =
+ .label = Zapisz klatkę jako…
+ .accesskey = k
+main-context-menu-video-email =
+ .label = Wyślij wideo…
+ .accesskey = o
+main-context-menu-audio-email =
+ .label = Wyślij dźwięk…
+ .accesskey = d
+main-context-menu-plugin-play =
+ .label = Aktywuj tę wtyczkę
+ .accesskey = w
+main-context-menu-plugin-hide =
+ .label = Ukryj tę wtyczkę
+ .accesskey = U
+main-context-menu-save-to-pocket =
+ .label = Wyślij stronę do { -pocket-brand-name }
+ .accesskey = W
+main-context-menu-send-to-device =
+ .label = Wyślij stronę do
+ .accesskey = W
+main-context-menu-view-background-image =
+ .label = Pokaż obraz tła
+ .accesskey = t
+main-context-menu-generate-new-password =
+ .label = Użyj wygenerowanego hasła…
+ .accesskey = h
+main-context-menu-keyword =
+ .label = Utwórz słowo kluczowe dla tej wyszukiwarki…
+ .accesskey = U
+main-context-menu-link-send-to-device =
+ .label = Wyślij odnośnik do
+ .accesskey = W
+main-context-menu-frame =
+ .label = Ramka
+ .accesskey = R
+main-context-menu-frame-show-this =
+ .label = Pokaż tylko tę ramkę
+ .accesskey = r
+main-context-menu-frame-open-tab =
+ .label = Otwórz ramkę w nowej karcie
+ .accesskey = j
+main-context-menu-frame-open-window =
+ .label = Otwórz ramkę w nowym oknie
+ .accesskey = n
+main-context-menu-frame-reload =
+ .label = Odśwież ramkę
+ .accesskey = O
+main-context-menu-frame-bookmark =
+ .label = Dodaj zakładkę do ramki
+ .accesskey = D
+main-context-menu-frame-save-as =
+ .label = Zapisz ramkę jako…
+ .accesskey = Z
+main-context-menu-frame-print =
+ .label = Drukuj ramkę…
+ .accesskey = u
+main-context-menu-frame-view-source =
+ .label = Pokaż źródło ramki
+ .accesskey = P
+main-context-menu-frame-view-info =
+ .label = Pokaż informacje o ramce
+ .accesskey = i
+main-context-menu-print-selection =
+ .label = Drukuj tylko zaznaczenie
+ .accesskey = u
+main-context-menu-view-selection-source =
+ .label = Pokaż źródło zaznaczenia
+ .accesskey = d
+main-context-menu-view-page-source =
+ .label = Pokaż źródło strony
+ .accesskey = y
+main-context-menu-view-page-info =
+ .label = Pokaż informacje o stronie
+ .accesskey = I
+main-context-menu-bidi-switch-text =
+ .label = Przełącz kierunek tekstu
+ .accesskey = t
+main-context-menu-bidi-switch-page =
+ .label = Przełącz kierunek strony
+ .accesskey = s
+main-context-menu-inspect-element =
+ .label = Zbadaj element
+ .accesskey = t
+main-context-menu-inspect-a11y-properties =
+ .label = Zbadaj własności dostępności
+main-context-menu-eme-learn-more =
+ .label = Więcej informacji o DRM…
+ .accesskey = D
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/browserSets.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/browserSets.ftl
new file mode 100644
index 0000000000..dcc85a1d74
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/browserSets.ftl
@@ -0,0 +1,180 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+window-minimize-command =
+ .label = Miniaturka
+window-zoom-command =
+ .label = Powiększenie
+window-new-shortcut =
+ .key = N
+window-minimize-shortcut =
+ .key = m
+close-shortcut =
+ .key = W
+tab-new-shortcut =
+ .key = t
+location-open-shortcut =
+ .key = l
+location-open-shortcut-alt =
+ .key = d
+search-focus-shortcut =
+ .key = k
+# This shortcut is used in two contexts:
+# - web search
+# - find in page
+find-shortcut =
+ .key = f
+search-find-again-shortcut =
+ .key = g
+search-find-again-shortcut-alt =
+ .keycode = VK_F3
+search-find-selection-shortcut =
+ .key = e
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+search-focus-shortcut-alt =
+ .key =
+ { PLATFORM() ->
+ [linux] j
+ *[other] e
+ }
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+downloads-shortcut =
+ .key =
+ { PLATFORM() ->
+ [linux] y
+ *[other] j
+ }
+addons-shortcut =
+ .key = A
+file-open-shortcut =
+ .key = o
+save-page-shortcut =
+ .key = s
+page-source-shortcut =
+ .key = u
+# This should match the Option+Command keyboard shortcut letter that Safari
+# and Chrome use for "View Source" on macOS. `page-source-shortcut` above
+# is Firefox's official keyboard shortcut shown in the GUI.
+# Safari variant is an alias provided for the convenience of Safari and Chrome
+# users on macOS. See bug 1398988.
+page-source-shortcut-safari =
+ .key = u
+page-info-shortcut =
+ .key = i
+print-shortcut =
+ .key = p
+mute-toggle-shortcut =
+ .key = M
+nav-back-shortcut-alt =
+ .key = [
+nav-fwd-shortcut-alt =
+ .key = ]
+nav-reload-shortcut =
+ .key = r
+# Shortcut available only on macOS.
+nav-stop-shortcut =
+ .key = .
+history-show-all-shortcut =
+ .key = H
+history-show-all-shortcut-mac =
+ .key = Y
+history-sidebar-shortcut =
+ .key = h
+full-screen-shortcut =
+ .key = f
+reader-mode-toggle-shortcut-windows =
+ .keycode = VK_F9
+reader-mode-toggle-shortcut-other =
+ .key = R
+picture-in-picture-toggle-shortcut-mac =
+ .key = ]
+# Pick the key that is commonly present
+# in your locale keyboards above the
+# `picture-in-picture-toggle-shortcut-mac` key.
+picture-in-picture-toggle-shortcut-mac-alt =
+ .key = { "}" }
+picture-in-picture-toggle-shortcut =
+ .key = ]
+# Pick the key that is commonly present
+# in your locale keyboards above the
+# `picture-in-picture-toggle-shortcut` key.
+picture-in-picture-toggle-shortcut-alt =
+ .key = { "}" }
+bookmark-this-page-shortcut =
+ .key = d
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+bookmark-show-all-shortcut =
+ .key =
+ { PLATFORM() ->
+ [linux] o
+ *[other] b
+ }
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+bookmark-show-library-shortcut =
+ .key = o
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+bookmark-show-sidebar-shortcut =
+ .key = b
+# Verify what shortcut for that operation
+# are recommended by the Human Interface Guidelines
+# of each platform for your locale.
+bookmark-show-toolbar-shortcut =
+ .key = b
+
+## All `-alt*` messages are alternative acceleration keys for zoom.
+## If shift key is needed with your locale popular keyboard for them,
+## you can use these alternative items. Otherwise, their values should be empty.
+
+full-zoom-reduce-shortcut =
+ .key = -
+# If in keyboard layouts popular for your locale you need to use the shift key
+# to access the original shortcuts, the following shortcuts can be used.
+# Otherwise their values should remain empty.
+full-zoom-reduce-shortcut-alt =
+ .key = { "" }
+full-zoom-reduce-shortcut-alt-a =
+ .key = _
+full-zoom-reduce-shortcut-alt-b =
+ .key = { "" }
+full-zoom-enlarge-shortcut =
+ .key = +
+full-zoom-enlarge-shortcut-alt =
+ .key = =
+full-zoom-enlarge-shortcut-alt2 =
+ .key = { "" }
+full-zoom-reset-shortcut =
+ .key = 0
+full-zoom-reset-shortcut-alt =
+ .key = { "" }
+
+##
+
+bidi-switch-direction-shortcut =
+ .key = X
+private-browsing-shortcut =
+ .key = P
+
+## The shortcuts below are for Mac specific
+## global menu.
+
+quit-app-shortcut =
+ .key = Q
+help-shortcut =
+ .key = ?
+preferences-shortcut =
+ .key = ,
+hide-app-shortcut =
+ .key = H
+hide-other-apps-shortcut =
+ .key = H
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/downloads.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/downloads.ftl
new file mode 100644
index 0000000000..4b33b890ad
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/downloads.ftl
@@ -0,0 +1,166 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+## The title and aria-label attributes are used by screen readers to describe
+## the Downloads Panel.
+
+downloads-window =
+ .title = Pobierane pliki
+downloads-panel =
+ .aria-label = Pobierane pliki
+
+##
+
+# The style attribute has the width of the Downloads Panel expressed using
+# a CSS unit. The longest labels that should fit are usually those of
+# in-progress and blocked downloads.
+downloads-panel-list =
+ .style = width: 60ch
+
+downloads-cmd-pause =
+ .label = Wstrzymaj
+ .accesskey = W
+downloads-cmd-resume =
+ .label = Wznów
+ .accesskey = z
+downloads-cmd-cancel =
+ .tooltiptext = Anuluj
+downloads-cmd-cancel-panel =
+ .aria-label = Anuluj
+
+# This message is only displayed on Windows and Linux devices
+downloads-cmd-show-menuitem =
+ .label = Otwórz folder nadrzędny
+ .accesskey = f
+
+# This message is only displayed on macOS devices
+downloads-cmd-show-menuitem-mac =
+ .label = Pokaż w Finderze
+ .accesskey = F
+
+downloads-cmd-use-system-default =
+ .label = Otwórz w przeglądarce systemowej
+ .accesskey = O
+
+downloads-cmd-always-use-system-default =
+ .label = Zawsze otwieraj w przeglądarce systemowej
+ .accesskey = Z
+
+downloads-cmd-show-button =
+ .tooltiptext =
+ { PLATFORM() ->
+ [macos] Pokaż w Finderze
+ *[other] Otwórz folder nadrzędny
+ }
+
+downloads-cmd-show-panel =
+ .aria-label =
+ { PLATFORM() ->
+ [macos] Pokaż w Finderze
+ *[other] Otwórz folder nadrzędny
+ }
+downloads-cmd-show-description =
+ .value =
+ { PLATFORM() ->
+ [macos] Pokaż w Finderze
+ *[other] Otwórz folder nadrzędny
+ }
+
+downloads-cmd-show-downloads =
+ .label = Pokaż folder z pobranymi
+downloads-cmd-retry =
+ .tooltiptext = Spróbuj ponownie
+downloads-cmd-retry-panel =
+ .aria-label = Spróbuj ponownie
+downloads-cmd-go-to-download-page =
+ .label = Przejdź do strony pobierania
+ .accesskey = P
+downloads-cmd-copy-download-link =
+ .label = Kopiuj adres, z którego pobrano plik
+ .accesskey = K
+downloads-cmd-remove-from-history =
+ .label = Usuń z historii
+ .accesskey = U
+downloads-cmd-clear-list =
+ .label = Wyczyść listę
+ .accesskey = c
+downloads-cmd-clear-downloads =
+ .label = Wyczyść listę
+ .accesskey = c
+
+# This command is shown in the context menu when downloads are blocked.
+downloads-cmd-unblock =
+ .label = Pozwól pobrać
+ .accesskey = P
+
+# This is the tooltip of the action button shown when malware is blocked.
+downloads-cmd-remove-file =
+ .tooltiptext = Usuń plik
+
+downloads-cmd-remove-file-panel =
+ .aria-label = Usuń plik
+
+# This is the tooltip of the action button shown when potentially unwanted
+# downloads are blocked. This opens a dialog where the user can choose
+# whether to unblock or remove the download. Removing is the default option.
+downloads-cmd-choose-unblock =
+ .tooltiptext = Usuń plik lub pozwól go pobrać
+
+downloads-cmd-choose-unblock-panel =
+ .aria-label = Usuń plik lub pozwól go pobrać
+
+# This is the tooltip of the action button shown when uncommon downloads are
+# blocked.This opens a dialog where the user can choose whether to open the
+# file or remove the download. Opening is the default option.
+downloads-cmd-choose-open =
+ .tooltiptext = Otwórz lub usuń plik
+
+downloads-cmd-choose-open-panel =
+ .aria-label = Otwórz lub usuń plik
+
+# Displayed when hovering a blocked download, indicates that it's possible to
+# show more information for user to take the next action.
+downloads-show-more-information =
+ .value = Wyświetl więcej informacji
+
+# Displayed when hovering a complete download, indicates that it's possible to
+# open the file using an app available in the system.
+downloads-open-file =
+ .value = Otwórz plik
+
+# Displayed when hovering a download which is able to be retried by users,
+# indicates that it's possible to download this file again.
+downloads-retry-download =
+ .value = Pobierz ponownie
+
+# Displayed when hovering a download which is able to be cancelled by users,
+# indicates that it's possible to cancel and stop the download.
+downloads-cancel-download =
+ .value = Anuluj pobieranie
+
+# This string is shown at the bottom of the Downloads Panel when all the
+# downloads fit in the available space, or when there are no downloads in
+# the panel at all.
+downloads-history =
+ .label = Wyświetl wszystkie
+ .accesskey = W
+
+# This string is shown at the top of the Download Details Panel, to indicate
+# that we are showing the details of a single download.
+downloads-details =
+ .title = Szczegóły pobieranego pliku
+
+downloads-clear-downloads-button =
+ .label = Wyczyść listę
+ .tooltiptext = Ukończone, anulowane i nieudane pobierania zostaną usunięte
+
+# This string is shown when there are no items in the Downloads view, when it
+# is displayed inside a browser tab.
+downloads-list-empty =
+ .value = Brak pobranych plików
+
+# This string is shown when there are no items in the Downloads Panel.
+downloads-panel-empty =
+ .value = Brak pobranych podczas tej sesji.
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/menubar.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/menubar.ftl
new file mode 100644
index 0000000000..b21441f088
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/menubar.ftl
@@ -0,0 +1,274 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+## File Menu
+
+menu-file =
+ .label = Plik
+ .accesskey = P
+menu-file-new-tab =
+ .label = Nowa karta
+ .accesskey = t
+menu-file-new-container-tab =
+ .label = Nowa karta z kontekstem
+ .accesskey = k
+menu-file-new-window =
+ .label = Nowe okno
+ .accesskey = N
+menu-file-new-private-window =
+ .label = Nowe okno prywatne
+ .accesskey = p
+# "Open Location" is only displayed on macOS, and only on windows
+# that aren't main browser windows, or when there are no windows
+# but Firefox is still running.
+menu-file-open-location =
+ .label = Otwórz adres…
+menu-file-open-file =
+ .label = Otwórz plik…
+ .accesskey = o
+menu-file-close =
+ .label = Zamknij
+ .accesskey = Z
+menu-file-close-window =
+ .label = Zamknij okno
+ .accesskey = m
+menu-file-save-page =
+ .label = Zapisz stronę jako…
+ .accesskey = p
+menu-file-email-link =
+ .label = Wyślij odnośnik…
+ .accesskey = n
+menu-file-print-setup =
+ .label = Ustawienia strony…
+ .accesskey = U
+menu-file-print-preview =
+ .label = Podgląd wydruku
+ .accesskey = g
+menu-file-print =
+ .label = Drukuj…
+ .accesskey = D
+menu-file-import-from-another-browser =
+ .label = Importuj z innej przeglądarki…
+ .accesskey = I
+menu-file-go-offline =
+ .label = Pracuj w trybie offline
+ .accesskey = c
+
+## Edit Menu
+
+menu-edit =
+ .label = Edycja
+ .accesskey = E
+menu-edit-find-on =
+ .label = Znajdź na tej stronie…
+ .accesskey = Z
+menu-edit-find-again =
+ .label = Znajdź następne
+ .accesskey = n
+menu-edit-bidi-switch-text-direction =
+ .label = Przełącz kierunek tekstu
+ .accesskey = t
+
+## View Menu
+
+menu-view =
+ .label = Widok
+ .accesskey = W
+menu-view-toolbars-menu =
+ .label = Paski narzędzi
+ .accesskey = P
+menu-view-customize-toolbar =
+ .label = Dostosuj…
+ .accesskey = t
+menu-view-sidebar =
+ .label = Panel boczny
+ .accesskey = b
+menu-view-bookmarks =
+ .label = Zakładki
+menu-view-history-button =
+ .label = Historia
+menu-view-synced-tabs-sidebar =
+ .label = Karty z innych urządzeń
+menu-view-full-zoom =
+ .label = Powiększenie
+ .accesskey = w
+menu-view-full-zoom-enlarge =
+ .label = Powiększ
+ .accesskey = w
+menu-view-full-zoom-reduce =
+ .label = Pomniejsz
+ .accesskey = m
+menu-view-full-zoom-actual-size =
+ .label = Rozmiar oryginalny
+ .accesskey = R
+menu-view-full-zoom-toggle =
+ .label = Powiększaj tylko tekst
+ .accesskey = k
+menu-view-page-style-menu =
+ .label = Styl strony
+ .accesskey = S
+menu-view-page-style-no-style =
+ .label = Ignoruj style
+ .accesskey = n
+menu-view-page-basic-style =
+ .label = Styl podstawowy
+ .accesskey = S
+menu-view-charset =
+ .label = Kodowanie tekstu
+ .accesskey = K
+
+## These should match what Safari and other Apple applications
+## use on macOS.
+
+menu-view-enter-full-screen =
+ .label = Tryb pełnoekranowy
+ .accesskey = T
+menu-view-exit-full-screen =
+ .label = Opuść tryb pełnoekranowy
+ .accesskey = O
+menu-view-full-screen =
+ .label = Tryb pełnoekranowy
+ .accesskey = T
+
+##
+
+menu-view-show-all-tabs =
+ .label = Wyświetl wszystkie karty
+ .accesskey = W
+menu-view-bidi-switch-page-direction =
+ .label = Przełącz kierunek strony
+ .accesskey = s
+
+## History Menu
+
+menu-history =
+ .label = Historia
+ .accesskey = h
+menu-history-show-all-history =
+ .label = Wyświetl całą historię
+menu-history-clear-recent-history =
+ .label = Wyczyść historię przeglądania…
+menu-history-synced-tabs =
+ .label = Karty z innych urządzeń
+menu-history-restore-last-session =
+ .label = Przywróć poprzednią sesję
+menu-history-hidden-tabs =
+ .label = Ukryte karty
+menu-history-undo-menu =
+ .label = Ostatnio zamknięte karty
+menu-history-undo-window-menu =
+ .label = Ostatnio zamknięte okna
+
+## Bookmarks Menu
+
+menu-bookmarks-menu =
+ .label = Zakładki
+ .accesskey = Z
+menu-bookmarks-show-all =
+ .label = Wyświetl wszystkie zakładki
+menu-bookmark-this-page =
+ .label = Dodaj zakładkę
+menu-bookmark-edit =
+ .label = Edytuj zakładkę
+menu-bookmarks-all-tabs =
+ .label = Dodaj zakładki do wszystkich kart…
+menu-bookmarks-toolbar =
+ .label = Pasek zakładek
+menu-bookmarks-other =
+ .label = Pozostałe zakładki
+menu-bookmarks-mobile =
+ .label = Zakładki z telefonu
+
+## Tools Menu
+
+menu-tools =
+ .label = Narzędzia
+ .accesskey = N
+menu-tools-downloads =
+ .label = Pobieranie plików
+ .accesskey = P
+menu-tools-addons =
+ .label = Dodatki
+ .accesskey = D
+menu-tools-fxa-sign-in =
+ .label = Zaloguj się w przeglądarce { -brand-product-name }…
+ .accesskey = Z
+menu-tools-turn-on-sync =
+ .label = Włącz { -sync-brand-short-name(case: "acc", capitalization: "lower") }…
+ .accesskey = W
+menu-tools-sync-now =
+ .label = Synchronizuj teraz
+ .accesskey = S
+menu-tools-fxa-re-auth =
+ .label = Zaloguj się ponownie w przeglądarce { -brand-product-name }…
+ .accesskey = Z
+menu-tools-web-developer =
+ .label = Dla twórców witryn
+ .accesskey = W
+menu-tools-page-source =
+ .label = Źródło strony
+ .accesskey = d
+menu-tools-page-info =
+ .label = Informacje o stronie
+ .accesskey = m
+menu-preferences =
+ .label =
+ { PLATFORM() ->
+ [windows] Opcje
+ *[other] Preferencje
+ }
+ .accesskey =
+ { PLATFORM() ->
+ [windows] O
+ *[other] r
+ }
+menu-tools-layout-debugger =
+ .label = Debuger układu
+ .accesskey = r
+
+## Window Menu
+
+menu-window-menu =
+ .label = Okno
+menu-window-bring-all-to-front =
+ .label = Pokaż wszystko na wierzchu
+
+## Help Menu
+
+menu-help =
+ .label = Pomoc
+ .accesskey = c
+menu-help-product =
+ .label = Pomoc programu { -brand-shorter-name }
+ .accesskey = P
+menu-help-show-tour =
+ .label = Przewodnik po programie { -brand-shorter-name }
+ .accesskey = r
+menu-help-import-from-another-browser =
+ .label = Importuj z innej przeglądarki…
+ .accesskey = I
+menu-help-keyboard-shortcuts =
+ .label = Skróty klawiaturowe
+ .accesskey = S
+menu-help-troubleshooting-info =
+ .label = Informacje dla pomocy technicznej
+ .accesskey = n
+menu-help-feedback-page =
+ .label = Prześlij swoją opinię…
+ .accesskey = e
+menu-help-safe-mode-without-addons =
+ .label = Uruchom ponownie z wyłączonymi dodatkami…
+ .accesskey = U
+menu-help-safe-mode-with-addons =
+ .label = Uruchom ponownie z włączonymi dodatkami
+ .accesskey = U
+# Label of the Help menu item. Either this or
+# menu-help-notdeceptive is shown.
+menu-help-report-deceptive-site =
+ .label = Zgłoś oszustwo internetowe…
+ .accesskey = Z
+menu-help-not-deceptive =
+ .label = To nie jest oszustwo…
+ .accesskey = n
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/places.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/places.ftl
new file mode 100644
index 0000000000..def39e8219
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/places.ftl
@@ -0,0 +1,68 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+places-open =
+ .label = Otwórz
+ .accesskey = O
+places-open-tab =
+ .label = Otwórz w nowej karcie
+ .accesskey = w
+places-open-all-in-tabs =
+ .label = Otwórz wszystkie w kartach
+ .accesskey = O
+places-open-window =
+ .label = Otwórz w nowym oknie
+ .accesskey = n
+places-open-private-window =
+ .label = Otwórz w nowym oknie prywatnym
+ .accesskey = p
+places-new-bookmark =
+ .label = Nowa zakładka…
+ .accesskey = z
+places-new-folder-contextmenu =
+ .label = Nowy folder…
+ .accesskey = f
+places-new-folder =
+ .label = Nowy folder…
+ .accesskey = f
+places-new-separator =
+ .label = Nowy separator
+ .accesskey = S
+places-view =
+ .label = Widok
+ .accesskey = k
+places-by-date =
+ .label = Według daty
+ .accesskey = d
+places-by-site =
+ .label = Według witryny
+ .accesskey = w
+places-by-most-visited =
+ .label = Według liczby wizyt
+ .accesskey = l
+places-by-last-visited =
+ .label = Według ostatniej wizyty
+ .accesskey = o
+places-by-day-and-site =
+ .label = Według daty i witryny
+ .accesskey = t
+places-history-search =
+ .placeholder = Szukaj w historii
+places-bookmarks-search =
+ .placeholder = Szukaj w zakładkach
+places-delete-domain-data =
+ .label = Usuń całą witrynę
+ .accesskey = w
+places-sortby-name =
+ .label = Sortuj wg nazw
+ .accesskey = r
+places-properties =
+ .label = Właściwości
+ .accesskey = i
+# Managed bookmarks are created by an administrator and cannot be changed by the user.
+managed-bookmarks =
+ .label = Zakładki zarządzane przez administratora
+# This label is used when a managed bookmarks folder doesn't have a name.
+managed-bookmarks-subfolder =
+ .label = Podfolder
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/addEngine.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/addEngine.ftl
new file mode 100644
index 0000000000..a5b93881eb
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/addEngine.ftl
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+add-engine-window =
+ .title = Dodawanie wyszukiwarki
+ .style = width: 32em;
+add-engine-button = Dodaj inną wyszukiwarkę
+add-engine-name = Nazwa wyszukiwarki
+add-engine-alias = Alias
+add-engine-url = Adres wyszukiwarki, wyszukiwany tekst zastąp „%s”
+add-engine-cancel =
+ .label = Anuluj
+ .accesskey = A
+add-engine-ok =
+ .label = Dodaj wyszukiwarkę
+ .accesskey = D
+add-engine-dialog =
+ .buttonlabelaccept = Dodaj wyszukiwarkę
+ .buttonaccesskeyaccept = D
+engine-name-exists = Wyszukiwarka o tej nazwie już istnieje
+engine-alias-exists = Wyszukiwarka o tym aliasie już istnieje
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/blocklists.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/blocklists.ftl
new file mode 100644
index 0000000000..a5af3a943d
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/blocklists.ftl
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+blocklist-window =
+ .title = Lista blokowanych elementów
+ .style = width: 57em
+blocklist-description = Wybierz listę używaną przez przeglądarkę { -brand-short-name } do blokowania elementów śledzących użytkownika w Internecie. Listy są dostarczane przez <a data-l10n-name="disconnect-link" title="Disconnect">Disconnect</a>.
+blocklist-close-key =
+ .key = w
+blocklist-treehead-list =
+ .label = Lista
+blocklist-button-cancel =
+ .label = Anuluj
+ .accesskey = A
+blocklist-button-ok =
+ .label = Zachowaj zmiany
+ .accesskey = Z
+blocklist-dialog =
+ .buttonlabelaccept = Zachowaj zmiany
+ .buttonaccesskeyaccept = Z
+# This template constructs the name of the block list in the block lists dialog.
+# It combines the list name and description.
+# e.g. "Standard (Recommended). This list does a pretty good job."
+#
+# Variables:
+# $listName {string, "Standard (Recommended)."} - List name.
+# $description {string, "This list does a pretty good job."} - Description of the list.
+blocklist-item-list-template = { $listName } { $description }
+blocklist-item-moz-std-listName = Lista blokowanych elementów 1. poziomu (zalecana).
+blocklist-item-moz-std-description = Zezwala na niektóre elementy śledzące, więc powoduje mniej problemów na stronach.
+blocklist-item-moz-full-listName = Lista blokowanych elementów 2. poziomu.
+blocklist-item-moz-full-description = Blokuje wszystkie wykryte elementy śledzące. Część stron lub ich treść mogą się niepoprawnie wczytywać.
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/clearSiteData.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/clearSiteData.ftl
new file mode 100644
index 0000000000..14652e025c
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/clearSiteData.ftl
@@ -0,0 +1,53 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+clear-site-data-window =
+ .title = Czyszczenie danych
+ .style = width: 35em
+clear-site-data-description = Wyczyszczenie wszystkich ciasteczek i danych stron przechowywanych przez przeglądarkę { -brand-short-name } może spowodować wylogowanie ze stron i usunąć treści offline. Czyszczenie danych pamięci podręcznej nie wpłynie na zachowane dane logowania.
+clear-site-data-close-key =
+ .key = w
+# The parameters in parentheses in this string describe disk usage
+# in the format ($amount $unit), e.g. "Cookies and Site Data (24 KB)"
+# Variables:
+# $amount (Number) - Amount of site data currently stored on disk
+# $unit (String) - Abbreviation of the unit that $amount is in, e.g. "MB"
+clear-site-data-cookies-with-data =
+ .label = Ciasteczka i dane stron ({ $amount } { $unit })
+ .accesskey = C
+# This string is a placeholder for while the data used to fill
+# clear-site-data-cookies-with-data is loading. This placeholder is usually
+# only shown for a very short time (< 1s), so it should be very similar
+# or the same as clear-site-data-cookies-with-data (except the amount and unit),
+# to avoid flickering.
+clear-site-data-cookies-empty =
+ .label = Ciasteczka i dane stron
+ .accesskey = C
+clear-site-data-cookies-info = Może skutkować wylogowaniem ze stron po wyczyszczeniu
+# The parameters in parentheses in this string describe disk usage
+# in the format ($amount $unit), e.g. "Cached Web Content (24 KB)"
+# Variables:
+# $amount (Number) - Amount of cache currently stored on disk
+# $unit (String) - Abbreviation of the unit that $amount is in, e.g. "MB"
+clear-site-data-cache-with-data =
+ .label = Treści zachowane w pamięci podręcznej ({ $amount } { $unit })
+ .accesskey = T
+# This string is a placeholder for while the data used to fill
+# clear-site-data-cache-with-data is loading. This placeholder is usually
+# only shown for a very short time (< 1s), so it should be very similar
+# or the same as clear-site-data-cache-with-data (except the amount and unit),
+# to avoid flickering.
+clear-site-data-cache-empty =
+ .label = Treści zachowane w pamięci podręcznej
+ .accesskey = T
+clear-site-data-cache-info = Skutkuje koniecznością ponownego pobrania obrazów i innych danych przez strony
+clear-site-data-cancel =
+ .label = Anuluj
+ .accesskey = A
+clear-site-data-clear =
+ .label = Wyczyść
+ .accesskey = W
+clear-site-data-dialog =
+ .buttonlabelaccept = Wyczyść
+ .buttonaccesskeyaccept = W
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/colors.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/colors.ftl
new file mode 100644
index 0000000000..3dd2a6dae7
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/colors.ftl
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+colors-window =
+ .title = Kolory
+ .style =
+ { PLATFORM() ->
+ [macos] width: 41em
+ *[other] width: 38em
+ }
+colors-close-key =
+ .key = w
+colors-page-override = Zastępuj kolory określone w treści wybranymi powyżej:
+ .accesskey = Z
+colors-page-override-option-always =
+ .label = Zawsze
+colors-page-override-option-auto =
+ .label = Tylko w motywach o wysokim kontraście
+colors-page-override-option-never =
+ .label = Nigdy
+colors-text-and-background = Tekst i tło
+colors-text-header = Tekst
+ .accesskey = e
+colors-background = Tło
+ .accesskey = T
+colors-use-system =
+ .label = Kolory systemowe
+ .accesskey = K
+colors-underline-links =
+ .label = Podkreślanie odnośników
+ .accesskey = P
+colors-links-header = Kolory odnośników
+colors-unvisited-links = Nieodwiedzone odnośniki
+ .accesskey = w
+colors-visited-links = Odwiedzone odnośniki
+ .accesskey = O
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/connection.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/connection.ftl
new file mode 100644
index 0000000000..aad261bdd0
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/connection.ftl
@@ -0,0 +1,107 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+connection-window =
+ .title = Ustawienia połączenia
+ .style =
+ { PLATFORM() ->
+ [macos] width: 45em
+ *[other] width: 49em
+ }
+
+connection-close-key =
+ .key = w
+
+connection-disable-extension =
+ .label = Wyłącz rozszerzenie
+
+connection-proxy-configure = Konfiguracja proxy do łączenia z Internetem
+
+connection-proxy-option-no =
+ .label = Bez serwera proxy
+ .accesskey = B
+connection-proxy-option-system =
+ .label = Używaj systemowych ustawień serwerów proxy
+ .accesskey = w
+connection-proxy-option-auto =
+ .label = Automatycznie wykrywaj ustawienia serwerów proxy dla tej sieci
+ .accesskey = A
+connection-proxy-option-manual =
+ .label = Ręczna konfiguracja serwerów proxy:
+ .accesskey = k
+
+connection-proxy-http = Serwer proxy HTTP:
+ .accesskey = H
+connection-proxy-http-port = Port:
+ .accesskey = o
+
+connection-proxy-http-sharing =
+ .label = Użyj tego serwera proxy także dla FTP i HTTPS
+ .accesskey = U
+
+connection-proxy-https = Serwer proxy HTTPS:
+ .accesskey = S
+connection-proxy-ssl-port = Port:
+ .accesskey = r
+
+connection-proxy-ftp = Serwer proxy FTP:
+ .accesskey = F
+connection-proxy-ftp-port = Port:
+ .accesskey = t
+
+connection-proxy-socks = Host SOCKS:
+ .accesskey = C
+connection-proxy-socks-port = Port:
+ .accesskey = P
+
+connection-proxy-socks4 =
+ .label = SOCKS v4
+ .accesskey = 4
+connection-proxy-socks5 =
+ .label = SOCKS v5
+ .accesskey = 5
+connection-proxy-noproxy = Nie używaj proxy dla:
+ .accesskey = N
+
+connection-proxy-noproxy-desc = Przykład: .mozilla.org, .com.pl, 192.168.1.0/24
+
+# Do not translate localhost, 127.0.0.1 and ::1.
+connection-proxy-noproxy-localhost-desc = Połączania z localhost, 127.0.0.1 i ::1 nigdy nie używają serwera proxy.
+
+connection-proxy-autotype =
+ .label = Adres URL automatycznej konfiguracji proxy:
+ .accesskey = e
+
+connection-proxy-reload =
+ .label = Odśwież
+ .accesskey = d
+
+connection-proxy-autologin =
+ .label = Nie pytaj o uwierzytelnianie, jeśli istnieje zachowane hasło
+ .accesskey = j
+ .tooltip = Umożliwia automatyczne uwierzytelnianie na serwerach proxy, jeśli wcześniej zostały zachowane dane logowania. W przypadku nieudanego uwierzytelniania zostanie wyświetlone standardowe pytanie.
+
+connection-proxy-socks-remote-dns =
+ .label = Proxy DNS podczas używania SOCKS v5
+ .accesskey = x
+
+connection-dns-over-https =
+ .label = DNS poprzez HTTPS
+ .accesskey = D
+
+connection-dns-over-https-url-resolver = Dostawca
+ .accesskey = D
+
+# Variables:
+# $name (String) - Display name or URL for the DNS over HTTPS provider
+connection-dns-over-https-url-item-default =
+ .label = { $name } (domyślny)
+ .tooltiptext = Użyj domyślnego adresu serwera DNS udostępnionego poprzez HTTPS
+
+connection-dns-over-https-url-custom =
+ .label = Własny adres
+ .accesskey = W
+ .tooltiptext = Podaj adres wybranego serwera DNS udostępnionego poprzez HTTPS
+
+connection-dns-over-https-custom-label = Własny adres:
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/fonts.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/fonts.ftl
new file mode 100644
index 0000000000..dc072e25dd
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/fonts.ftl
@@ -0,0 +1,157 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+fonts-window =
+ .title = Czcionki
+
+fonts-window-close =
+ .key = w
+
+## Font groups by language
+
+fonts-langgroup-header = Czcionki:
+ .accesskey = C
+
+fonts-langgroup-arabic=
+ .label=arabskie
+fonts-langgroup-armenian=
+ .label=ormiańskie
+fonts-langgroup-bengali=
+ .label=bengalskie
+fonts-langgroup-simpl-chinese=
+ .label=chińskie uproszczone
+fonts-langgroup-trad-chinese-hk=
+ .label=chińskie tradycyjne (Hongkong)
+fonts-langgroup-trad-chinese=
+ .label=chińskie tradycyjne (Tajwan)
+fonts-langgroup-cyrillic=
+ .label=cyryliczne
+fonts-langgroup-devanagari=
+ .label=dewanagari
+fonts-langgroup-ethiopic=
+ .label=etiopskie
+fonts-langgroup-georgian=
+ .label=gruzińskie
+fonts-langgroup-el=
+ .label=greckie
+fonts-langgroup-gujarati=
+ .label=gudżarati
+fonts-langgroup-gurmukhi=
+ .label=gurmukhi
+fonts-langgroup-japanese=
+ .label=japońskie
+fonts-langgroup-hebrew=
+ .label=hebrajskie
+fonts-langgroup-kannada=
+ .label=kannada
+fonts-langgroup-khmer=
+ .label=khmerskie
+fonts-langgroup-korean=
+ .label=koreańskie
+fonts-langgroup-latin=
+ .label=łacińskie
+fonts-langgroup-malayalam=
+ .label=malajalam
+fonts-langgroup-math=
+ .label=matematyczne
+fonts-langgroup-odia=
+ .label=orija
+fonts-langgroup-sinhala=
+ .label=syngaleskie
+fonts-langgroup-tamil=
+ .label=tamilskie
+fonts-langgroup-telugu=
+ .label=telugu
+fonts-langgroup-thai=
+ .label=tajskie
+fonts-langgroup-tibetan=
+ .label=tybetańskie
+fonts-langgroup-canadian=
+ .label=ujednoliconego sylabariusza kanadyjskiego
+fonts-langgroup-other=
+ .label=innych systemów pisma
+
+## Default fonts and their sizes
+
+fonts-proportional-header = Proporcjonalna:
+ .accesskey = a
+
+fonts-default-serif=
+ .label=szeryfowa
+fonts-default-sans-serif=
+ .label=bezszeryfowa
+
+fonts-proportional-size = Rozmiar:
+ .accesskey = R
+
+fonts-serif = Szeryfowa:
+ .accesskey = S
+
+fonts-sans-serif = Bezszeryfowa:
+ .accesskey = B
+
+fonts-monospace = O stałej szerokości:
+ .accesskey = O
+
+fonts-monospace-size = Rozmiar:
+ .accesskey = z
+
+fonts-minsize = Minimalny rozmiar czcionki:
+ .accesskey = M
+
+fonts-minsize-none=
+ .label=brak
+
+fonts-allow-own=
+ .label=Pozwalaj stronom stosować inne czcionki niż ustawione tutaj
+ .accesskey=P
+
+## Text Encodings
+##
+## Translate the encoding names as adjectives for an encoding, not as the name
+## of the language.
+
+fonts-languages-fallback-header = Kodowanie tekstu dla przestarzałych treści
+fonts-languages-fallback-desc = Kodowanie tekstu używane na stronach, które same go nie określają.
+
+fonts-languages-fallback-label=Rezerwowe kodowanie tekstu:
+ .accesskey=R
+
+fonts-languages-fallback-name-auto=
+ .label=domyślne dla obecnej lokalizacji
+fonts-languages-fallback-name-arabic=
+ .label=arabskie
+fonts-languages-fallback-name-baltic=
+ .label=bałtyckie
+fonts-languages-fallback-name-ceiso=
+ .label=środkowoeuropejskie ISO
+fonts-languages-fallback-name-cewindows=
+ .label=środkowoeuropejskie Microsoftu
+fonts-languages-fallback-name-simplified=
+ .label=chińskie uproszczone
+fonts-languages-fallback-name-traditional=
+ .label=chińskie tradycyjne
+fonts-languages-fallback-name-cyrillic=
+ .label=cyryliczne
+fonts-languages-fallback-name-greek=
+ .label=greckie
+fonts-languages-fallback-name-hebrew=
+ .label=hebrajskie
+fonts-languages-fallback-name-japanese=
+ .label=japońskie
+fonts-languages-fallback-name-korean=
+ .label=koreańskie
+fonts-languages-fallback-name-thai=
+ .label=tajskie
+fonts-languages-fallback-name-turkish=
+ .label=tureckie
+fonts-languages-fallback-name-vietnamese=
+ .label=wietnamskie
+fonts-languages-fallback-name-other=
+ .label=inne (łącznie z zachodnioeuropejskimi)
+
+fonts-label-default=
+ .label=domyślna ({ $name })
+fonts-label-default-unnamed=
+ .label=domyślna
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/languages.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/languages.ftl
new file mode 100644
index 0000000000..3baa28807b
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/languages.ftl
@@ -0,0 +1,73 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+webpage-languages-window =
+ .title = Ustawienia językowe stron WWW
+ .style = width: 35em
+
+languages-close-key =
+ .key = w
+
+languages-description = Niektóre strony internetowe dostępne są w wielu wersjach językowych. Ustal listę kolejno preferowanych języków dla tego rodzaju stron.
+
+languages-customize-spoof-english =
+ .label = Żądanie angielskich wersji witryn, aby zwiększyć prywatność
+
+languages-customize-moveup =
+ .label = W górę
+ .accesskey = g
+
+languages-customize-movedown =
+ .label = W dół
+ .accesskey = d
+
+languages-customize-remove =
+ .label = Usuń
+ .accesskey = U
+
+languages-customize-select-language =
+ .placeholder = Wybierz język do dodania…
+
+languages-customize-add =
+ .label = Dodaj
+ .accesskey = o
+
+# The pattern used to generate strings presented to the user in the
+# locale selection list.
+#
+# Example:
+# Icelandic [is]
+# Spanish (Chile) [es-CL]
+#
+# Variables:
+# $locale (String) - A name of the locale (for example: "Icelandic", "Spanish (Chile)")
+# $code (String) - Locale code of the locale (for example: "is", "es-CL")
+languages-code-format =
+ .label = { $locale } ({ $code })
+
+languages-active-code-format =
+ .value = { languages-code-format.label }
+
+browser-languages-window =
+ .title = Ustawienia języka przeglądarki { -brand-short-name }
+ .style = width: 37em
+
+browser-languages-description = { -brand-short-name } będzie domyślnie wyświetlał pierwszy język z listy. W razie potrzeby, następne języki będą używane kolejno.
+
+browser-languages-search = Wyszukaj więcej języków…
+
+browser-languages-searching =
+ .label = Wyszukiwanie języków…
+
+browser-languages-downloading =
+ .label = Pobieranie…
+
+browser-languages-select-language =
+ .label = Wybierz język do dodania…
+ .placeholder = Wybierz język do dodania…
+
+browser-languages-installed-label = Zainstalowane języki
+browser-languages-available-label = Dostępne języki
+
+browser-languages-error = { -brand-short-name } obecnie nie może uaktualnić listy języków. Sprawdź połączenie z Internetem lub spróbuj ponownie.
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/permissions.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/permissions.ftl
new file mode 100644
index 0000000000..d72612c547
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/permissions.ftl
@@ -0,0 +1,157 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+permissions-window =
+ .title = Wyjątki
+ .style = width: 35em
+permissions-close-key =
+ .key = w
+permissions-address = Adres witryny:
+ .accesskey = s
+permissions-block =
+ .label = Blokuj
+ .accesskey = B
+permissions-session =
+ .label = Zezwalaj na czas sesji
+ .accesskey = c
+permissions-allow =
+ .label = Zezwalaj
+ .accesskey = Z
+permissions-site-name =
+ .label = Witryna
+permissions-status =
+ .label = Stan
+permissions-remove =
+ .label = Usuń witrynę
+ .accesskey = U
+permissions-remove-all =
+ .label = Usuń wszystkie witryny
+ .accesskey = w
+permissions-button-cancel =
+ .label = Anuluj
+ .accesskey = A
+permissions-button-ok =
+ .label = Zachowaj
+ .accesskey = h
+permission-dialog =
+ .buttonlabelaccept = Zachowaj
+ .buttonaccesskeyaccept = h
+permissions-autoplay-menu = Domyślne dla wszystkich witryn:
+permissions-searchbox =
+ .placeholder = Szukaj witryn
+permissions-capabilities-autoplay-allow =
+ .label = zezwalanie na dźwięk i wideo
+permissions-capabilities-autoplay-block =
+ .label = blokowanie dźwięku
+permissions-capabilities-autoplay-blockall =
+ .label = blokowanie dźwięku i wideo
+permissions-capabilities-allow =
+ .label = zezwalaj
+permissions-capabilities-block =
+ .label = blokuj
+permissions-capabilities-prompt =
+ .label = zawsze pytaj
+permissions-capabilities-listitem-allow =
+ .value = zezwalaj
+permissions-capabilities-listitem-block =
+ .value = blokuj
+permissions-capabilities-listitem-allow-session =
+ .value = zezwalaj na czas sesji
+
+## Invalid Hostname Dialog
+
+permissions-invalid-uri-title = Wprowadzono nieprawidłową nazwę hosta
+permissions-invalid-uri-label = Podaj prawidłową nazwę hosta
+
+## Exceptions - Tracking Protection
+
+permissions-exceptions-etp-window =
+ .title = Wzmocniona ochrona przed śledzeniem — wyjątki
+ .style = { permissions-window.style }
+permissions-exceptions-etp-desc = Ochrona została wyłączona dla tych witryn.
+
+## Exceptions - Cookies
+
+permissions-exceptions-cookie-window =
+ .title = Ciasteczka i dane stron — wyjątki
+ .style = { permissions-window.style }
+permissions-exceptions-cookie-desc = Określ zasady akceptacji ciasteczek i danych stron. Podaj dokładny adres witryny, której uprawnienia chcesz zmodyfikować, a następnie naciśnij Zezwalaj, Blokuj lub Zezwalaj na czas sesji.
+
+## Exceptions - Pop-ups
+
+permissions-exceptions-popup-window =
+ .title = Wyskakujące okna — uprawnione witryny
+ .style = { permissions-window.style }
+permissions-exceptions-popup-desc = Określ, które witryny mogą otwierać wyskakujące okna. Podaj dokładny adres witryny, której chcesz na to zezwolić, i naciśnij Zezwalaj.
+
+## Exceptions - Saved Logins
+
+permissions-exceptions-saved-logins-window =
+ .title = Zachowywanie danych logowania — wyjątki
+ .style = { permissions-window.style }
+permissions-exceptions-saved-logins-desc = Dane logowania dla następujących witryn nie będą zachowywane.
+
+## Exceptions - Add-ons
+
+permissions-exceptions-addons-window =
+ .title = Instalacja dodatków — uprawnione witryny
+ .style = { permissions-window.style }
+permissions-exceptions-addons-desc = Określ, które witryny mogą instalować dodatki. Podaj dokładny adres witryny, której chcesz na to zezwolić, i naciśnij Zezwalaj.
+
+## Site Permissions - Autoplay
+
+permissions-site-autoplay-window =
+ .title = Automatyczne odtwarzanie — uprawnione witryny
+ .style = { permissions-window.style }
+permissions-site-autoplay-desc = Zarządzaj witrynami, które nie korzystają z domyślnych ustawień automatycznego odtwarzania.
+
+## Site Permissions - Notifications
+
+permissions-site-notification-window =
+ .title = Wyświetlanie powiadomień — uprawnione witryny
+ .style = { permissions-window.style }
+permissions-site-notification-desc = Następujące strony prosiły o możliwość wyświetlania powiadomień. Określ, które witryny mogą je wyświetlać. Można także zablokować nowe prośby.
+permissions-site-notification-disable-label =
+ .label = Blokowanie nowych próśb o możliwość wyświetlania powiadomień
+permissions-site-notification-disable-desc = Uniemożliwi to witrynom spoza listy powyżej proszenie o możliwość wysyłania powiadomień. Zablokowanie powiadomień może spowodować, że niektóre funkcje witryny nie będą działać.
+
+## Site Permissions - Location
+
+permissions-site-location-window =
+ .title = Informowanie o położeniu — uprawnione witryny
+ .style = { permissions-window.style }
+permissions-site-location-desc = Następujące strony prosiły o możliwość uzyskiwania informacji o położeniu. Określ, które witryny mogą je uzyskiwać. Można także zablokować nowe prośby.
+permissions-site-location-disable-label =
+ .label = Blokowanie nowych próśb o możliwość uzyskiwania informacji o położeniu
+permissions-site-location-disable-desc = Uniemożliwi to witrynom spoza listy powyżej proszenie o możliwość uzyskiwania informacji o położeniu. Zablokowanie uzyskiwania informacji o położeniu może spowodować, że niektóre funkcje witryny nie będą działać.
+
+## Site Permissions - Virtual Reality
+
+permissions-site-xr-window =
+ .title = Rzeczywistość wirtualna — uprawnione witryny
+ .style = { permissions-window.style }
+permissions-site-xr-desc = Następujące strony prosiły o dostęp do urządzeń rzeczywistości wirtualnej. Określ, które witryny mogą uzyskiwać do nich dostęp. Można także zablokować nowe prośby.
+permissions-site-xr-disable-label =
+ .label = Blokowanie nowych próśb o dostęp do urządzeń rzeczywistości wirtualnej
+permissions-site-xr-disable-desc = Uniemożliwi to witrynom spoza listy powyżej proszenie o dostęp do urządzeń rzeczywistości wirtualnej. Zablokowanie dostępu do urządzeń rzeczywistości wirtualnej może spowodować, że niektóre funkcje witryny nie będą działać.
+
+## Site Permissions - Camera
+
+permissions-site-camera-window =
+ .title = Udostępnianie kamery — uprawnione witryny
+ .style = { permissions-window.style }
+permissions-site-camera-desc = Następujące strony prosiły o dostęp do kamery. Określ, które witryny mogą uzyskiwać do niej dostęp. Można także zablokować nowe prośby.
+permissions-site-camera-disable-label =
+ .label = Blokowanie nowych próśb o dostęp do kamery
+permissions-site-camera-disable-desc = Uniemożliwi to witrynom spoza listy powyżej proszenie o dostęp do kamery. Zablokowanie dostępu do kamery może spowodować, że niektóre funkcje witryny nie będą działać.
+
+## Site Permissions - Microphone
+
+permissions-site-microphone-window =
+ .title = Udostępnianie mikrofonu — uprawnione witryny
+ .style = { permissions-window.style }
+permissions-site-microphone-desc = Następujące strony prosiły o dostęp do mikrofonu. Określ, które witryny mogą uzyskiwać do niego dostęp. Można także zablokować nowe prośby.
+permissions-site-microphone-disable-label =
+ .label = Blokowanie nowych próśb o dostęp do mikrofonu
+permissions-site-microphone-disable-desc = Uniemożliwi to witrynom spoza listy powyżej proszenie o dostęp do mikrofonu. Zablokowanie dostępu do mikrofonu może spowodować, że niektóre funkcje witryny nie będą działać.
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/preferences.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/preferences.ftl
new file mode 100644
index 0000000000..65ccf77017
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/preferences.ftl
@@ -0,0 +1,1166 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+do-not-track-description = Informowanie witryn o preferencjach względem śledzenia (wysyłanie nagłówka „Do Not Track”):
+do-not-track-learn-more = Więcej informacji
+do-not-track-option-default-content-blocking-known =
+ .label = gdy { -brand-short-name } blokuje znane elementy śledzące
+do-not-track-option-always =
+ .label = zawsze
+pref-page-title =
+ { PLATFORM() ->
+ [windows] Opcje
+ *[other] Preferencje
+ }
+# This is used to determine the width of the search field in about:preferences,
+# in order to make the entire placeholder string visible
+#
+# Please keep the placeholder string short to avoid truncation.
+#
+# Notice: The value of the `.style` attribute is a CSS string, and the `width`
+# is the name of the CSS property. It is intended only to adjust the element's width.
+# Do not translate.
+search-input-box =
+ .style = width: 15.4em
+ .placeholder =
+ { PLATFORM() ->
+ [windows] Szukaj w opcjach
+ *[other] Szukaj w preferencjach
+ }
+managed-notice = Przeglądarka jest zarządzana przez administratora
+category-list =
+ .aria-label = Kategorie
+pane-general-title = Ogólne
+category-general =
+ .tooltiptext = Ogólne ustawienia
+pane-home-title = Uruchamianie
+category-home =
+ .tooltiptext = Ustawienia powiązane z uruchamianiem programu
+pane-search-title = Wyszukiwanie
+category-search =
+ .tooltiptext = Ustawienia dotyczące wyszukiwania
+pane-privacy-title = Prywatność i bezpieczeństwo
+category-privacy =
+ .tooltiptext = Ustawienia dotyczące prywatności i bezpieczeństwa
+pane-sync-title2 = { -sync-brand-short-name }
+category-sync2 =
+ .tooltiptext = Ustawienia dotyczące synchronizacji
+pane-experimental-title = Eksperymenty
+category-experimental =
+ .tooltiptext = Eksperymenty przeglądarki { -brand-short-name }
+pane-experimental-subtitle = Zachowaj ostrożność
+pane-experimental-search-results-header = Eksperymenty przeglądarki { -brand-short-name }: zachowaj ostrożność
+pane-experimental-description = Modyfikacja zaawansowanych preferencji może wpłynąć na wydajność lub bezpieczeństwo przeglądarki { -brand-short-name }.
+pane-experimental-reset =
+ .label = Przywróć domyślne
+ .accesskey = P
+help-button-label = Wsparcie programu { -brand-short-name }
+addons-button-label = Rozszerzenia i motywy
+focus-search =
+ .key = f
+close-button =
+ .aria-label = Zamknij
+
+## Browser Restart Dialog
+
+feature-enable-requires-restart = Konieczne jest ponowne uruchomienie przeglądarki { -brand-short-name }, aby włączyć tę funkcję.
+feature-disable-requires-restart = Konieczne jest ponowne uruchomienie przeglądarki { -brand-short-name }, aby wyłączyć tę funkcję.
+should-restart-title = Ponowne uruchomienie przeglądarki { -brand-short-name }
+should-restart-ok = Uruchom przeglądarkę { -brand-short-name } ponownie
+cancel-no-restart-button = Anuluj
+restart-later = Później
+
+## Extension Control Notifications
+##
+## These strings are used to inform the user
+## about changes made by extensions to browser settings.
+##
+## <img data-l10n-name="icon"/> is going to be replaced by the extension icon.
+##
+## Variables:
+## $name (String): name of the extension
+
+# This string is shown to notify the user that their home page
+# is being controlled by an extension.
+extension-controlled-homepage-override = Rozszerzenie „{ $name }” <img data-l10n-name="icon"/> kontroluje stronę startową.
+# This string is shown to notify the user that their new tab page
+# is being controlled by an extension.
+extension-controlled-new-tab-url = Rozszerzenie „{ $name }” <img data-l10n-name="icon"/> kontroluje stronę nowej karty.
+# This string is shown to notify the user that the password manager setting
+# is being controlled by an extension
+extension-controlled-password-saving = Rozszerzenie „{ $name }” <img data-l10n-name="icon"/> kontroluje to ustawienie.
+# This string is shown to notify the user that their notifications permission
+# is being controlled by an extension.
+extension-controlled-web-notifications = Rozszerzenie „{ $name }” <img data-l10n-name="icon"/> kontroluje to ustawienie.
+# This string is shown to notify the user that the default search engine
+# is being controlled by an extension.
+extension-controlled-default-search = Rozszerzenie „{ $name }” <img data-l10n-name="icon"/> zmieniło domyślną wyszukiwarkę.
+# This string is shown to notify the user that Container Tabs
+# are being enabled by an extension.
+extension-controlled-privacy-containers = Rozszerzenie „{ $name }” <img data-l10n-name="icon"/> wymaga włączonych „Kart z kontekstem”.
+# This string is shown to notify the user that their content blocking "All Detected Trackers"
+# preferences are being controlled by an extension.
+extension-controlled-websites-content-blocking-all-trackers = Rozszerzenie „{ $name }” <img data-l10n-name="icon"/> kontroluje to ustawienie.
+# This string is shown to notify the user that their proxy configuration preferences
+# are being controlled by an extension.
+extension-controlled-proxy-config = Rozszerzenie „{ $name }” <img data-l10n-name="icon"/> kontroluje, jak { -brand-short-name } łączy się z Internetem.
+# This string is shown after the user disables an extension to notify the user
+# how to enable an extension that they disabled.
+#
+# <img data-l10n-name="addons-icon"/> will be replaced with Add-ons icon
+# <img data-l10n-name="menu-icon"/> will be replaced with Menu icon
+extension-controlled-enable = Aby włączyć rozszerzenie, przejdź do sekcji dodatki <img data-l10n-name="addons-icon"/> w menu <img data-l10n-name="menu-icon"/>.
+
+## Preferences UI Search Results
+
+search-results-header = Wyniki wyszukiwania
+# `<span data-l10n-name="query"></span>` will be replaced by the search term.
+search-results-empty-message =
+ { PLATFORM() ->
+ [windows] Niestety! W opcjach niczego nie odnaleziono dla wyszukiwania „<span data-l10n-name="query"></span>”.
+ *[other] Niestety! W preferencjach niczego nie odnaleziono dla wyszukiwania „<span data-l10n-name="query"></span>”.
+ }
+search-results-help-link = Potrzebujesz pomocy? Odwiedź <a data-l10n-name="url">pomoc przeglądarki { -brand-short-name }</a>.
+
+## General Section
+
+startup-header = Uruchamianie
+# { -brand-short-name } will be 'Firefox Developer Edition',
+# since this setting is only exposed in Firefox Developer Edition
+separate-profile-mode =
+ .label = Jednoczesne działanie programu { -brand-short-name } oraz programu Firefox
+use-firefox-sync = Podczas jednoczesnego działania wykorzystywane są oddzielne profile. Można wykorzystać { -sync-brand-short-name(case: "acc", capitalization: "lower") }, aby dzielić dane między nimi.
+get-started-not-logged-in = Zaloguj się do { -sync-brand-short-name(case: "gen", capitalization: "lower") }…
+get-started-configured = Otwórz preferencje { -sync-brand-short-name(case: "gen", capitalization: "lower") }
+always-check-default =
+ .label = Sprawdzanie, czy { -brand-short-name } jest domyślną przeglądarką
+ .accesskey = e
+is-default = { -brand-short-name } jest obecnie domyślną przeglądarką
+is-not-default = { -brand-short-name } nie jest obecnie domyślną przeglądarką
+set-as-my-default-browser =
+ .label = Ustaw jako domyślną…
+ .accesskey = U
+startup-restore-previous-session =
+ .label = Przywracanie poprzedniej sesji
+ .accesskey = P
+startup-restore-warn-on-quit =
+ .label = Ostrzeganie przy zamykaniu przeglądarki
+disable-extension =
+ .label = Wyłącz rozszerzenie
+tabs-group-header = Karty
+ctrl-tab-recently-used-order =
+ .label = Przełączanie kart za pomocą Ctrl+Tab w kolejności ostatnich wyświetleń
+ .accesskey = T
+open-new-link-as-tabs =
+ .label = Otwieranie odnośników w kartach zamiast w nowych oknach
+ .accesskey = O
+warn-on-close-multiple-tabs =
+ .label = Ostrzeganie przed zamknięciem wielu kart
+ .accesskey = a
+warn-on-open-many-tabs =
+ .label = Ostrzeganie o otwarciu zbyt wielu kart mogących spowolnić przeglądarkę { -brand-short-name }
+ .accesskey = m
+switch-links-to-new-tabs =
+ .label = Przechodzenie do nowych kart otwieranych poprzez odnośniki
+ .accesskey = c
+show-tabs-in-taskbar =
+ .label = Podgląd kart na pasku zadań Windows
+ .accesskey = W
+browser-containers-enabled =
+ .label = Karty z kontekstem.
+ .accesskey = K
+browser-containers-learn-more = Więcej informacji
+browser-containers-settings =
+ .label = Dostosuj…
+ .accesskey = D
+containers-disable-alert-title = Czy zamknąć wszystkie karty z kontekstem?
+containers-disable-alert-desc =
+ { $tabCount ->
+ [one] Jeśli wyłączysz funkcję kart z kontekstem, jedna taka karta zostanie zamknięta. Czy na pewno wyłączyć karty z kontekstem?
+ [few] Jeśli wyłączysz funkcję kart z kontekstem, { $tabCount } takie karty zostaną zamknięte. Czy na pewno wyłączyć karty z kontekstem?
+ *[many] Jeśli wyłączysz funkcję kart z kontekstem, { $tabCount } takich kart zostanie zamkniętych. Czy na pewno wyłączyć karty z kontekstem?
+ }
+containers-disable-alert-ok-button =
+ { $tabCount ->
+ [one] Zamknij kartę z kontekstem
+ [few] Zamknij { $tabCount } karty z kontekstem
+ *[many] Zamknij { $tabCount } kart z kontekstem
+ }
+containers-disable-alert-cancel-button = Anuluj
+containers-remove-alert-title = Usuwanie kontekstu
+# Variables:
+# $count (Number) - Number of tabs that will be closed.
+containers-remove-alert-msg =
+ { $count ->
+ [one] Jeśli usuniesz teraz ten kontekst, { $count } karta zostanie zamknięta. Czy na pewno usunąć ten kontekst?
+ [few] Jeśli usuniesz teraz ten kontekst, { $count } karty zostaną zamknięte. Czy na pewno usunąć ten kontekst?
+ *[many] Jeśli usuniesz teraz ten kontekst, { $count } kart zostanie zamkniętych. Czy na pewno usunąć ten kontekst?
+ }
+containers-remove-ok-button = Usuń
+containers-remove-cancel-button = Nie usuwaj
+
+## General Section - Language & Appearance
+
+language-and-appearance-header = Język i wygląd
+fonts-and-colors-header = Czcionki i kolory
+default-font = Domyślna czcionka:
+ .accesskey = D
+default-font-size = Rozmiar:
+ .accesskey = R
+advanced-fonts =
+ .label = Zaawansowane…
+ .accesskey = s
+colors-settings =
+ .label = Kolory…
+ .accesskey = K
+# Zoom is a noun, and the message is used as header for a group of options
+preferences-zoom-header = Powiększenie
+preferences-default-zoom = Domyślne powiększenie:
+ .accesskey = D
+preferences-default-zoom-value =
+ .label = { $percentage }%
+preferences-zoom-text-only =
+ .label = Powiększaj tylko tekst
+ .accesskey = P
+language-header = Język
+choose-language-description = Wybierz preferowany język, w jakim mają być wyświetlane strony
+choose-button =
+ .label = Wybierz…
+ .accesskey = e
+choose-browser-language-description = Wybierz język używany do wyświetlania interfejsu użytkownika przeglądarki { -brand-short-name } (menu, komunikaty, powiadomienia itp.).
+manage-browser-languages-button =
+ .label = Wybierz alternatywne…
+ .accesskey = W
+confirm-browser-language-change-description = Uruchom przeglądarkę { -brand-short-name } ponownie, aby zastosować zmiany.
+confirm-browser-language-change-button = Uruchom ponownie
+translate-web-pages =
+ .label = Tłumaczenie stron WWW
+ .accesskey = T
+# The <img> element is replaced by the logo of the provider
+# used to provide machine translations for web pages.
+translate-attribution = Tłumaczeń dostarcza <img data-l10n-name="logo"/>
+translate-exceptions =
+ .label = Wyjątki…
+ .accesskey = i
+# Variables:
+# $localeName (string) - Localized name of the locale to be used.
+use-system-locale =
+ .label = Używaj ustawień systemu operacyjnego dla języka „{ $localeName }” do formatowania dat, czasu, liczb i miar
+check-user-spelling =
+ .label = Sprawdzanie pisowni podczas wprowadzania tekstu
+ .accesskey = S
+
+## General Section - Files and Applications
+
+files-and-applications-title = Pliki i aplikacje
+download-header = Pobieranie
+download-save-to =
+ .label = Pobierane pliki zapisuj do:
+ .accesskey = o
+download-choose-folder =
+ .label =
+ { PLATFORM() ->
+ [macos] Wybierz…
+ *[other] Przeglądaj…
+ }
+ .accesskey =
+ { PLATFORM() ->
+ [macos] y
+ *[other] g
+ }
+download-always-ask-where =
+ .label = Pytaj, gdzie zapisać każdy plik
+ .accesskey = t
+applications-header = Aplikacje
+applications-description = Wybierz, jak { -brand-short-name } będzie obsługiwać pobierane z sieci pliki i aplikacje używane podczas przeglądania.
+applications-filter =
+ .placeholder = Typ pliku lub nazwa aplikacji
+applications-type-column =
+ .label = Typ zawartości
+ .accesskey = T
+applications-action-column =
+ .label = Czynność
+ .accesskey = C
+# Variables:
+# $extension (String) - file extension (e.g .TXT)
+applications-file-ending = plik { $extension }
+applications-action-save =
+ .label = Zapisz plik
+# Variables:
+# $app-name (String) - Name of an application (e.g Adobe Acrobat)
+applications-use-app =
+ .label = Użyj aplikacji { $app-name }
+# Variables:
+# $app-name (String) - Name of an application (e.g Adobe Acrobat)
+applications-use-app-default =
+ .label = Użyj aplikacji { $app-name } (domyślnej)
+applications-use-os-default =
+ .label =
+ { PLATFORM() ->
+ [macos] Użyj domyślnej aplikacji systemu macOS
+ [windows] Użyj domyślnej aplikacji systemu Windows
+ *[other] Użyj domyślnej aplikacji systemu
+ }
+applications-use-other =
+ .label = Użyj innej aplikacji…
+applications-select-helper = Wybierz aplikację pomocniczą
+applications-manage-app =
+ .label = Szczegóły aplikacji…
+applications-always-ask =
+ .label = Zawsze pytaj
+applications-type-pdf = Dokument PDF
+# Variables:
+# $type (String) - the MIME type (e.g application/binary)
+applications-type-pdf-with-type = { applications-type-pdf } ({ $type })
+# Variables:
+# $type-description (String) - Description of the type (e.g "Portable Document Format")
+# $type (String) - the MIME type (e.g application/binary)
+applications-type-description-with-type = { $type-description } ({ $type })
+# Variables:
+# $extension (String) - file extension (e.g .TXT)
+# $type (String) - the MIME type (e.g application/binary)
+applications-file-ending-with-type = { applications-file-ending } ({ $type })
+# Variables:
+# $plugin-name (String) - Name of a plugin (e.g Adobe Flash)
+applications-use-plugin-in =
+ .label = Wtyczka { $plugin-name } (w programie { -brand-short-name })
+applications-open-inapp =
+ .label = Otwórz w programie { -brand-short-name }
+
+## The strings in this group are used to populate
+## selected label element based on the string from
+## the selected menu item.
+
+applications-use-plugin-in-label =
+ .value = { applications-use-plugin-in.label }
+applications-action-save-label =
+ .value = { applications-action-save.label }
+applications-use-app-label =
+ .value = { applications-use-app.label }
+applications-open-inapp-label =
+ .value = { applications-open-inapp.label }
+applications-always-ask-label =
+ .value = { applications-always-ask.label }
+applications-use-app-default-label =
+ .value = { applications-use-app-default.label }
+applications-use-other-label =
+ .value = { applications-use-other.label }
+applications-use-os-default-label =
+ .value = { applications-use-os-default.label }
+
+##
+
+drm-content-header = Treści DRM (Digital Rights Management)
+play-drm-content =
+ .label = Odtwarzanie treści chronionych przez DRM.
+ .accesskey = O
+play-drm-content-learn-more = Więcej informacji
+update-application-title = Aktualizacje przeglądarki { -brand-short-name }
+update-application-description = Utrzymuj przeglądarkę { -brand-short-name } aktualną dla najlepszej wydajności, stabilności i bezpieczeństwa.
+update-application-version = Wersja: { $version }. <a data-l10n-name="learn-more">Informacje o wydaniu</a>.
+update-history =
+ .label = Wyświetl historię aktualizacji…
+ .accesskey = W
+update-application-allow-description = Zezwalaj przeglądarce { -brand-short-name } na:
+update-application-auto =
+ .label = automatyczne instalowanie aktualizacji (zalecane)
+ .accesskey = a
+update-application-check-choose =
+ .label = sprawdzanie dostępności aktualizacji i pytania o ich instalację
+ .accesskey = s
+update-application-manual =
+ .label = niesprawdzanie dostępności aktualizacji (niezalecane)
+ .accesskey = n
+update-application-warning-cross-user-setting = To ustawienie będzie obowiązywać dla wszystkich kont systemu Windows i profilów programu { -brand-short-name } używających tej instalacji.
+update-application-use-service =
+ .label = Używaj usługi instalowania aktualizacji działającej w tle
+ .accesskey = U
+update-setting-write-failure-title = Błąd podczas zachowywania preferencji aktualizacji
+# Variables:
+# $path (String) - Path to the configuration file
+# The newlines between the main text and the line containing the path is
+# intentional so the path is easier to identify.
+update-setting-write-failure-message =
+ W przeglądarce { -brand-short-name } wystąpił błąd i nie zachowano tej zmiany. Zauważ, że ustawienie tej preferencji aktualizacji wymaga uprawnienia do zapisu do poniższego pliku. Ty lub administrator komputera może móc rozwiązać błąd przez udzielenie grupie „Użytkownicy” pełnej kontroli nad tym plikiem.
+
+ Nie można zapisać do pliku: { $path }
+update-in-progress-title = Trwa aktualizacja
+update-in-progress-message = Czy { -brand-short-name } ma kontynuować tę aktualizację?
+update-in-progress-ok-button = &Odrzuć
+# Continue is the cancel button so pressing escape or using a platform standard
+# method of closing the UI will not discard the update.
+update-in-progress-cancel-button = &Kontynuuj
+
+## General Section - Performance
+
+performance-title = Wydajność
+performance-use-recommended-settings-checkbox =
+ .label = Zalecane ustawienia wydajności.
+ .accesskey = w
+performance-use-recommended-settings-desc = Ustawienia te są specjalnie dostosowane do specyfikacji tego komputera i systemu operacyjnego.
+performance-settings-learn-more = Więcej informacji
+performance-allow-hw-accel =
+ .label = Korzystaj ze sprzętowego przyspieszania, jeśli jest dostępne
+ .accesskey = s
+performance-limit-content-process-option = Limit liczby procesów treści:
+ .accesskey = o
+performance-limit-content-process-enabled-desc = Więcej procesów treści może poprawić wydajność przy wielu otwartych kartach, ale zwiększy też zapotrzebowanie na pamięć.
+performance-limit-content-process-blocked-desc = Zmiana liczby procesów jest możliwa, jeśli { -brand-short-name } został uruchomiony z obsługą wielu procesów. <a data-l10n-name="learn-more">Jak sprawdzić, czy obsługa wielu procesów jest włączona</a>?
+# Variables:
+# $num - default value of the `dom.ipc.processCount` pref.
+performance-default-content-process-count =
+ .label = { $num } (domyślnie)
+
+## General Section - Browsing
+
+browsing-title = Przeglądanie
+browsing-use-autoscroll =
+ .label = Używaj automatycznego przewijania
+ .accesskey = y
+browsing-use-smooth-scrolling =
+ .label = Używaj płynnego przewijania
+ .accesskey = n
+browsing-use-onscreen-keyboard =
+ .label = Wyświetlaj klawiaturę ekranową, gdy zachodzi taka potrzeba
+ .accesskey = e
+browsing-use-cursor-navigation =
+ .label = Zawsze używaj klawiszy kursora do nawigacji po stronach
+ .accesskey = g
+browsing-search-on-start-typing =
+ .label = Rozpoczynaj wyszukiwanie podczas wpisywania tekstu
+ .accesskey = R
+browsing-picture-in-picture-toggle-enabled =
+ .label = Wyświetlaj przycisk trybu „Obraz w obrazie”.
+ .accesskey = O
+browsing-picture-in-picture-learn-more = Więcej informacji
+browsing-media-control =
+ .label = Sterowanie multimediami za pomocą klawiatury, zestawu słuchawkowego lub interfejsu wirtualnego.
+ .accesskey = m
+browsing-media-control-learn-more = Więcej informacji
+browsing-cfr-recommendations =
+ .label = Polecaj rozszerzenia podczas przeglądania.
+ .accesskey = P
+browsing-cfr-features =
+ .label = Polecaj funkcje podczas przeglądania.
+ .accesskey = u
+browsing-cfr-recommendations-learn-more = Więcej informacji
+
+## General Section - Proxy
+
+network-settings-title = Sieć
+network-proxy-connection-description = Konfiguruj, jak { -brand-short-name } ma się łączyć z Internetem.
+network-proxy-connection-learn-more = Więcej informacji
+network-proxy-connection-settings =
+ .label = Ustawienia…
+ .accesskey = U
+
+## Home Section
+
+home-new-windows-tabs-header = Nowe okna i karty
+home-new-windows-tabs-description2 = Wybierz, co ma być wyświetlane przy otwieraniu strony startowej, nowych okien i kart.
+
+## Home Section - Home Page Customization
+
+home-homepage-mode-label = Strona startowa i nowe okna:
+home-newtabs-mode-label = Nowa karta:
+home-restore-defaults =
+ .label = Przywróć domyślne
+ .accesskey = P
+# "Firefox" should be treated as a brand and kept in English,
+# while "Home" and "(Default)" can be localized.
+home-mode-choice-default =
+ .label = strona startowa Firefoksa (domyślnie)
+home-mode-choice-custom =
+ .label = inne strony
+home-mode-choice-blank =
+ .label = pusta strona
+home-homepage-custom-url =
+ .placeholder = Adres URL
+# This string has a special case for '1' and [other] (default). If necessary for
+# your language, you can add {$tabCount} to your translations and use the
+# standard CLDR forms, or only use the form for [other] if both strings should
+# be identical.
+use-current-pages =
+ .label =
+ { $tabCount ->
+ [1] Użyj bieżącej strony
+ *[other] Użyj bieżących stron
+ }
+ .accesskey = b
+choose-bookmark =
+ .label = Użyj zakładki…
+ .accesskey = z
+
+## Home Section - Firefox Home Content Customization
+
+home-prefs-content-header = Strona startowa Firefoksa
+home-prefs-content-description = Wybierz, co wyświetlać na stronie startowej Firefoksa.
+home-prefs-search-header =
+ .label = Pasek wyszukiwania
+home-prefs-topsites-header =
+ .label = Popularne
+home-prefs-topsites-description = Najczęściej odwiedzane strony.
+home-prefs-topsites-by-option-sponsored =
+ .label = Sponsorowane popularne witryny
+
+## Variables:
+## $provider (String): Name of the corresponding content provider, e.g "Pocket".
+
+home-prefs-recommended-by-header =
+ .label = Polecane przez { $provider }
+home-prefs-recommended-by-description-update = Wyjątkowe rzeczy z całego Internetu, wybrane przez { $provider }
+
+##
+
+home-prefs-recommended-by-learn-more = Jak to działa?
+home-prefs-recommended-by-option-sponsored-stories =
+ .label = Sponsorowane artykuły
+home-prefs-highlights-header =
+ .label = Wyróżnione
+home-prefs-recent-activity-header =
+ .label = Ostatnia aktywność
+home-prefs-highlights-description = Wybierane z zachowanych i odwiedzonych stron.
+home-prefs-highlights-option-visited-pages =
+ .label = Historia
+home-prefs-highlights-options-bookmarks =
+ .label = Zakładki
+home-prefs-highlights-option-most-recent-download =
+ .label = Ostatnio pobrane pliki
+home-prefs-highlights-option-saved-to-pocket =
+ .label = Zachowane w { -pocket-brand-name }
+# For the "Snippets" feature traditionally on about:home.
+# Alternative translation options: "Small Note" or something that
+# expresses the idea of "a small message, shortened from something else,
+# and non-essential but also not entirely trivial and useless.
+home-prefs-snippets-header =
+ .label = Od Mozilli
+home-prefs-snippets-description = Informacje od organizacji { -vendor-short-name } i przeglądarki { -brand-product-name }.
+home-prefs-sections-rows-option =
+ .label =
+ { $num ->
+ [one] { $num } wiersz
+ [few] { $num } wiersze
+ *[many] { $num } wierszy
+ }
+
+## Search Section
+
+search-bar-header = Pasek wyszukiwania
+search-bar-hidden =
+ .label = Pasek adresu z funkcjami wyszukiwania i nawigacji
+search-bar-shown =
+ .label = Osobny pasek wyszukiwania
+search-engine-default-header = Domyślna wyszukiwarka
+search-engine-default-desc-2 = To domyślna wyszukiwarka paska adresu i paska wyszukiwania. Można ją zmienić w każdej chwili.
+search-engine-default-private-desc-2 = Wybierz inną domyślną wyszukiwarkę tylko w oknach prywatnych.
+search-separate-default-engine =
+ .label = Używaj tej wyszukiwarki w oknach prywatnych
+ .accesskey = U
+search-suggestions-header = Podpowiedzi wyszukiwania
+search-suggestions-desc = Wybierz, jak wyświetlać podpowiedzi od wyszukiwarek.
+search-suggestions-option =
+ .label = Podpowiedzi wyszukiwania
+ .accesskey = P
+search-show-suggestions-url-bar-option =
+ .label = Podpowiedzi wyszukiwania w wynikach paska adresu
+ .accesskey = e
+# This string describes what the user will observe when the system
+# prioritizes search suggestions over browsing history in the results
+# that extend down from the address bar. In the original English string,
+# "ahead" refers to location (appearing most proximate to), not time
+# (appearing before).
+search-show-suggestions-above-history-option =
+ .label = Podpowiedzi wyszukiwania nad historią przeglądania w wynikach paska adresu
+search-show-suggestions-private-windows =
+ .label = Podpowiedzi wyszukiwania w prywatnych oknach
+suggestions-addressbar-settings-generic = Zmień preferencje innych podpowiedzi w pasku adresu
+search-suggestions-cant-show = Podpowiedzi wyszukiwania nie będą wyświetlane w wynikach paska adresu, ponieważ wyłączono zachowywanie historii przeglądania programu { -brand-short-name }.
+search-one-click-header = Dodatkowe wyszukiwarki
+search-one-click-header2 = Skróty wyszukiwania
+search-one-click-desc = Wybierz dodatkowe wyszukiwarki wyświetlane na dole wyników wyszukiwania w pasku adresu i pasku wyszukiwania.
+search-choose-engine-column =
+ .label = Nazwa
+search-choose-keyword-column =
+ .label = Słowo kluczowe
+search-restore-default =
+ .label = Przywróć domyślne
+ .accesskey = d
+search-remove-engine =
+ .label = Usuń
+ .accesskey = U
+search-add-engine =
+ .label = Dodaj
+ .accesskey = o
+search-find-more-link = Znajdź więcej wyszukiwarek
+# This warning is displayed when the chosen keyword is already in use
+# ('Duplicate' is an adjective)
+search-keyword-warning-title = Słowo kluczowe już istnieje
+# Variables:
+# $name (String) - Name of a search engine.
+search-keyword-warning-engine = Wybrano słowo kluczowe używane obecnie przez wyszukiwarkę { $name }. Należy wybrać inne.
+search-keyword-warning-bookmark = Wybrano słowo kluczowe używane obecnie przez zakładkę. Należy wybrać inne.
+
+## Containers Section
+
+containers-back-button =
+ .aria-label =
+ { PLATFORM() ->
+ [windows] Wróć do opcji
+ *[other] Wróć do preferencji
+ }
+containers-header = Karty z kontekstem
+containers-add-button =
+ .label = Dodaj kontekst
+ .accesskey = D
+containers-new-tab-check =
+ .label = Wybieraj kontekst dla każdej nowej karty
+ .accesskey = W
+containers-preferences-button =
+ .label = Preferencje
+containers-remove-button =
+ .label = Usuń
+
+## Firefox Account - Signed out. Note that "Sync" and "Firefox Account" are now
+## more discrete ("signed in" no longer means "and sync is connected").
+
+sync-signedout-caption = Zabierz swoją sieć ze sobą
+sync-signedout-description = Synchronizuj zakładki, historię, karty, hasła, dodatki i preferencje między wszystkimi swoimi urządzeniami.
+sync-signedout-account-signin2 =
+ .label = Zaloguj się do { -sync-brand-short-name(case: "gen", capitalization: "lower") }…
+ .accesskey = Z
+# This message contains two links and two icon images.
+# `<img data-l10n-name="android-icon"/>` - Android logo icon
+# `<a data-l10n-name="android-link">` - Link to Android Download
+# `<img data-l10n-name="ios-icon">` - iOS logo icon
+# `<a data-l10n-name="ios-link">` - Link to iOS Download
+#
+# They can be moved within the sentence as needed to adapt
+# to your language, but should not be changed or translated.
+sync-mobile-promo = Firefox na <a data-l10n-name="android-link">Androida</a> <img data-l10n-name="android-icon"/> i <a data-l10n-name="ios-link">iOS</a> <img data-l10n-name="ios-icon"/> daje możliwość synchronizacji z urządzeniami przenośnymi.
+
+## Firefox Account - Signed in
+
+sync-profile-picture =
+ .tooltiptext = Zmień obraz przypisany do konta
+sync-sign-out =
+ .label = Wyloguj się…
+ .accesskey = W
+sync-manage-account = Zarządzaj kontem
+ .accesskey = Z
+sync-signedin-unverified = Konto { $email } nie zostało zweryfikowane.
+sync-signedin-login-failure = Zaloguj się, aby ponownie połączyć konto { $email }
+sync-resend-verification =
+ .label = Wyślij nową wiadomość weryfikującą
+ .accesskey = W
+sync-remove-account =
+ .label = Usuń konto
+ .accesskey = U
+sync-sign-in =
+ .label = Zaloguj się
+ .accesskey = o
+
+## Sync section - enabling or disabling sync.
+
+prefs-syncing-on = Synchronizowanie: włączone
+prefs-syncing-off = Synchronizowanie: wyłączone
+prefs-sync-setup =
+ .label = Skonfiguruj { -sync-brand-short-name(case: "acc", capitalization: "lower") }…
+ .accesskey = S
+prefs-sync-offer-setup-label = Synchronizuj zakładki, historię, karty, hasła, dodatki i preferencje między wszystkimi swoimi urządzeniami.
+prefs-sync-now =
+ .labelnotsyncing = Synchronizuj teraz
+ .accesskeynotsyncing = S
+ .labelsyncing = Synchronizowanie…
+
+## The list of things currently syncing.
+
+sync-currently-syncing-heading = Obecnie synchronizowane:
+sync-currently-syncing-bookmarks = zakładki
+sync-currently-syncing-history = historia
+sync-currently-syncing-tabs = otwarte karty
+sync-currently-syncing-logins-passwords = dane logowania i hasła
+sync-currently-syncing-addresses = adresy
+sync-currently-syncing-creditcards = karty płatnicze
+sync-currently-syncing-addons = dodatki
+sync-currently-syncing-prefs =
+ { PLATFORM() ->
+ [windows] opcje
+ *[other] preferencje
+ }
+sync-change-options =
+ .label = Zmień…
+ .accesskey = m
+
+## The "Choose what to sync" dialog.
+
+sync-choose-what-to-sync-dialog =
+ .title = Wybierz, co synchronizować
+ .style = width: 36em; min-height: 35em;
+ .buttonlabelaccept = Zapisz zmiany
+ .buttonaccesskeyaccept = Z
+ .buttonlabelextra2 = Rozłącz…
+ .buttonaccesskeyextra2 = R
+sync-engine-bookmarks =
+ .label = zakładki
+ .accesskey = z
+sync-engine-history =
+ .label = historia
+ .accesskey = h
+sync-engine-tabs =
+ .label = karty
+ .tooltiptext = Lista otwartych stron na wszystkich synchronizowanych urządzeniach
+ .accesskey = k
+sync-engine-logins-passwords =
+ .label = dane logowania i hasła
+ .tooltiptext = Zachowane nazwy użytkownika i hasła
+ .accesskey = l
+sync-engine-addresses =
+ .label = adresy
+ .tooltiptext = Zachowane adresy pocztowe (tylko na komputerach)
+ .accesskey = a
+sync-engine-creditcards =
+ .label = dane kart płatniczych
+ .tooltiptext = Nazwiska, numery i okresy ważności (tylko na komputerach)
+ .accesskey = d
+sync-engine-addons =
+ .label = dodatki
+ .tooltiptext = Rozszerzenia i motywy w wersji na komputery
+ .accesskey = d
+sync-engine-prefs =
+ .label =
+ { PLATFORM() ->
+ [windows] opcje
+ *[other] preferencje
+ }
+ .tooltiptext = Zmienione ustawienia ogólne, uruchamiania, wyszukiwania, prywatności i bezpieczeństwa
+ .accesskey = e
+
+## The device name controls.
+
+sync-device-name-header = Nazwa urządzenia
+sync-device-name-change =
+ .label = Zmień nazwę urządzenia…
+ .accesskey = n
+sync-device-name-cancel =
+ .label = Anuluj
+ .accesskey = A
+sync-device-name-save =
+ .label = Zachowaj
+ .accesskey = Z
+sync-connect-another-device = Połącz inne urządzenie
+
+## Privacy Section
+
+privacy-header = Prywatność
+
+## Privacy Section - Logins and Passwords
+
+# The search keyword isn't shown to users but is used to find relevant settings in about:preferences.
+pane-privacy-logins-and-passwords-header = Dane logowania i hasła
+ .searchkeywords = { -lockwise-brand-short-name }
+# Checkbox to control whether UI is shown to users to save or fill logins/passwords.
+forms-ask-to-save-logins =
+ .label = Pytanie o zachowywanie danych logowania do witryn
+ .accesskey = P
+forms-exceptions =
+ .label = Wyjątki…
+ .accesskey = i
+forms-generate-passwords =
+ .label = Proponowanie i generowanie silnych haseł
+ .accesskey = s
+forms-breach-alerts =
+ .label = Powiadomienia o hasłach do stron, z których wyciekły dane.
+ .accesskey = o
+forms-breach-alerts-learn-more-link = Więcej informacji
+# Checkbox which controls filling saved logins into fields automatically when they appear, in some cases without user interaction.
+forms-fill-logins-and-passwords =
+ .label = Automatyczne wypełnianie formularzy logowania
+ .accesskey = A
+forms-saved-logins =
+ .label = Zachowane dane logowania…
+ .accesskey = d
+forms-master-pw-use =
+ .label = Hasło główne
+ .accesskey = H
+forms-primary-pw-use =
+ .label = Hasło główne.
+ .accesskey = H
+forms-primary-pw-learn-more-link = Więcej informacji
+# This string uses the former name of the Primary Password feature
+# ("Master Password" in English) so that the preferences can be found
+# when searching for the old name. The accesskey is unused.
+forms-master-pw-change =
+ .label = Zmień hasło główne…
+ .accesskey = Z
+forms-master-pw-fips-title = Program pracuje obecnie w trybie FIPS. Tryb FIPS wymaga niepustego hasła głównego.
+forms-primary-pw-change =
+ .label = Zmień hasło główne…
+ .accesskey = Z
+# Leave this message empty if the translation for "Primary Password" matches
+# "Master Password" in your language. If you're editing the FTL file directly,
+# use { "" } as the value.
+forms-primary-pw-former-name = { "" }
+forms-primary-pw-fips-title = Program pracuje obecnie w trybie FIPS. Tryb FIPS wymaga niepustego hasła głównego.
+forms-master-pw-fips-desc = Zmiana hasła się nie powiodła.
+
+## OS Authentication dialog
+
+# This message can be seen by trying to add a Master Password.
+master-password-os-auth-dialog-message-win = Aby utworzyć hasło główne, wprowadź swoje dane logowania do systemu Windows. Pomaga to chronić bezpieczeństwo Twoich kont.
+# This message can be seen by trying to add a Master Password.
+# The macOS strings are preceded by the operating system with "Firefox is trying to "
+# and includes subtitle of "Enter password for the user "xxx" to allow this." These
+# notes are only valid for English. Please test in your locale.
+master-password-os-auth-dialog-message-macosx = utworzenie hasła głównego
+# This message can be seen by trying to add a Primary Password.
+primary-password-os-auth-dialog-message-win = Aby utworzyć hasło główne, wprowadź swoje dane logowania do systemu Windows. Pomaga to chronić bezpieczeństwo Twoich kont.
+# This message can be seen by trying to add a Primary Password.
+# The macOS strings are preceded by the operating system with "Firefox is trying to "
+# and includes subtitle of "Enter password for the user "xxx" to allow this." These
+# notes are only valid for English. Please test in your locale.
+primary-password-os-auth-dialog-message-macosx = utworzenie hasła głównego
+master-password-os-auth-dialog-caption = { -brand-full-name }
+
+## Privacy Section - History
+
+history-header = Historia
+# This label is followed, on the same line, by a dropdown list of options
+# (Remember history, etc.).
+# In English it visually creates a full sentence, e.g.
+# "Firefox will" + "Remember history".
+#
+# If this doesn't work for your language, you can translate this message:
+# - Simply as "Firefox", moving the verb into each option.
+# This will result in "Firefox" + "Will remember history", etc.
+# - As a stand-alone message, for example "Firefox history settings:".
+history-remember-label = Program { -brand-short-name }:
+ .accesskey = m
+history-remember-option-all =
+ .label = będzie zachowywał historię
+history-remember-option-never =
+ .label = nie będzie zachowywał historii
+history-remember-option-custom =
+ .label = będzie używał ustawień użytkownika
+history-remember-description = { -brand-short-name } zachowa historię przeglądania, wyszukiwania, pobieranych plików i danych formularzy.
+history-dontremember-description = { -brand-short-name } będzie używał tych samych ustawień co w trybie prywatnym i nie będzie zachowywał historii podczas przeglądania witryn WWW.
+history-private-browsing-permanent =
+ .label = Zawsze aktywny tryb przeglądania prywatnego
+ .accesskey = t
+history-remember-browser-option =
+ .label = Zachowywanie historii przeglądania i pobierania plików
+ .accesskey = h
+history-remember-search-option =
+ .label = Zachowywanie historii pola wyszukiwania i formularzy
+ .accesskey = o
+history-clear-on-close-option =
+ .label = Czyszczenie historii podczas zamykania przeglądarki { -brand-short-name }
+ .accesskey = z
+history-clear-on-close-settings =
+ .label = Ustawienia…
+ .accesskey = U
+history-clear-button =
+ .label = Wyczyść historię…
+ .accesskey = W
+
+## Privacy Section - Site Data
+
+sitedata-header = Ciasteczka i dane stron
+sitedata-total-size-calculating = Obliczanie rozmiaru danych i pamięci podręcznej stron…
+# Variables:
+# $value (Number) - Value of the unit (for example: 4.6, 500)
+# $unit (String) - Name of the unit (for example: "bytes", "KB")
+sitedata-total-size = Przechowywane ciasteczka, dane i pamięć podręczna stron zajmują { $value } { $unit } na dysku.
+sitedata-learn-more = Więcej informacji
+sitedata-delete-on-close =
+ .label = Usuwanie ciasteczek i danych stron podczas zamykania przeglądarki { -brand-short-name }
+ .accesskey = U
+sitedata-delete-on-close-private-browsing = W stale aktywnym trybie przeglądania prywatnego ciasteczka i dane stron są zawsze usuwane podczas zamykania programu { -brand-short-name }.
+sitedata-allow-cookies-option =
+ .label = Akceptowanie ciasteczek i danych stron
+ .accesskey = A
+sitedata-disallow-cookies-option =
+ .label = Blokowanie ciasteczek i danych stron
+ .accesskey = B
+# This label means 'type of content that is blocked', and is followed by a drop-down list with content types below.
+# The list items are the strings named sitedata-block-*-option*.
+sitedata-block-desc = Blokowanie:
+ .accesskey = B
+sitedata-option-block-cross-site-trackers =
+ .label = śledzące między witrynami
+sitedata-option-block-cross-site-and-social-media-trackers =
+ .label = śledzące między witrynami i serwisów społecznościowych
+sitedata-option-block-cross-site-tracking-cookies-including-social-media =
+ .label = ciasteczka śledzące między witrynami — w tym ciasteczka serwisów społecznościowych
+sitedata-option-block-cross-site-cookies-including-social-media =
+ .label = ciasteczka między witrynami — w tym ciasteczka serwisów społecznościowych
+sitedata-option-block-cross-site-and-social-media-trackers-plus-isolate =
+ .label = śledzące między witrynami i serwisów społecznościowych oraz izolowanie pozostałych ciasteczek
+sitedata-option-block-unvisited =
+ .label = nieodwiedzonych witryn
+sitedata-option-block-all-third-party =
+ .label = wszystkie zewnętrznych witryn (może powodować problemy)
+sitedata-option-block-all =
+ .label = wszystkie (powoduje problemy)
+sitedata-clear =
+ .label = Wyczyść dane…
+ .accesskey = a
+sitedata-settings =
+ .label = Zachowane dane…
+ .accesskey = c
+sitedata-cookies-permissions =
+ .label = Wyjątki…
+ .accesskey = W
+sitedata-cookies-exceptions =
+ .label = Wyjątki…
+ .accesskey = W
+
+## Privacy Section - Address Bar
+
+addressbar-header = Pasek adresu
+addressbar-suggest = Podpowiedzi w pasku adresu opieraj na:
+addressbar-locbar-history-option =
+ .label = historii przeglądania
+ .accesskey = h
+addressbar-locbar-bookmarks-option =
+ .label = zakładkach
+ .accesskey = z
+addressbar-locbar-openpage-option =
+ .label = otwartych kartach
+ .accesskey = k
+addressbar-locbar-topsites-option =
+ .label = popularnych witrynach
+ .accesskey = w
+addressbar-suggestions-settings = Zmień preferencje podpowiedzi dostarczanych przez wyszukiwarki
+
+## Privacy Section - Content Blocking
+
+content-blocking-enhanced-tracking-protection = Wzmocniona ochrona przed śledzeniem
+content-blocking-section-top-level-description = Elementy śledzące monitorują Cię w Internecie, zbierając informacje o Twoich działaniach i zainteresowaniach. { -brand-short-name } blokuje wiele tych elementów i inne złośliwe skrypty.
+content-blocking-learn-more = Więcej informacji
+content-blocking-fpi-incompatibility-warning = Używasz funkcji FPI („First Party Isolation”), która zastępuje część ustawień ciasteczek przeglądarki { -brand-short-name }.
+
+## These strings are used to define the different levels of
+## Enhanced Tracking Protection.
+
+# "Standard" in this case is an adjective, meaning "default" or "normal".
+enhanced-tracking-protection-setting-standard =
+ .label = Standardowa
+ .accesskey = S
+enhanced-tracking-protection-setting-strict =
+ .label = Ścisła
+ .accesskey = c
+enhanced-tracking-protection-setting-custom =
+ .label = Własna
+ .accesskey = W
+
+##
+
+content-blocking-etp-standard-desc = Równowaga między bezpieczeństwem a szybkością wczytywania stron. Strony będą działać bez problemów.
+content-blocking-etp-strict-desc = Silniejsza ochrona, ale może powodować niepoprawne działanie niektórych stron.
+content-blocking-etp-custom-desc = Wybierz, które elementy śledzące i skrypty blokować:
+content-blocking-private-windows = treści z elementami śledzącymi w oknach prywatnych
+content-blocking-cross-site-cookies = ciasteczka między witrynami
+content-blocking-cross-site-tracking-cookies = ciasteczka śledzące między witrynami
+content-blocking-cross-site-tracking-cookies-plus-isolate = ciasteczka śledzące między witrynami i izolowanie pozostałych
+content-blocking-social-media-trackers = elementy śledzące serwisów społecznościowych
+content-blocking-all-cookies = wszystkie ciasteczka
+content-blocking-unvisited-cookies = ciasteczka z nieodwiedzonych witryn
+content-blocking-all-windows-tracking-content = treści z elementami śledzącymi we wszystkich oknach
+content-blocking-all-third-party-cookies = wszystkie ciasteczka zewnętrznych witryn
+content-blocking-cryptominers = elementy używające komputera użytkownika do generowania kryptowalut
+content-blocking-fingerprinters = elementy śledzące przez zbieranie informacji o konfiguracji
+content-blocking-warning-title = Ostrzeżenie
+content-blocking-and-isolating-etp-warning-description = Blokowanie elementów śledzących i izolowanie ciasteczek może wpłynąć na funkcjonowanie niektórych stron. Odśwież stronę z włączonymi elementami śledzącymi, aby wyświetlić całą jej zawartość.
+content-blocking-and-isolating-etp-warning-description-2 = To ustawienie może spowodować niepoprawne działanie lub wyświetlanie niektórych stron. Jeśli dana strona wydaje się niewłaściwie działać, możesz wyłączyć dla niej ochronę przed śledzeniem, aby wczytać ją w całości.
+content-blocking-warning-learn-how = Więcej informacji
+content-blocking-reload-description = Zastosowanie tych zmian wymaga odświeżenia kart.
+content-blocking-reload-tabs-button =
+ .label = Odśwież wszystkie karty
+ .accesskey = O
+content-blocking-tracking-content-label =
+ .label = treści z elementami śledzącymi:
+ .accesskey = e
+content-blocking-tracking-protection-option-all-windows =
+ .label = zawsze
+ .accesskey = z
+content-blocking-option-private =
+ .label = w oknach prywatnych
+ .accesskey = w
+content-blocking-tracking-protection-change-block-list = Zmień listę blokowanych
+content-blocking-cookies-label =
+ .label = ciasteczka:
+ .accesskey = c
+content-blocking-expand-section =
+ .tooltiptext = Więcej informacji
+# Cryptomining refers to using scripts on websites that can use a computer’s resources to mine cryptocurrency without a user’s knowledge.
+content-blocking-cryptominers-label =
+ .label = elementy używające komputera użytkownika do generowania kryptowalut
+ .accesskey = e
+# Browser fingerprinting is a method of tracking users by the configuration and settings information (their "digital fingerprint")
+# that is visible to websites they browse, rather than traditional tracking methods such as IP addresses and unique cookies.
+content-blocking-fingerprinters-label =
+ .label = elementy śledzące przez zbieranie informacji o konfiguracji
+ .accesskey = k
+
+## Privacy Section - Tracking
+
+tracking-manage-exceptions =
+ .label = Wyjątki…
+ .accesskey = W
+
+## Privacy Section - Permissions
+
+permissions-header = Uprawnienia
+permissions-location = Położenie
+permissions-location-settings =
+ .label = Ustawienia…
+ .accesskey = t
+permissions-xr = Rzeczywistość wirtualna
+permissions-xr-settings =
+ .label = Ustawienia…
+ .accesskey = e
+permissions-camera = Kamera
+permissions-camera-settings =
+ .label = Ustawienia…
+ .accesskey = a
+permissions-microphone = Mikrofon
+permissions-microphone-settings =
+ .label = Ustawienia…
+ .accesskey = w
+permissions-notification = Powiadomienia.
+permissions-notification-settings =
+ .label = Ustawienia…
+ .accesskey = s
+permissions-notification-link = Więcej informacji
+permissions-notification-pause =
+ .label = Wstrzymaj powiadomienia do czasu ponownego uruchomienia przeglądarki { -brand-short-name }
+ .accesskey = W
+permissions-autoplay = Automatyczne odtwarzanie
+permissions-autoplay-settings =
+ .label = Ustawienia…
+ .accesskey = n
+permissions-block-popups =
+ .label = Blokowanie wyskakujących okien
+ .accesskey = B
+permissions-block-popups-exceptions =
+ .label = Wyjątki…
+ .accesskey = t
+permissions-addon-install-warning =
+ .label = Ostrzeganie, gdy witryny próbują instalować dodatki
+ .accesskey = O
+permissions-addon-exceptions =
+ .label = Wyjątki…
+ .accesskey = W
+permissions-a11y-privacy-checkbox =
+ .label = Blokowanie dostępu do przeglądarki usługom ułatwień dostępu.
+ .accesskey = u
+permissions-a11y-privacy-link = Więcej informacji
+
+## Privacy Section - Data Collection
+
+collection-header = Dane zbierane przez program { -brand-short-name }
+collection-description = Dążymy do zapewnienia odpowiedniego wyboru i zbierania wyłącznie niezbędnych danych, aby dostarczać i doskonalić program { -brand-short-name } dla nas wszystkich. Zawsze prosimy o pozwolenie przed przesłaniem danych osobistych.
+collection-privacy-notice = Prywatność
+collection-health-report-telemetry-disabled = { -vendor-short-name } nie ma już zezwolenia na zbieranie danych technicznych i o interakcjach z przeglądarką. Wszystkie wcześniej zebrane dane zostaną usunięte w ciągu 30 dni.
+collection-health-report-telemetry-disabled-link = Więcej informacji
+collection-health-report =
+ .label = Przesyłanie do organizacji { -vendor-short-name } danych technicznych i o interakcjach z przeglądarką { -brand-short-name }.
+ .accesskey = z
+collection-health-report-link = Więcej informacji
+collection-studies =
+ .label = Instalowanie i przeprowadzanie badań przez przeglądarkę { -brand-short-name }.
+collection-studies-link = Wyświetl badania przeglądarki { -brand-short-name }
+addon-recommendations =
+ .label = Personalizowane polecenia rozszerzeń przez przeglądarkę { -brand-short-name }.
+addon-recommendations-link = Więcej informacji
+# This message is displayed above disabled data sharing options in developer builds
+# or builds with no Telemetry support available.
+collection-health-report-disabled = Przesyłanie danych jest wyłączone przy tej konfiguracji programu
+collection-backlogged-crash-reports =
+ .label = Przesyłanie zgromadzonych zgłoszeń awarii przeglądarki { -brand-short-name }.
+ .accesskey = o
+collection-backlogged-crash-reports-link = Więcej informacji
+
+## Privacy Section - Security
+##
+## It is important that wording follows the guidelines outlined on this page:
+## https://developers.google.com/safe-browsing/developers_guide_v2#AcceptableUsage
+
+security-header = Bezpieczeństwo
+security-browsing-protection = Ochrona przed oszustwami i niebezpiecznym oprogramowaniem
+security-enable-safe-browsing =
+ .label = Blokowanie niebezpiecznych i podejrzanych treści.
+ .accesskey = B
+security-enable-safe-browsing-link = Więcej informacji
+security-block-downloads =
+ .label = Blokowanie możliwości pobierania niebezpiecznych plików
+ .accesskey = e
+security-block-uncommon-software =
+ .label = Ostrzeganie przed niepożądanym i nietypowym oprogramowaniem
+ .accesskey = n
+
+## Privacy Section - Certificates
+
+certs-header = Certyfikaty
+certs-personal-label = Kiedy serwer żąda osobistego certyfikatu użytkownika:
+certs-select-auto-option =
+ .label = wybierz certyfikat automatycznie
+ .accesskey = a
+certs-select-ask-option =
+ .label = pytaj za każdym razem
+ .accesskey = r
+certs-enable-ocsp =
+ .label = Odpytywanie serwerów OCSP w celu potwierdzenia wiarygodności certyfikatów
+ .accesskey = O
+certs-view =
+ .label = Wyświetl certyfikaty…
+ .accesskey = W
+certs-devices =
+ .label = Urządzenia zabezpieczające…
+ .accesskey = U
+space-alert-learn-more-button =
+ .label = Więcej informacji
+ .accesskey = W
+space-alert-over-5gb-pref-button =
+ .label =
+ { PLATFORM() ->
+ [windows] Otwórz opcje
+ *[other] Otwórz preferencje
+ }
+ .accesskey =
+ { PLATFORM() ->
+ [windows] O
+ *[other] O
+ }
+space-alert-over-5gb-message =
+ { PLATFORM() ->
+ [windows] Przeglądarce { -brand-short-name } zaczyna brakować miejsca na dysku. Zawartość stron może być wyświetlana niepoprawnie. Przechowywane dane może wyczyścić w Opcje → Prywatność i bezpieczeństwo → Ciasteczka i dane stron.
+ *[other] Przeglądarce { -brand-short-name } zaczyna brakować miejsca na dysku. Zawartość stron może być wyświetlana niepoprawnie. Przechowywane dane może wyczyścić w Preferencje → Prywatność i bezpieczeństwo → Ciasteczka i dane stron.
+ }
+space-alert-under-5gb-ok-button =
+ .label = OK
+ .accesskey = O
+space-alert-under-5gb-message = Przeglądarce { -brand-short-name } zaczyna brakować miejsca na dysku. Zawartość stron może być wyświetlana niepoprawnie. Skorzystaj z odnośnika „Więcej informacji”, aby zoptymalizować użycie dysku dla lepszego przeglądania.
+
+## Privacy Section - HTTPS-Only
+
+httpsonly-header = Tryb używania wyłącznie protokołu HTTPS
+httpsonly-description = Protokół HTTPS zapewnia zabezpieczone, zaszyfrowane połączenie między przeglądarką { -brand-short-name } a odwiedzanymi witrynami. Większość witryn obsługuje HTTPS, a jeśli tryb używania wyłącznie protokołu HTTPS jest włączony, to { -brand-short-name } przełączy wszystkie połączenia na HTTPS.
+httpsonly-learn-more = Więcej informacji
+httpsonly-radio-enabled =
+ .label = Tryb używania wyłącznie protokołu HTTPS we wszystkich oknach
+httpsonly-radio-enabled-pbm =
+ .label = Tryb używania wyłącznie protokołu HTTPS tylko w oknach prywatnych
+httpsonly-radio-disabled =
+ .label = Nie włączaj trybu używania wyłącznie protokołu HTTPS
+
+## The following strings are used in the Download section of settings
+
+desktop-folder-name = Pulpit
+downloads-folder-name = Pobrane
+choose-download-folder-title = Wybór folderu dla pobieranych plików
+# Variables:
+# $service-name (String) - Name of a cloud storage provider like Dropbox, Google Drive, etc...
+save-files-to-cloud-storage =
+ .label = Wysyłanie plików do usługi { $service-name }
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/selectBookmark.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/selectBookmark.ftl
new file mode 100644
index 0000000000..af7f888172
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/selectBookmark.ftl
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+select-bookmark-window =
+ .title = Ustaw stronę startową
+ .style = width: 32em;
+
+select-bookmark-desc = Wybierz zakładkę, która ma stać się stroną startową. Jeśli wskazany zostanie folder, zakładki z tego folderu zostaną otwarte w kartach.
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/siteDataSettings.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/siteDataSettings.ftl
new file mode 100644
index 0000000000..815863aca9
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/preferences/siteDataSettings.ftl
@@ -0,0 +1,57 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+## Settings
+
+site-data-settings-window =
+ .title = Zachowane ciasteczka i dane stron
+site-data-settings-description = Następujące witryny przechowują ciasteczka i dane na tym komputerze. { -brand-short-name } trwale przechowuje dane do czasu ręcznego ich usunięcia i usuwa nietrwałe dane, jeśli potrzebna jest przestrzeń.
+site-data-search-textbox =
+ .placeholder = Szukaj witryn
+ .accesskey = S
+site-data-column-host =
+ .label = Witryna
+site-data-column-cookies =
+ .label = Ciasteczka
+site-data-column-storage =
+ .label = Rozmiar
+site-data-column-last-used =
+ .label = Ostatni dostęp
+# This label is used in the "Host" column for local files, which have no host.
+site-data-local-file-host = (plik lokalny)
+site-data-remove-selected =
+ .label = Usuń zaznaczone
+ .accesskey = U
+site-data-button-cancel =
+ .label = Anuluj
+ .accesskey = A
+site-data-button-save =
+ .label = Zapisz zmiany
+ .accesskey = Z
+site-data-settings-dialog =
+ .buttonlabelaccept = Zapisz zmiany
+ .buttonaccesskeyaccept = Z
+# Variables:
+# $value (Number) - Value of the unit (for example: 4.6, 500)
+# $unit (String) - Name of the unit (for example: "bytes", "KB")
+site-storage-usage =
+ .value = { $value } { $unit }
+site-storage-persistent =
+ .value = { site-storage-usage.value } (trwałe)
+site-data-remove-all =
+ .label = Usuń wszystkie
+ .accesskey = U
+site-data-remove-shown =
+ .label = Usuń wszystkie wyświetlane
+ .accesskey = U
+
+## Removing
+
+site-data-removing-dialog =
+ .title = { site-data-removing-header }
+ .buttonlabelaccept = Usuń
+site-data-removing-header = Usuwanie ciasteczek i danych stron
+site-data-removing-desc = Usunięcie ciasteczek i danych stron może spowodować wylogowanie z niektórych witryn. Czy wprowadzić zmiany?
+site-data-removing-table = Ciasteczka i dane stron następujących witryn zostaną usunięte:
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/protectionsPanel.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/protectionsPanel.ftl
new file mode 100644
index 0000000000..8a3af1162d
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/protectionsPanel.ftl
@@ -0,0 +1,108 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+protections-panel-sendreportview-error = Wystąpił błąd podczas wysyłania zgłoszenia. Proszę spróbować ponownie później.
+
+# A link shown when ETP is disabled for a site. Opens the breakage report subview when clicked.
+protections-panel-sitefixedsendreport-label = Naprawiło to stronę? Wyślij zgłoszenie
+
+## These strings are used to define the different levels of
+## Enhanced Tracking Protection.
+
+protections-popup-footer-protection-label-strict = Ścisła
+ .label = Ścisła
+protections-popup-footer-protection-label-custom = Własna
+ .label = Własna
+protections-popup-footer-protection-label-standard = Standardowa
+ .label = Standardowa
+
+##
+
+# The text a screen reader speaks when focused on the info button.
+protections-panel-etp-more-info =
+ .aria-label = Więcej informacji o wzmocnionej ochronie przed śledzeniem
+
+protections-panel-etp-on-header = Wzmocniona ochrona przed śledzeniem jest włączona na tej witrynie
+protections-panel-etp-off-header = Wzmocniona ochrona przed śledzeniem jest wyłączona na tej witrynie
+
+# The link to be clicked to open the sub-panel view
+protections-panel-site-not-working = Strona nie działa?
+
+# The heading/title of the sub-panel view
+protections-panel-site-not-working-view =
+ .title = Strona nie działa?
+
+## The "Allowed" header also includes a "Why?" link that, when hovered, shows
+## a tooltip explaining why these items were not blocked in the page.
+
+protections-panel-not-blocking-why-label = Dlaczego?
+protections-panel-not-blocking-why-etp-on-tooltip = Blokowanie tych elementów może powodować niepoprawne działanie niektórych stron. Bez elementów śledzących niektóre przyciski, formularze i pola logowania mogą nie działać.
+protections-panel-not-blocking-why-etp-off-tooltip = Wszystkie elementy śledzące na tej stronie zostały wczytane, ponieważ ochrona jest wyłączona.
+
+##
+
+protections-panel-no-trackers-found = { -brand-short-name } nie wykrył na tej stronie znanych elementów śledzących.
+
+protections-panel-content-blocking-tracking-protection = Treści z elementami śledzącymi
+
+protections-panel-content-blocking-socialblock = Elementy śledzące serwisów społecznościowych
+protections-panel-content-blocking-cryptominers-label = Elementy używające komputera użytkownika do generowania kryptowalut
+protections-panel-content-blocking-fingerprinters-label = Elementy śledzące przez zbieranie informacji o konfiguracji
+
+## In the protections panel, Content Blocking category items are in three sections:
+## "Blocked" for categories being blocked in the current page,
+## "Allowed" for categories detected but not blocked in the current page, and
+## "None Detected" for categories not detected in the current page.
+## These strings are used in the header labels of each of these sections.
+
+protections-panel-blocking-label = Zablokowane
+protections-panel-not-blocking-label = Dopuszczone
+protections-panel-not-found-label = Niewykryte
+
+##
+
+protections-panel-settings-label = Ustawienia ochrony
+# This should match the "appmenuitem-protection-dashboard-title" string in browser/appmenu.ftl.
+protections-panel-protectionsdashboard-label = Panel ochrony
+
+## In the Site Not Working? view, we suggest turning off protections if
+## the user is experiencing issues with any of a variety of functionality.
+
+# The header of the list
+protections-panel-site-not-working-view-header = Wyłącz ochronę, jeśli masz problemy z:
+
+# The list items, shown in a <ul>
+protections-panel-site-not-working-view-issue-list-login-fields = polami logowania
+protections-panel-site-not-working-view-issue-list-forms = formularzami
+protections-panel-site-not-working-view-issue-list-payments = płatnościami
+protections-panel-site-not-working-view-issue-list-comments = komentarzami
+protections-panel-site-not-working-view-issue-list-videos = filmami
+
+protections-panel-site-not-working-view-send-report = Wyślij zgłoszenie
+
+##
+
+protections-panel-cross-site-tracking-cookies = Te ciasteczka śledzą Cię od strony do strony w celu zbierania danych o tym, co robisz w Internecie. Są umieszczane przez zewnętrzne firmy, takie jak agencje reklamowe i firmy analityczne.
+protections-panel-cryptominers = Te elementy wykorzystują moc obliczeniową Twojego komputera do generowania cyfrowych walut. Skrypty generujące kryptowaluty rozładowują baterię, spowalniają komputer i mogą zwiększyć rachunek za prąd.
+protections-panel-fingerprinters = Te elementy zbierają ustawienia przeglądarki i komputera, aby utworzyć profil użytkownika. Za pomocą tego cyfrowego odcisku palca mogą śledzić Cię między różnymi witrynami.
+protections-panel-tracking-content = Witryny mogą wczytywać zewnętrzne reklamy, filmy i inne treści z elementami śledzącymi. Blokowanie ich może przyspieszyć wczytywanie stron, ale niektóre przyciski, formularze i pola logowania mogą działać niepoprawnie.
+protections-panel-social-media-trackers = Serwisy społecznościowe umieszczają elementy śledzące na innych witrynach, aby śledzić co robisz, widzisz i oglądasz w Internecie. Dzięki temu ich właściciele wiedzą o Tobie więcej, niż udostępniasz w ich serwisach.
+
+protections-panel-content-blocking-manage-settings =
+ .label = Zarządzaj ustawieniami ochrony
+ .accesskey = Z
+
+protections-panel-content-blocking-breakage-report-view =
+ .title = Zgłoś niepoprawnie działającą stronę
+protections-panel-content-blocking-breakage-report-view-description = Blokowanie pewnych elementów śledzących może powodować problemy z niektórymi stronami. Zgłaszając problemy, pomagasz ulepszać program { -brand-short-name } (adres odwiedzanej strony oraz informacje o ustawieniach przeglądarki zostaną przesłane do Mozilli). <label data-l10n-name="learn-more">Więcej informacji</label>
+protections-panel-content-blocking-breakage-report-view-collection-url = Adres URL problematycznej strony
+protections-panel-content-blocking-breakage-report-view-collection-url-label =
+ .aria-label = Adres URL problematycznej strony
+protections-panel-content-blocking-breakage-report-view-collection-comments = Opcjonalnie: opisz problem
+protections-panel-content-blocking-breakage-report-view-collection-comments-label =
+ .aria-label = Opcjonalnie: opisz problem
+protections-panel-content-blocking-breakage-report-view-cancel =
+ .label = Anuluj
+protections-panel-content-blocking-breakage-report-view-send-report =
+ .label = Wyślij zgłoszenie
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/sanitize.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/sanitize.ftl
new file mode 100644
index 0000000000..dcb99b654c
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/sanitize.ftl
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+sanitize-prefs =
+ .title = Ustawienia czyszczenia historii
+ .style = width: 41em
+
+sanitize-prefs-style =
+ .style = width: 25em
+
+dialog-title =
+ .title = Czyszczenie historii
+ .style = width: 42em
+
+# When "Time range to clear" is set to "Everything", this message is used for the
+# title instead of dialog-title.
+dialog-title-everything =
+ .title = Czyszczenie historii
+ .style = width: 42em
+
+clear-data-settings-label = Rzeczy zaznaczone poniżej będą usuwane podczas zamykania przeglądarki { -brand-short-name }.
+
+## clear-time-duration-prefix is followed by a dropdown list, with
+## values localized using clear-time-duration-value-* messages.
+## clear-time-duration-suffix is left empty in English, but can be
+## used in other languages to change the structure of the message.
+##
+## This results in English:
+## Time range to clear: (Last Hour, Today, etc.)
+
+clear-time-duration-prefix =
+ .value = Okres do wyczyszczenia:
+ .accesskey = O
+
+clear-time-duration-value-last-hour =
+ .label = ostatnia godzina
+
+clear-time-duration-value-last-2-hours =
+ .label = ostatnie dwie godziny
+
+clear-time-duration-value-last-4-hours =
+ .label = ostatnie cztery godziny
+
+clear-time-duration-value-today =
+ .label = dzisiaj
+
+clear-time-duration-value-everything =
+ .label = wszystko
+
+clear-time-duration-suffix =
+ .value = { "" }
+
+## These strings are used as section comments and checkboxes
+## to select the items to remove
+
+history-section-label = Historia
+
+item-history-and-downloads =
+ .label = Historia przeglądanych stron i pobranych plików
+ .accesskey = H
+
+item-cookies =
+ .label = Ciasteczka
+ .accesskey = C
+
+item-active-logins =
+ .label = Aktywne zalogowania
+ .accesskey = A
+
+item-cache =
+ .label = Pamięć podręczna
+ .accesskey = P
+
+item-form-search-history =
+ .label = Dane formularzy i historia paska wyszukiwania
+ .accesskey = D
+
+data-section-label = Dane
+
+item-site-preferences =
+ .label = Ustawienia uprawnień witryn
+ .accesskey = U
+
+item-offline-apps =
+ .label = Dane witryn trybu offline
+ .accesskey = w
+
+sanitize-everything-undo-warning = Tej czynności nie można cofnąć!
+
+window-close =
+ .key = w
+
+sanitize-button-ok =
+ .label = Wyczyść
+
+# The label for the default button between the user clicking it and the window
+# closing. Indicates the items are being cleared.
+sanitize-button-clearing =
+ .label = Czyszczenie…
+
+# Warning that appears when "Time range to clear" is set to "Everything" in Clear
+# Recent History dialog, provided that the user has not modified the default set
+# of history items to clear.
+sanitize-everything-warning = Cała historia zostanie wyczyszczona.
+
+# Warning that appears when "Time range to clear" is set to "Everything" in Clear
+# Recent History dialog, provided that the user has modified the default set of
+# history items to clear.
+sanitize-selected-warning = Wszystkie zaznaczone elementy zostaną wyczyszczone.
diff --git a/third_party/rust/fluent-testing/resources/browser/pl/browser/sidebarMenu.ftl b/third_party/rust/fluent-testing/resources/browser/pl/browser/sidebarMenu.ftl
new file mode 100644
index 0000000000..8cb8bda969
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/browser/pl/browser/sidebarMenu.ftl
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+sidebar-menu-bookmarks =
+ .label = Zakładki
+
+sidebar-menu-history =
+ .label = Historia
+
+sidebar-menu-synced-tabs =
+ .label = Karty z innych urządzeń
+
+sidebar-menu-close =
+ .label = Zamknij panel boczny
diff --git a/third_party/rust/fluent-testing/resources/empty-resource/en-US/empty/empty-all.ftl b/third_party/rust/fluent-testing/resources/empty-resource/en-US/empty/empty-all.ftl
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/empty-resource/en-US/empty/empty-all.ftl
diff --git a/third_party/rust/fluent-testing/resources/empty-resource/en-US/empty/empty-one.ftl b/third_party/rust/fluent-testing/resources/empty-resource/en-US/empty/empty-one.ftl
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/empty-resource/en-US/empty/empty-one.ftl
diff --git a/third_party/rust/fluent-testing/resources/empty-resource/pl/empty/empty-all.ftl b/third_party/rust/fluent-testing/resources/empty-resource/pl/empty/empty-all.ftl
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/empty-resource/pl/empty/empty-all.ftl
diff --git a/third_party/rust/fluent-testing/resources/empty-resource/pl/empty/empty-one.ftl b/third_party/rust/fluent-testing/resources/empty-resource/pl/empty/empty-one.ftl
new file mode 100644
index 0000000000..82e3352229
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/empty-resource/pl/empty/empty-one.ftl
@@ -0,0 +1 @@
+empty-one = pusty
diff --git a/third_party/rust/fluent-testing/resources/missing-resource/pl/missing/missing-one.ftl b/third_party/rust/fluent-testing/resources/missing-resource/pl/missing/missing-one.ftl
new file mode 100644
index 0000000000..62a853fa68
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/missing-resource/pl/missing/missing-one.ftl
@@ -0,0 +1 @@
+missing-one = zaginiony
diff --git a/third_party/rust/fluent-testing/resources/toolkit/en-US/security/certificates/certManager.ftl b/third_party/rust/fluent-testing/resources/toolkit/en-US/security/certificates/certManager.ftl
new file mode 100644
index 0000000000..df99de9c0b
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/en-US/security/certificates/certManager.ftl
@@ -0,0 +1,225 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+certmgr-title =
+ .title = Certificate Manager
+
+certmgr-tab-mine =
+ .label = Your Certificates
+
+certmgr-tab-remembered =
+ .label = Authentication Decisions
+
+certmgr-tab-people =
+ .label = People
+
+certmgr-tab-servers =
+ .label = Servers
+
+certmgr-tab-ca =
+ .label = Authorities
+
+certmgr-mine = You have certificates from these organizations that identify you
+certmgr-remembered = These certificates are used to identify you to websites
+certmgr-people = You have certificates on file that identify these people
+certmgr-server = These entries identify server certificate error exceptions
+certmgr-ca = You have certificates on file that identify these certificate authorities
+
+certmgr-edit-ca-cert =
+ .title = Edit CA certificate trust settings
+ .style = width: 48em;
+
+certmgr-edit-cert-edit-trust = Edit trust settings:
+
+certmgr-edit-cert-trust-ssl =
+ .label = This certificate can identify websites.
+
+certmgr-edit-cert-trust-email =
+ .label = This certificate can identify mail users.
+
+certmgr-delete-cert =
+ .title = Delete Certificate
+ .style = width: 48em; height: 24em;
+
+certmgr-cert-host =
+ .label = Host
+
+certmgr-cert-name =
+ .label = Certificate Name
+
+certmgr-cert-server =
+ .label = Server
+
+certmgr-override-lifetime =
+ .label = Lifetime
+
+certmgr-token-name =
+ .label = Security Device
+
+certmgr-begins-label =
+ .label = Begins On
+
+certmgr-expires-label =
+ .label = Expires On
+
+certmgr-email =
+ .label = E-Mail Address
+
+certmgr-serial =
+ .label = Serial Number
+
+certmgr-view =
+ .label = View…
+ .accesskey = V
+
+certmgr-edit =
+ .label = Edit Trust…
+ .accesskey = E
+
+certmgr-export =
+ .label = Export…
+ .accesskey = x
+
+certmgr-delete =
+ .label = Delete…
+ .accesskey = D
+
+certmgr-delete-builtin =
+ .label = Delete or Distrust…
+ .accesskey = D
+
+certmgr-backup =
+ .label = Backup…
+ .accesskey = B
+
+certmgr-backup-all =
+ .label = Backup All…
+ .accesskey = k
+
+certmgr-restore =
+ .label = Import…
+ .accesskey = m
+
+certmgr-add-exception =
+ .label = Add Exception…
+ .accesskey = x
+
+exception-mgr =
+ .title = Add Security Exception
+
+exception-mgr-extra-button =
+ .label = Confirm Security Exception
+ .accesskey = C
+
+exception-mgr-supplemental-warning = Legitimate banks, stores, and other public sites will not ask you to do this.
+
+exception-mgr-cert-location-url =
+ .value = Location:
+
+exception-mgr-cert-location-download =
+ .label = Get Certificate
+ .accesskey = G
+
+exception-mgr-cert-status-view-cert =
+ .label = View…
+ .accesskey = V
+
+exception-mgr-permanent =
+ .label = Permanently store this exception
+ .accesskey = P
+
+pk11-bad-password = The password entered was incorrect.
+pkcs12-decode-err = Failed to decode the file. Either it is not in PKCS #12 format, has been corrupted, or the password you entered was incorrect.
+pkcs12-unknown-err-restore = Failed to restore the PKCS #12 file for unknown reasons.
+pkcs12-unknown-err-backup = Failed to create the PKCS #12 backup file for unknown reasons.
+pkcs12-unknown-err = The PKCS #12 operation failed for unknown reasons.
+pkcs12-info-no-smartcard-backup = It is not possible to back up certificates from a hardware security device such as a smart card.
+pkcs12-dup-data = The certificate and private key already exist on the security device.
+
+## PKCS#12 file dialogs
+
+choose-p12-backup-file-dialog = File Name to Backup
+file-browse-pkcs12-spec = PKCS12 Files
+choose-p12-restore-file-dialog = Certificate File to Import
+
+## Import certificate(s) file dialog
+
+file-browse-certificate-spec = Certificate Files
+import-ca-certs-prompt = Select File containing CA certificate(s) to import
+import-email-cert-prompt = Select File containing somebody’s Email certificate to import
+
+## For editing certificates trust
+
+# Variables:
+# $certName: the name of certificate
+edit-trust-ca = The certificate “{ $certName }” represents a Certificate Authority.
+
+## For Deleting Certificates
+
+delete-user-cert-title =
+ .title = Delete your Certificates
+delete-user-cert-confirm = Are you sure you want to delete these certificates?
+delete-user-cert-impact = If you delete one of your own certificates, you can no longer use it to identify yourself.
+
+
+delete-ssl-override-title =
+ .title = Delete Server Certificate Exception
+delete-ssl-override-confirm = Are you sure you want to delete this server exception?
+delete-ssl-override-impact = If you delete a server exception, you restore the usual security checks for that server and require it uses a valid certificate.
+
+delete-ca-cert-title =
+ .title = Delete or Distrust CA Certificates
+delete-ca-cert-confirm = You have requested to delete these CA certificates. For built-in certificates all trust will be removed, which has the same effect. Are you sure you want to delete or distrust?
+delete-ca-cert-impact = If you delete or distrust a certificate authority (CA) certificate, this application will no longer trust any certificates issued by that CA.
+
+
+delete-email-cert-title =
+ .title = Delete E-Mail Certificates
+delete-email-cert-confirm = Are you sure you want to delete these people’s e-mail certificates?
+delete-email-cert-impact = If you delete a person’s e-mail certificate, you will no longer be able to send encrypted e-mail to that person.
+
+# Used for semi-uniquely representing a cert.
+#
+# Variables:
+# $serialNumber : the serial number of the cert in AA:BB:CC hex format.
+cert-with-serial =
+ .value = Certificate with serial number: { $serialNumber }
+
+# Used to indicate that the user chose not to send a client authentication certificate to a server that requested one in a TLS handshake.
+send-no-client-certificate = Send no client certificate
+
+# Used when no cert is stored for an override
+no-cert-stored-for-override = (Not Stored)
+
+## Used to show whether an override is temporary or permanent
+
+permanent-override = Permanent
+temporary-override = Temporary
+
+## Add Security Exception dialog
+
+add-exception-branded-warning = You are about to override how { -brand-short-name } identifies this site.
+add-exception-invalid-header = This site attempts to identify itself with invalid information.
+add-exception-domain-mismatch-short = Wrong Site
+add-exception-domain-mismatch-long = The certificate belongs to a different site, which could mean that someone is trying to impersonate this site.
+add-exception-expired-short = Outdated Information
+add-exception-expired-long = The certificate is not currently valid. It may have been stolen or lost, and could be used by someone to impersonate this site.
+add-exception-unverified-or-bad-signature-short = Unknown Identity
+add-exception-unverified-or-bad-signature-long = The certificate is not trusted because it hasn’t been verified as issued by a trusted authority using a secure signature.
+add-exception-valid-short = Valid Certificate
+add-exception-valid-long = This site provides valid, verified identification. There is no need to add an exception.
+add-exception-checking-short = Checking Information
+add-exception-checking-long = Attempting to identify this site…
+add-exception-no-cert-short = No Information Available
+add-exception-no-cert-long = Unable to obtain identification status for this site.
+
+## Certificate export "Save as" and error dialogs
+
+save-cert-as = Save Certificate To File
+cert-format-base64 = X.509 Certificate (PEM)
+cert-format-base64-chain = X.509 Certificate with chain (PEM)
+cert-format-der = X.509 Certificate (DER)
+cert-format-pkcs7 = X.509 Certificate (PKCS#7)
+cert-format-pkcs7-chain = X.509 Certificate with chain (PKCS#7)
+write-file-failure = File Error
diff --git a/third_party/rust/fluent-testing/resources/toolkit/en-US/security/certificates/deviceManager.ftl b/third_party/rust/fluent-testing/resources/toolkit/en-US/security/certificates/deviceManager.ftl
new file mode 100644
index 0000000000..333c3d6143
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/en-US/security/certificates/deviceManager.ftl
@@ -0,0 +1,130 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## Strings used for device manager
+devmgr =
+ .title = Device Manager
+ .style = width: 67em; height: 32em;
+
+devmgr-devlist =
+ .label = Security Modules and Devices
+
+devmgr-header-details =
+ .label = Details
+
+devmgr-header-value =
+ .label = Value
+
+devmgr-button-login =
+ .label = Log In
+ .accesskey = n
+
+devmgr-button-logout =
+ .label = Log Out
+ .accesskey = O
+
+devmgr-button-changepw =
+ .label = Change Password
+ .accesskey = P
+
+devmgr-button-load =
+ .label = Load
+ .accesskey = L
+
+devmgr-button-unload =
+ .label = Unload
+ .accesskey = U
+
+devmgr-button-enable-fips =
+ .label = Enable FIPS
+ .accesskey = F
+
+devmgr-button-disable-fips =
+ .label = Disable FIPS
+ .accesskey = F
+
+## Strings used for load device
+load-device =
+ .title = Load PKCS#11 Device Driver
+
+load-device-info = Enter the information for the module you want to add.
+
+load-device-modname =
+ .value = Module Name
+ .accesskey = M
+
+load-device-modname-default =
+ .value = New PKCS#11 Module
+
+load-device-filename =
+ .value = Module filename
+ .accesskey = f
+
+load-device-browse =
+ .label = Browse…
+ .accesskey = B
+
+## Token Manager
+
+devinfo-status =
+ .label = Status
+
+devinfo-status-disabled =
+ .label = Disabled
+
+devinfo-status-not-present =
+ .label = Not Present
+
+devinfo-status-uninitialized =
+ .label = Uninitialized
+
+devinfo-status-not-logged-in =
+ .label = Not Logged In
+
+devinfo-status-logged-in =
+ .label = Logged In
+
+devinfo-status-ready =
+ .label = Ready
+
+devinfo-desc =
+ .label = Description
+
+devinfo-man-id =
+ .label = Manufacturer
+
+devinfo-hwversion =
+ .label = HW Version
+devinfo-fwversion =
+ .label = FW Version
+
+devinfo-modname =
+ .label = Module
+
+devinfo-modpath =
+ .label = Path
+
+login-failed = Failed to Login
+
+devinfo-label =
+ .label = Label
+
+devinfo-serialnum =
+ .label = Serial Number
+
+fips-nonempty-primary-password-required = FIPS mode requires that you have a Primary Password set for each security device. Please set the password before trying to enable FIPS mode.
+unable-to-toggle-fips = Unable to change the FIPS mode for the security device. It is recommended that you exit and restart this application.
+load-pk11-module-file-picker-title = Choose a PKCS#11 device driver to load
+
+# Load Module Dialog
+load-module-help-empty-module-name =
+ .value = The module name cannot be empty.
+
+# Do not translate 'Root Certs'
+load-module-help-root-certs-module-name =
+ .value = ‘Root Certs‘ is reserved and cannot be used as the module name.
+
+add-module-failure = Unable to add module
+del-module-warning = Are you sure you want to delete this security module?
+del-module-error = Unable to delete module
diff --git a/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/featuregates/features.ftl b/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/featuregates/features.ftl
new file mode 100644
index 0000000000..f1ba75be14
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/featuregates/features.ftl
@@ -0,0 +1,124 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-css-masonry2 =
+ .label = CSS: Masonry Layout
+experimental-features-css-masonry-description = Enables support for the experimental CSS Masonry Layout feature. See the <a data-l10n-name="explainer">explainer</a> for a high level description of the feature. To provide feedback, please comment in <a data-l10n-name="w3c-issue">this GitHub issue</a> or <a data-l10n-name="bug">this bug</a>.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-web-gpu2 =
+ .label = Web API: WebGPU
+experimental-features-web-gpu-description2 = This new API provides low-level support for performing computation and graphics rendering using the <a data-l10n-name="wikipedia">Graphics Processing Unit (GPU)</a> of the user’s device or computer. The <a data-l10n-name="spec">specification</a> is still a work-in-progress. See <a data-l10n-name="bugzilla">bug 1602129</a> for more details.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-media-avif =
+ .label = Media: AVIF
+experimental-features-media-avif-description = With this feature enabled, { -brand-short-name } supports the AV1 Image File (AVIF) format. This is a still image file format that leverages the capabilities of the AV1 video compression algorithms to reduce image size. See <a data-l10n-name="bugzilla">bug 1443863</a> for more details.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-web-api-inputmode =
+ .label = Web API: inputmode
+# "inputmode" and "contenteditable" are technical terms and shouldn't be translated.
+experimental-features-web-api-inputmode-description = Our implementation of the <a data-l10n-name="mdn-inputmode">inputmode</a> global attribute has been updated as per <a data-l10n-name="whatwg">the WHATWG specification</a>, but we still need to make other changes too, like making it available on contenteditable content. See <a data-l10n-name="bugzilla">bug 1205133</a> for more details.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-web-api-link-preload =
+ .label = Web API: <link rel="preload">
+# Do not translate "rel", "preload" or "link" here, as they are all HTML spec
+# values that do not get translated.
+experimental-features-web-api-link-preload-description = The <a data-l10n-name="rel">rel</a> attribute with value <code>"preload"</code> on a <a data-l10n-name="link">&lt;link&gt;</a> element is intended to help provide performance gains by letting you download resources earlier in the page lifecycle, ensuring that they’re available earlier and are less likely to block page rendering. Read <a data-l10n-name="readmore">“Preloading content with <code>rel="preload"</code>”</a> or see <a data-l10n-name="bugzilla">bug 1583604</a> for more details.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-css-focus-visible =
+ .label = CSS: Pseudo-class: :focus-visible
+experimental-features-css-focus-visible-description = Allows focus styles to be applied to elements like buttons and form controls, only when they are focused using the keyboard (e.g. when tabbing between elements), and not when they are focused using a mouse or other pointing device. See <a data-l10n-name="bugzilla">bug 1617600</a> for more details.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-web-api-beforeinput =
+ .label = Web API: beforeinput Event
+# The terms "beforeinput", "input", "textarea", and "contenteditable" are technical terms
+# and shouldn't be translated.
+experimental-features-web-api-beforeinput-description = The global <a data-l10n-name="mdn-beforeinput">beforeinput</a> event is fired on an <a data-l10n-name="mdn-input">&lt;input&gt;</a> and <a data-l10n-name="mdn-textarea">&lt;textarea&gt;</a> elements, or any element whose <a data-l10n-name="mdn-contenteditable">contenteditable</a> attribute is enabled, immediately before the element’s value changes. The event allows web apps to override the browser’s default behavior for user interaction, e.g., web apps can cancel user input only for specific characters or can modify pasting styled text only with approved styles.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-css-constructable-stylesheets =
+ .label = CSS: Constructable Stylesheets
+experimental-features-css-constructable-stylesheets-description = The addition of a constructor to the <a data-l10n-name="mdn-cssstylesheet">CSSStyleSheet</a> interface as well as a variety of related changes makes it possible to directly create new stylesheets without having to add the sheet to the HTML. This makes it much easier to create reusable stylesheets for use with <a data-l10n-name="mdn-shadowdom">Shadow DOM</a>. See <a data-l10n-name="bugzilla">bug 1520690</a> for more details.
+
+experimental-features-devtools-color-scheme-simulation =
+ .label = Developer Tools: Color Scheme Simulation
+experimental-features-devtools-color-scheme-simulation-description = Adds an option to simulate different color schemes allowing you to test <a data-l10n-name="mdn-preferscolorscheme">@prefers-color-scheme</a> media queries. Using this media query lets your stylesheet respond to whether the user prefers a light or dark user interface. This feature lets you test your code without having to change settings in your browser (or operating system, if the browser follows a system-wide color scheme setting). See <a data-l10n-name="bugzilla1">bug 1550804</a> and <a data-l10n-name="bugzilla2">bug 1137699</a> for more details.
+
+experimental-features-devtools-execution-context-selector =
+ .label = Developer Tools: Execution Context Selector
+experimental-features-devtools-execution-context-selector-description = This feature displays a button on the console’s command line that lets you change the context in which the expression you enter will be executed. See <a data-l10n-name="bugzilla1">bug 1605154</a> and <a data-l10n-name="bugzilla2">bug 1605153</a> for more details.
+
+experimental-features-devtools-compatibility-panel =
+ .label = Developer Tools: Compatibility Panel
+experimental-features-devtools-compatibility-panel-description = A side panel for the Page Inspector that shows you information detailing your app’s cross-browser compatibility status. See <a data-l10n-name="bugzilla">bug 1584464</a> for more details.
+
+# Do not translate 'SameSite', 'Lax' and 'None'.
+experimental-features-cookie-samesite-lax-by-default2 =
+ .label = Cookies: SameSite=Lax by default
+experimental-features-cookie-samesite-lax-by-default2-description = Treat cookies as “SameSite=Lax” by default if no “SameSite” attribute is specified. Developers must opt-in to the current status quo of unrestricted use by explicitly asserting “SameSite=None”.
+
+# Do not translate 'SameSite', 'Lax' and 'None'.
+experimental-features-cookie-samesite-none-requires-secure2 =
+ .label = Cookies: SameSite=None requires secure attribute
+experimental-features-cookie-samesite-none-requires-secure2-description = Cookies with “SameSite=None” attribute require the secure attribute. This feature requires “Cookies: SameSite=Lax by default”.
+
+# about:home should be kept in English, as it refers to the the URI for
+# the internal default home page.
+experimental-features-abouthome-startup-cache =
+ .label = about:home startup cache
+experimental-features-abouthome-startup-cache-description = A cache for the initial about:home document that is loaded by default at startup. The purpose of the cache is to improve startup performance.
+
+experimental-features-print-preview-tab-modal =
+ .label = Print Preview Redesign
+experimental-features-print-preview-tab-modal-description = Introduces the redesigned print preview and makes print preview available on macOS. This potentially introduces breakage and does not include all print-related settings. To access all print-related settings, select “Print using the system dialog…” from within the Print panel.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-cookie-samesite-schemeful =
+ .label = Cookies: Schemeful SameSite
+experimental-features-cookie-samesite-schemeful-description = Treat cookies from the same domain, but with different schemes (e.g. http://example.com and https://example.com) as cross-site instead of same-site. Improves security, but potentially introduces breakage.
+
+# "Service Worker" is an API name and is usually not translated.
+experimental-features-devtools-serviceworker-debugger-support =
+ .label = Developer Tools: Service Worker debugging
+# "Service Worker" is an API name and is usually not translated.
+experimental-features-devtools-serviceworker-debugger-support-description = Enables experimental support for Service Workers in the Debugger panel. This feature may slow the Developer Tools down and increase memory consumption.
+
+# WebRTC global mute toggle controls
+experimental-features-webrtc-global-mute-toggles =
+ .label = WebRTC Global Mute Toggles
+experimental-features-webrtc-global-mute-toggles-description = Add controls to the WebRTC global sharing indicator that allow users to globally mute their microphone and camera feeds.
+
+# JS JIT Warp project
+experimental-features-js-warp =
+ .label = JavaScript JIT: Warp
+experimental-features-js-warp-description = Enable Warp, a project to improve JavaScript performance and memory usage.
+
+# Fission is the name of the feature and should not be translated.
+experimental-features-fission =
+ .label = Fission (Site Isolation)
+experimental-features-fission-description = Fission (site isolation) is an experimental feature in { -brand-short-name } to provide an additional layer of defense against security bugs. By isolating each site into a separate process, Fission makes it harder for malicious websites to get access to information from other pages you are visiting. This is a major architectural change in { -brand-short-name } and we appreciate you testing and reporting any issues you might encounter. For more details, see <a data-l10n-name="wiki">the wiki</a>.
+
+# Support for having multiple Picture-in-Picture windows open simultaneously
+experimental-features-multi-pip =
+ .label = Multiple Picture-in-Picture Support
+experimental-features-multi-pip-description = Experimental support for allowing multiple Picture-in-Picture windows to be open at the same time.
+
+experimental-features-http3 =
+ .label = HTTP/3 protocol
+experimental-features-http3-description = Experimental support for the HTTP/3 protocol.
diff --git a/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/global/textActions.ftl b/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/global/textActions.ftl
new file mode 100644
index 0000000000..c0a6637206
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/global/textActions.ftl
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+text-action-undo =
+ .label = Undo
+ .accesskey = U
+
+text-action-undo-shortcut =
+ .key = Z
+
+text-action-redo =
+ .label = Redo
+ .accesskey = R
+
+text-action-redo-shortcut =
+ .key = Y
+
+text-action-cut =
+ .label = Cut
+ .accesskey = t
+
+text-action-cut-shortcut =
+ .key = X
+
+text-action-copy =
+ .label = Copy
+ .accesskey = C
+
+text-action-copy-shortcut =
+ .key = C
+
+text-action-paste =
+ .label = Paste
+ .accesskey = P
+
+text-action-paste-shortcut =
+ .key = V
+
+text-action-delete =
+ .label = Delete
+ .accesskey = D
+
+text-action-select-all =
+ .label = Select All
+ .accesskey = A
+
+text-action-select-all-shortcut =
+ .key = A
diff --git a/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/printing/printUI.ftl b/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/printing/printUI.ftl
new file mode 100644
index 0000000000..eedca87354
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/printing/printUI.ftl
@@ -0,0 +1,120 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+printui-title = Print
+# Dialog title to prompt the user for a filename to save print to PDF.
+printui-save-to-pdf-title = Save As
+
+# Variables
+# $sheetCount (integer) - Number of paper sheets
+printui-sheets-count =
+ { $sheetCount ->
+ [one] { $sheetCount } sheet of paper
+ *[other] { $sheetCount } sheets of paper
+ }
+
+printui-page-range-all = All
+printui-page-range-custom = Custom
+printui-page-range-label = Pages
+printui-page-range-picker =
+ .aria-label = Pick page range
+printui-page-custom-range-input =
+ .aria-label = Enter custom page range
+ .placeholder = e.g. 2-6, 9, 12-16
+
+# Section title for the number of copies to print
+printui-copies-label = Copies
+
+printui-orientation = Orientation
+printui-landscape = Landscape
+printui-portrait = Portrait
+
+# Section title for the printer or destination device to target
+printui-destination-label = Destination
+printui-destination-pdf-label = Save to PDF
+
+printui-more-settings = More settings
+printui-less-settings = Fewer settings
+
+printui-paper-size-label = Paper size
+
+# Section title (noun) for the print scaling options
+printui-scale = Scale
+printui-scale-fit-to-page-width = Fit to page width
+# Label for input control where user can set the scale percentage
+printui-scale-pcent = Scale
+
+# Section title (noun) for the two-sided print options
+printui-two-sided-printing = Two-sided printing
+printui-duplex-checkbox = Print on both sides
+
+# Section title for miscellaneous print options
+printui-options = Options
+printui-headers-footers-checkbox = Print headers and footers
+printui-backgrounds-checkbox = Print backgrounds
+printui-selection-checkbox = Print selection only
+
+printui-color-mode-label = Color mode
+printui-color-mode-color = Color
+printui-color-mode-bw = Black and white
+
+printui-margins = Margins
+printui-margins-default = Default
+printui-margins-min = Minimum
+printui-margins-none = None
+printui-margins-custom-inches = Custom (inches)
+printui-margins-custom-top = Top
+printui-margins-custom-top-inches = Top (inches)
+printui-margins-custom-bottom = Bottom
+printui-margins-custom-bottom-inches = Bottom (inches)
+printui-margins-custom-left = Left
+printui-margins-custom-left-inches = Left (inches)
+printui-margins-custom-right = Right
+printui-margins-custom-right-inches = Right (inches)
+
+printui-system-dialog-link = Print using the system dialog…
+
+printui-primary-button = Print
+printui-primary-button-save = Save
+printui-cancel-button = Cancel
+printui-close-button = Close
+
+printui-loading = Preparing Preview
+
+# Reported by screen readers and other accessibility tools to indicate that
+# the print preview has focus.
+printui-preview-label =
+ .aria-label = Print Preview
+
+printui-pages-per-sheet = Pages per sheet
+
+# This is shown next to the Print button with an indefinite loading spinner
+# when the user prints a page and it is being sent to the printer.
+printui-print-progress-indicator = Printing…
+
+## Paper sizes that may be supported by the Save to PDF destination:
+
+printui-paper-a5 = A5
+printui-paper-a4 = A4
+printui-paper-a3 = A3
+printui-paper-a2 = A2
+printui-paper-a1 = A1
+printui-paper-a0 = A0
+printui-paper-b5 = B5
+printui-paper-b4 = B4
+printui-paper-jis-b5 = JIS-B5
+printui-paper-jis-b4 = JIS-B4
+printui-paper-letter = US Letter
+printui-paper-legal = US Legal
+printui-paper-tabloid = Tabloid
+
+## Error messages shown when a user has an invalid input
+
+printui-error-invalid-scale = Scale must be a number between 10 and 200.
+printui-error-invalid-margin = Please enter a valid margin for the selected paper size.
+
+# Variables
+# $numPages (integer) - Number of pages
+printui-error-invalid-range = Range must be a number between 1 and { $numPages }.
+printui-error-invalid-start-overflow = The “from” page number must be smaller than the “to” page number.
diff --git a/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/updates/history.ftl b/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/updates/history.ftl
new file mode 100644
index 0000000000..cfcdab5367
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/en-US/toolkit/updates/history.ftl
@@ -0,0 +1,27 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+history-title = Update History
+history-intro = The following updates have been installed
+
+close-button-label =
+ .buttonlabelcancel = Close
+ .title = Update History
+
+no-updates-label = No updates installed yet
+name-header = Update Name
+date-header = Install Date
+type-header = Type
+state-header = State
+
+# Used to display update history
+#
+# Variables:
+# $name (String): name of the update
+# $buildID (String): build identifier from the local updates.xml
+update-full-build-name = { $name } ({ $buildID })
+
+update-details = Details
+update-installed-on = Installed on: { $date }
+update-status = Status: { $status }
diff --git a/third_party/rust/fluent-testing/resources/toolkit/pl/security/certificates/certManager.ftl b/third_party/rust/fluent-testing/resources/toolkit/pl/security/certificates/certManager.ftl
new file mode 100644
index 0000000000..92bc0bd27d
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/pl/security/certificates/certManager.ftl
@@ -0,0 +1,251 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+certmgr-title =
+ .title = Menedżer certyfikatów
+certmgr-tab-mine =
+ .label = Użytkownik
+certmgr-tab-remembered =
+ .label = Decyzje uwierzytelniania
+certmgr-tab-people =
+ .label = Osoby
+certmgr-tab-servers =
+ .label = Serwery
+certmgr-tab-ca =
+ .label = Organy certyfikacji
+certmgr-mine = Masz identyfikujące certyfikaty z następujących organizacji:
+certmgr-remembered = Następujące certyfikaty są używane do identyfikowania użytkownika przez witryny:
+certmgr-people = Masz certyfikaty, które identyfikują następujące osoby:
+certmgr-servers = Masz certyfikaty, które identyfikują następujące serwery:
+certmgr-server = Następujące wpisy identyfikują wyjątki błędów certyfikatów serwera:
+certmgr-ca = Masz certyfikaty, które identyfikują następujące organy certyfikacji:
+certmgr-detail-general-tab-title =
+ .label = Ogólne
+ .accesskey = O
+certmgr-detail-pretty-print-tab-title =
+ .label = Szczegóły
+ .accesskey = S
+certmgr-pending-label =
+ .value = Trwa weryfikacja certyfikatu…
+certmgr-subject-label = Wystawiony dla
+certmgr-issuer-label = Wystawiony przez
+certmgr-period-of-validity = Okres ważności
+certmgr-fingerprints = Odciski
+certmgr-cert-detail =
+ .title = Szczegóły certyfikatu
+ .buttonlabelaccept = Zamknij
+ .buttonaccesskeyaccept = Z
+certmgr-cert-detail-commonname = Nazwa pospolita (CN)
+certmgr-cert-detail-org = Organizacja (O)
+certmgr-cert-detail-orgunit = Jednostka organizacyjna (OU)
+certmgr-cert-detail-serial-number = Numer seryjny
+certmgr-cert-detail-sha-256-fingerprint = Odcisk SHA-256
+certmgr-cert-detail-sha-1-fingerprint = Odcisk SHA1
+certmgr-edit-ca-cert =
+ .title = Edycja ustawień zaufania certyfikatu CA
+ .style = width: 40em;
+certmgr-edit-cert-edit-trust = Ustawienia zaufania:
+certmgr-edit-cert-trust-ssl =
+ .label = certyfikat identyfikuje witryny
+certmgr-edit-cert-trust-email =
+ .label = certyfikat identyfikuje użytkowników poczty
+certmgr-delete-cert =
+ .title = Usuń certyfikat
+ .style = width: 48em; height: 24em;
+certmgr-cert-host =
+ .label = Host
+certmgr-cert-name =
+ .label = Nazwa certyfikatu
+certmgr-cert-server =
+ .label = Serwer
+certmgr-override-lifetime =
+ .label = Czas życia
+certmgr-token-name =
+ .label = Urządzenie zabezpieczające
+certmgr-begins-on = Ważny od dnia
+certmgr-begins-label =
+ .label = Ważny od dnia:
+certmgr-expires-on = Wygasa dnia
+certmgr-expires-label =
+ .label = Wygasa dnia
+certmgr-email =
+ .label = Adres e-mail
+certmgr-serial =
+ .label = Numer seryjny
+certmgr-view =
+ .label = Wyświetl…
+ .accesskey = W
+certmgr-edit =
+ .label = Edytuj ustawienia zaufania…
+ .accesskey = d
+certmgr-export =
+ .label = Eksportuj…
+ .accesskey = E
+certmgr-delete =
+ .label = Usuń…
+ .accesskey = U
+certmgr-delete-builtin =
+ .label = Usuń lub przestań ufać…
+ .accesskey = U
+certmgr-backup =
+ .label = Kopia zapasowa…
+ .accesskey = K
+certmgr-backup-all =
+ .label = Kopia zapasowa wszystkich…
+ .accesskey = o
+certmgr-restore =
+ .label = Importuj…
+ .accesskey = m
+certmgr-details =
+ .value = Pola certyfikatu
+ .accesskey = P
+certmgr-fields =
+ .value = Wartość pola
+ .accesskey = a
+certmgr-hierarchy =
+ .value = Hierarchia certyfikatu
+ .accesskey = H
+certmgr-add-exception =
+ .label = Dodaj wyjątek…
+ .accesskey = o
+exception-mgr =
+ .title = Dodanie wyjątku bezpieczeństwa
+exception-mgr-extra-button =
+ .label = Potwierdź wyjątek bezpieczeństwa
+ .accesskey = P
+exception-mgr-supplemental-warning = Godne zaufania witryny, banki i inne witryny publiczne nie powinny tego żądać.
+exception-mgr-cert-location-url =
+ .value = Adres:
+exception-mgr-cert-location-download =
+ .label = Pobierz certyfikat
+ .accesskey = b
+exception-mgr-cert-status-view-cert =
+ .label = Wyświetl…
+ .accesskey = W
+exception-mgr-permanent =
+ .label = Zachowaj ten wyjątek na stałe
+ .accesskey = Z
+pk11-bad-password = Wprowadzone hasło tokenu jest nieprawidłowe.
+pkcs12-decode-err = Dekodowanie pliku się nie powiodło. Plik nie jest w formacie PKCS #12, jest uszkodzony lub wprowadzone hasło jest nieprawidłowe.
+pkcs12-unknown-err-restore = Nie udało się odtworzyć kopii bezpieczeństwa PKCS #12 z nieznanych powodów.
+pkcs12-unknown-err-backup = Nie udało się utworzyć kopii bezpieczeństwa PKCS #12 z nieznanych powodów.
+pkcs12-unknown-err = Operacja PKCS #12 się nie powiodła z nieznanych powodów.
+pkcs12-info-no-smartcard-backup = Zachowanie kopii certyfikatu zapisanego w urządzeniu zabezpieczającym, jak np. inteligentna karta, jest niemożliwe.
+pkcs12-dup-data = To urządzenie zabezpieczające ma już certyfikat oraz klucz prywatny.
+
+## PKCS#12 file dialogs
+
+choose-p12-backup-file-dialog = Nazwa pliku kopii zapasowej
+file-browse-pkcs12-spec = Pliki PKCS12
+choose-p12-restore-file-dialog = Plik certyfikatu do zaimportowania
+
+## Import certificate(s) file dialog
+
+file-browse-certificate-spec = Pliki certyfikatów
+import-ca-certs-prompt = Wybierz plik zawierający certyfikat(y) CA do zaimportowania
+import-email-cert-prompt = Wybierz plik zawierający certyfikat e-mail innej osoby do zaimportowania
+
+## For editing certificates trust
+
+# Variables:
+# $certName: the name of certificate
+edit-trust-ca = Certyfikat „{ $certName }” reprezentuje organ certyfikacji.
+
+## For Deleting Certificates
+
+delete-user-cert-title =
+ .title = Usuń własne certyfikaty
+delete-user-cert-confirm = Czy na pewno usunąć wybrane certyfikaty?
+delete-user-cert-impact = Usunięcie jednego z certyfikatów użytkownika spowoduje, że ponowne wykorzystanie go do potwierdzenia tożsamości użytkownika będzie niemożliwe.
+delete-ssl-cert-title =
+ .title = Usuń wyjątki dotyczące certyfikatów serwerów
+delete-ssl-cert-confirm = Czy na pewno usunąć te wyjątki dotyczące certyfikatów serwerów?
+delete-ssl-cert-impact = Jeżeli wyjątek dotyczący certyfikatu serwera zostanie usunięty, przywrócone zostaną zwykłe procedury bezpieczeństwa dla tego serwera, w tym wymóg stosowania przez niego poprawnego certyfikatu.
+delete-ssl-override-title =
+ .title = Usuń wyjątek dotyczący certyfikatu serwera
+delete-ssl-override-confirm = Czy na pewno usunąć ten wyjątek dotyczący certyfikatu serwera?
+delete-ssl-override-impact = Jeżeli wyjątek dotyczący certyfikatu serwera zostanie usunięty, przywrócone zostaną zwykłe procedury bezpieczeństwa dla tego serwera, w tym wymóg stosowania przez niego poprawnego certyfikatu.
+delete-ca-cert-title =
+ .title = Usuń lub przestań ufać certyfikatom CA
+delete-ca-cert-confirm = Zażądano usunięcia certyfikatów CA. Certyfikaty wbudowane przestaną być zaufane, co ma taki sam skutek. Czy na pewno usunąć lub przestać ufać wybranym certyfikatom CA?
+delete-ca-cert-impact = Jeżeli certyfikat organu certyfikacji (CA) zostanie usunięty lub przestanie być zaufany, certyfikaty wydane przez ten CA nie będą uznawane przez program za zaufane.
+delete-email-cert-title =
+ .title = Usuń certyfikaty e-mail
+delete-email-cert-confirm = Czy na pewno usunąć certyfikaty e-mail wybranych osób?
+delete-email-cert-impact = Jeśli certyfikat e-mail danej osoby zostanie usunięty, nie będzie można do niej wysłać zaszyfrowanych wiadomości.
+# Used for semi-uniquely representing a cert.
+#
+# Variables:
+# $serialNumber : the serial number of the cert in AA:BB:CC hex format.
+cert-with-serial =
+ .value = Certyfikat o numerze seryjnym { $serialNumber }
+
+## Cert Viewer
+
+# Title used for the Certificate Viewer.
+#
+# Variables:
+# $certificate : a string representative of the certificate being viewed.
+cert-viewer-title =
+ .title = Podgląd certyfikatu: „{ $certName }”
+not-present =
+ .value = <Nie jest częścią certyfikatu>
+# Cert verification
+cert-verified = Niniejszy certyfikat został zweryfikowany do wykorzystania przez:
+# Add usage
+verify-ssl-client =
+ .value = Certyfikat SSL klienta
+verify-ssl-server =
+ .value = Certyfikat SSL serwera
+verify-ssl-ca =
+ .value = Organ certyfikacji SSL
+verify-email-signer =
+ .value = Certyfikat osoby podpisującej wiadomość
+verify-email-recip =
+ .value = Certyfikat adresata wiadomości
+# Cert verification
+cert-not-verified-cert-revoked = Nie można sprawdzić tego certyfikatu, ponieważ został on unieważniony.
+cert-not-verified-cert-expired = Nie można sprawdzić tego certyfikatu, ponieważ stracił on ważność.
+cert-not-verified-cert-not-trusted = Nie można sprawdzić tego certyfikatu, ponieważ nie ma go na liście zaufanych.
+cert-not-verified-issuer-not-trusted = Nie można sprawdzić tego certyfikatu: wystawcy nie ma na liście zaufanych.
+cert-not-verified-issuer-unknown = Nie można sprawdzić tego certyfikatu, ponieważ jego wystawca jest nieznany.
+cert-not-verified-ca-invalid = Nie można sprawdzić tego certyfikatu, ponieważ jego CA jest nieprawidłowy.
+cert-not-verified_algorithm-disabled = Nie można sprawdzić tego certyfikatu, ponieważ został podpisany algorytmem, który został zablokowany, ponieważ nie jest bezpieczny.
+cert-not-verified-unknown = Nie można sprawdzić tego certyfikatu z nieznanych przyczyn.
+# Used to indicate that the user chose not to send a client authentication certificate to a server that requested one in a TLS handshake.
+send-no-client-certificate = Nie wysyłaj certyfikatu klienta
+# Used when no cert is stored for an override
+no-cert-stored-for-override = (nieprzechowywany)
+
+## Used to show whether an override is temporary or permanent
+
+permanent-override = Na stałe
+temporary-override = Tymczasowy
+
+## Add Security Exception dialog
+
+add-exception-branded-warning = Próba zmiany sposobu, w jaki { -brand-short-name } identyfikuje tę witrynę.
+add-exception-invalid-header = Ta witryna próbuje zidentyfikować się przy użyciu nieprawidłowych informacji.
+add-exception-domain-mismatch-short = Niewłaściwa witryna
+add-exception-domain-mismatch-long = Certyfikat należy do innej witryny, co może wskazywać na podszywanie się pod stronę.
+add-exception-expired-short = Informacje nieaktualne
+add-exception-expired-long = Certyfikat nie jest obecnie aktualny. Mógł zostać zagubiony lub skradziony i może być wykorzystywany do podszywania się pod stronę.
+add-exception-unverified-or-bad-signature-short = Tożsamość nieznana
+add-exception-unverified-or-bad-signature-long = Certyfikat nie jest zaufany, ponieważ nie został zweryfikowany jako wystawiony przez zaufany organ przy użyciu bezpiecznego podpisu.
+add-exception-valid-short = Certyfikat prawidłowy
+add-exception-valid-long = Ta witryna dostarcza prawidłowych i zweryfikowanych informacji identyfikujących. Nie ma potrzeby dodawania wyjątku.
+add-exception-checking-short = Sprawdzanie informacji
+add-exception-checking-long = Próba identyfikacji witryny…
+add-exception-no-cert-short = Brak dostępnych informacji
+add-exception-no-cert-long = Nie udało się pobrać stanu identyfikacji tej witryny.
+
+## Certificate export "Save as" and error dialogs
+
+save-cert-as = Zapisz certyfikat do pliku
+cert-format-base64 = Certyfikat X.509 (PEM)
+cert-format-base64-chain = Certyfikat X.509 z łańcuchem (PEM)
+cert-format-der = Certyfikat X.509 (DER)
+cert-format-pkcs7 = Certyfikat X.509 (PKCS#7)
+cert-format-pkcs7-chain = Certyfikat X.509 z łańcuchem (PKCS#7)
+write-file-failure = Błąd zapisu pliku
diff --git a/third_party/rust/fluent-testing/resources/toolkit/pl/security/certificates/deviceManager.ftl b/third_party/rust/fluent-testing/resources/toolkit/pl/security/certificates/deviceManager.ftl
new file mode 100644
index 0000000000..f029112c08
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/pl/security/certificates/deviceManager.ftl
@@ -0,0 +1,135 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+## Strings used for device manager
+
+devmgr =
+ .title = Menedżer urządzeń
+ .style = width: 67em; height: 32em;
+
+devmgr-devlist =
+ .label = Urządzenia i moduły zabezpieczające
+
+devmgr-header-details =
+ .label = Szczegóły
+
+devmgr-header-value =
+ .label = Wartość
+
+devmgr-button-login =
+ .label = Zaloguj
+ .accesskey = Z
+
+devmgr-button-logout =
+ .label = Wyloguj
+ .accesskey = W
+
+devmgr-button-changepw =
+ .label = Zmień hasło
+ .accesskey = h
+
+devmgr-button-load =
+ .label = Wczytaj
+ .accesskey = c
+
+devmgr-button-unload =
+ .label = Usuń z pamięci
+ .accesskey = U
+
+devmgr-button-enable-fips =
+ .label = Włącz FIPS
+ .accesskey = F
+
+devmgr-button-disable-fips =
+ .label = Wyłącz FIPS
+ .accesskey = F
+
+## Strings used for load device
+
+load-device =
+ .title = Wczytaj sterownik urządzenia PKCS#11
+
+load-device-info = Wprowadź informacje dla modułu, który ma zostać dodany.
+
+load-device-modname =
+ .value = Nazwa modułu:
+ .accesskey = m
+
+load-device-modname-default =
+ .value = Nowy moduł PKCS#11
+
+load-device-filename =
+ .value = Nazwa pliku modułu:
+ .accesskey = N
+
+load-device-browse =
+ .label = Przeglądaj…
+ .accesskey = P
+
+## Token Manager
+
+devinfo-status =
+ .label = Stan
+
+devinfo-status-disabled =
+ .label = Wyłączony
+
+devinfo-status-not-present =
+ .label = Nieobecny
+
+devinfo-status-uninitialized =
+ .label = Niezainicjowany
+
+devinfo-status-not-logged-in =
+ .label = Niezalogowany
+
+devinfo-status-logged-in =
+ .label = Zalogowany
+
+devinfo-status-ready =
+ .label = Gotowy
+
+devinfo-desc =
+ .label = Opis
+
+devinfo-man-id =
+ .label = Producent
+
+devinfo-hwversion =
+ .label = Wersja HW
+devinfo-fwversion =
+ .label = Wersja FW
+
+devinfo-modname =
+ .label = Moduł
+
+devinfo-modpath =
+ .label = Ścieżka
+
+login-failed = Nie można się zalogować
+
+devinfo-label =
+ .label = Etykieta
+
+devinfo-serialnum =
+ .label = Numer seryjny
+
+fips-nonempty-password-required = Tryb FIPS wymaga hasła głównego ustawionego dla każdego urządzenia zabezpieczającego. Ustaw hasło przed włączeniem trybu FIPS.
+
+fips-nonempty-primary-password-required = Tryb FIPS wymaga hasła głównego ustawionego dla każdego urządzenia zabezpieczającego. Ustaw hasło przed włączeniem trybu FIPS.
+unable-to-toggle-fips = Nie udało się zmienić trybu FIPS dla urządzenia bezpieczeństwa. Zaleca się zakończenie pracy i ponowne uruchomienie tego programu.
+load-pk11-module-file-picker-title = Wybierz sterownik urządzenia PKCS#11 do wczytania
+
+# Load Module Dialog
+load-module-help-empty-module-name =
+ .value = Nazwa modułu nie może być pusta.
+
+# Do not translate 'Root Certs'
+load-module-help-root-certs-module-name =
+ .value = Nazwa „Root Certs” jest zarezerwowana i nie może zostać użyta jako nazwa modułu.
+
+add-module-failure = Nie można dodać modułu
+del-module-warning = Czy na pewno usunąć wybrany moduł szyfrujący?
+del-module-error = Nie można usunąć modułu
diff --git a/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/featuregates/features.ftl b/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/featuregates/features.ftl
new file mode 100644
index 0000000000..83ec5a422e
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/featuregates/features.ftl
@@ -0,0 +1,100 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-css-masonry2 =
+ .label = CSS: układ typu „Masonry”
+experimental-features-css-masonry-description = Włącza obsługę eksperymentalnego układu CSS typu „Masonry”. Ta <a data-l10n-name="explainer">strona</a> zawiera jego ogólny opis. W <a data-l10n-name="w3c-issue">tym zgłoszeniu w serwisie GitHub</a> lub <a data-l10n-name="bug">tym błędzie</a> można dodać komentarz na jego temat.
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-web-gpu2 =
+ .label = API internetowe: WebGPU
+experimental-features-web-gpu-description2 = To nowe API dostarcza niskopoziomową obsługę wykonywania obliczeń i renderowania grafiki za pomocą <a data-l10n-name="wikipedia">procesora graficznego (GPU)</a> urządzenia lub komputera użytkownika. <a data-l10n-name="spec">Specyfikacja</a> jest nadal w trakcie przygotowywania. <a data-l10n-name="bugzilla">Zgłoszenie 1602129</a> zawiera więcej informacji.
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-media-avif =
+ .label = Multimedia: AVIF
+experimental-features-media-avif-description = Po włączeniu tej funkcji { -brand-short-name } obsługuje format obrazów AV1 (AVIF). Jest to format dla nieruchomych obrazów wykorzystujący możliwości algorytmów kompresji wideo AV1 w celu zmniejszenia rozmiaru pliku. <a data-l10n-name="bugzilla">Zgłoszenie 1443863</a> zawiera więcej informacji.
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-web-api-inputmode =
+ .label = API internetowe: inputmode
+# "inputmode" and "contenteditable" are technical terms and shouldn't be translated.
+experimental-features-web-api-inputmode-description = Nasza implementacja globalnego atrybutu <a data-l10n-name="mdn-inputmode">inputmode</a> została zaktualizowana zgodnie ze <a data-l10n-name="whatwg">specyfikacją WHATWG</a>, ale nadal musimy wprowadzić także inne zmiany, na przykład umożliwić działanie w treściach „contenteditable”. <a data-l10n-name="bugzilla">Zgłoszenie 1205133</a> zawiera więcej informacji.
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-web-api-link-preload =
+ .label = API internetowe: <link rel="preload">
+# Do not translate "rel", "preload" or "link" here, as they are all HTML spec
+# values that do not get translated.
+experimental-features-web-api-link-preload-description = Atrybut <a data-l10n-name="rel">rel</a> o wartości <code>"preload"</code> na elemencie <a data-l10n-name="link">&lt;link&gt;</a> ma na celu pomóc zwiększyć wydajność przez umożliwienie użytkownikowi pobrania zasobów wcześniej w cyklu życia strony, zapewniając, że są one dostępne wcześniej i rzadziej blokują wyświetlanie strony. Artykuł <a data-l10n-name="readmore">„Preloading content with <code>rel="preload"</code>”</a> i <a data-l10n-name="bugzilla">zgłoszenie 1583604</a> zawierają więcej informacji.
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-css-focus-visible =
+ .label = CSS: pseudoklasa „:focus-visible”
+experimental-features-css-focus-visible-description = Umożliwia stosowanie stylów aktywacji na elementach typu przyciski i formularze tylko wtedy, gdy zostały aktywowane za pomocą klawiatury (np. podczas przełączania między elementami klawiszem Tab), a nie gdy zostały aktywowane za pomocą myszy lub innego urządzenia wskazującego. <a data-l10n-name="bugzilla">Zgłoszenie 1617600</a> zawiera więcej informacji.
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-web-api-beforeinput =
+ .label = API internetowe: zdarzenie „beforeinput”
+# The terms "beforeinput", "input", "textarea", and "contenteditable" are technical terms
+# and shouldn't be translated.
+experimental-features-web-api-beforeinput-description = Globalne zdarzenie <a data-l10n-name="mdn-beforeinput">beforeinput</a> jest wywoływane na elementach <a data-l10n-name="mdn-input">&lt;input&gt;</a> i <a data-l10n-name="mdn-textarea">&lt;textarea&gt;</a> lub dowolnym elemencie, którego atrybut <a data-l10n-name="mdn-contenteditable">contenteditable</a> jest włączony, natychmiast przed zmianą wartości elementu. To zdarzenie umożliwia aplikacjom internetowym zastępowanie domyślnego zachowania przeglądarki podczas działań użytkownika, np. aplikacje internetowe mogą anulować wprowadzane dane przez użytkownika tylko dla określonych znaków lub mogą modyfikować wklejanie tekstu ze stylem tylko za pomocą zatwierdzonych stylów.
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-css-constructable-stylesheets =
+ .label = CSS: arkusze stylów za pomocą konstruktora
+experimental-features-css-constructable-stylesheets-description = Dodanie konstruktora do interfejsu <a data-l10n-name="mdn-cssstylesheet">CSSStyleSheet</a>, a także szereg powiązanych zmian umożliwia bezpośrednie tworzenie nowych arkuszy stylów bez konieczności dodawania arkusza do kodu HTML. Znacznie ułatwia to tworzenie arkuszy stylów wielokrotnego użytku do użycia za pomocą <a data-l10n-name="mdn-shadowdom">Shadow DOM</a>. <a data-l10n-name="bugzilla">Zgłoszenie 1520690</a> zawiera więcej informacji.
+experimental-features-devtools-color-scheme-simulation =
+ .label = Narzędzia dla programistów: symulacja schematu kolorów
+experimental-features-devtools-color-scheme-simulation-description = Dodaje opcję symulowania różnych schematów kolorów, umożliwiając testowanie zapytań <a data-l10n-name="mdn-preferscolorscheme">@prefers-color-scheme</a>. Użycie tego zapytania umożliwia arkuszowi stylów reagowanie na to, czy użytkownik preferuje jasny lub ciemny interfejs. Ta funkcja umożliwia testowanie kodu bez konieczności zmiany ustawień w przeglądarce (lub systemie operacyjnym, jeśli przeglądarka używa systemowego ustawienia schematu kolorów). Zgłoszenia <a data-l10n-name="bugzilla1">1550804</a> i <a data-l10n-name="bugzilla2">1137699</a> zawierają więcej informacji.
+experimental-features-devtools-execution-context-selector =
+ .label = Narzędzia dla programistów: wybór kontekstu wykonywania
+experimental-features-devtools-execution-context-selector-description = Ta funkcja wyświetla przycisk w wierszu poleceń konsoli, umożliwiający zmianę kontekstu, w którym wprowadzane wyrażenie będzie wykonywane. Zgłoszenia <a data-l10n-name="bugzilla1">1605154</a> i <a data-l10n-name="bugzilla2">1605153</a> zawierają więcej informacji.
+experimental-features-devtools-compatibility-panel =
+ .label = Narzędzia dla programistów: panel zgodności
+experimental-features-devtools-compatibility-panel-description = Panel boczny inspektora stron, wyświetlający informacje o stanie zgodności aplikacji z różnymi przeglądarkami. <a data-l10n-name="bugzilla">Zgłoszenie 1584464</a> zawiera więcej informacji.
+# Do not translate 'SameSite', 'Lax' and 'None'.
+experimental-features-cookie-samesite-lax-by-default2 =
+ .label = Ciasteczka: „SameSite=Lax” jest domyślne
+experimental-features-cookie-samesite-lax-by-default2-description = Domyślnie traktuje ciasteczka jako „SameSite=Lax”, jeśli nie określono żadnego atrybutu „SameSite”. Deweloperzy muszą wyrazić zgodę na obecne status quo nieograniczonego użytkowania, bezpośrednio ustawiając „SameSite=None”.
+# Do not translate 'SameSite', 'Lax' and 'None'.
+experimental-features-cookie-samesite-none-requires-secure2 =
+ .label = Ciasteczka: „SameSite=None” wymaga atrybutu bezpieczeństwa
+experimental-features-cookie-samesite-none-requires-secure2-description = Ciasteczka z atrybutem „SameSite=None” wymagają atrybutu bezpieczeństwa. Ta funkcja wymaga włączenia „Ciasteczka: »SameSite=Lax« jest domyślne”.
+# about:home should be kept in English, as it refers to the the URI for
+# the internal default home page.
+experimental-features-abouthome-startup-cache =
+ .label = Pamięć podręczna uruchamiania about:home
+experimental-features-abouthome-startup-cache-description = Pamięć podręczna dla początkowego dokumentu about:home, który jest domyślnie wczytywany podczas uruchamiania. Celem tej pamięci podręcznej jest przyspieszenie uruchamiania.
+experimental-features-print-preview-tab-modal =
+ .label = Przeprojektowany podgląd wydruku
+experimental-features-print-preview-tab-modal-description = Włącza przeprojektowany podgląd wydruku i udostępnia go w systemie macOS. Może nie działać i nie zawiera wszystkich ustawień związanych z drukowaniem. Kliknij „Drukuj za pomocą okna systemowego…” w panelu drukowania, aby móc korzystać ze wszystkich ustawień.
+# The title of the experiment should be kept in English as it may be referenced
+# by various online articles and is technical in nature.
+experimental-features-cookie-samesite-schemeful =
+ .label = Ciasteczka: SameSite typu „Schemeful”
+experimental-features-cookie-samesite-schemeful-description = Traktuje ciasteczka z tej samej domeny, ale o różnych protokołach (np. http://example.com i https://example.com) jako ciasteczka między witrynami, zamiast z tej samej witryny. Zwiększa bezpieczeństwo, ale potencjalnie zakłóca działanie witryn.
+# "Service Worker" is an API name and is usually not translated.
+experimental-features-devtools-serviceworker-debugger-support =
+ .label = Narzędzia dla programistów: debugowanie wątków usługowych
+# "Service Worker" is an API name and is usually not translated.
+experimental-features-devtools-serviceworker-debugger-support-description = Włącza eksperymentalną obsługę wątków usługowych w panelu debugera. Ta funkcja może spowolnić narzędzia dla programistów i zwiększyć zużycie pamięci.
+# WebRTC global mute toggle controls
+experimental-features-webrtc-global-mute-toggles =
+ .label = Przełączniki globalnego wyciszania WebRTC
+experimental-features-webrtc-global-mute-toggles-description = Dodaje elementy sterujące do globalnego wskaźnika udostępniania WebRTC umożliwiające użytkownikom globalne wyciszanie transmisji dźwięku z mikrofonu i obrazu z kamery.
+# JS JIT Warp project
+experimental-features-js-warp =
+ .label = JavaScript JIT: Warp
+experimental-features-js-warp-description = Włącza Warp, projekt mający na celu zwiększenie wydajności JavaScriptu i zmniejszenie zużycia pamięci.
+# Fission is the name of the feature and should not be translated.
+experimental-features-fission =
+ .label = Fission (izolacja witryn)
+experimental-features-fission-description = Fission (izolacja witryn) to eksperymentalna funkcja programu { -brand-short-name }, zapewniająca dodatkową warstwę obrony przed błędami zabezpieczeń. Izolując każdą witrynę w oddzielnym procesie, Fission utrudnia złośliwym witrynom dostęp do informacji z pozostałych odwiedzanych stron. To duża zmiana architektury programu { -brand-short-name } i serdecznie dziękujemy za testowanie i zgłaszanie napotkanych błędów. <a data-l10n-name="wiki">Strona wiki</a> zawiera więcej informacji.
+# Support for having multiple Picture-in-Picture windows open simultaneously
+experimental-features-multi-pip =
+ .label = Obsługa wielu okien „Obraz w obrazie”
+experimental-features-multi-pip-description = Eksperymentalna obsługa otwierania wielu okien „Obraz w obrazie” jednocześnie.
diff --git a/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/global/textActions.ftl b/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/global/textActions.ftl
new file mode 100644
index 0000000000..a1e57a491b
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/global/textActions.ftl
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+text-action-undo =
+ .label = Cofnij
+ .accesskey = C
+
+text-action-undo-shortcut =
+ .key = Z
+
+text-action-redo =
+ .label = Przywróć
+ .accesskey = P
+
+text-action-redo-shortcut =
+ .key = Y
+
+text-action-cut =
+ .label = Wytnij
+ .accesskey = W
+
+text-action-cut-shortcut =
+ .key = X
+
+text-action-copy =
+ .label = Kopiuj
+ .accesskey = K
+
+text-action-copy-shortcut =
+ .key = C
+
+text-action-paste =
+ .label = Wklej
+ .accesskey = e
+
+text-action-paste-shortcut =
+ .key = V
+
+text-action-delete =
+ .label = Usuń
+ .accesskey = U
+
+text-action-select-all =
+ .label = Zaznacz wszystko
+ .accesskey = a
+
+text-action-select-all-shortcut =
+ .key = A
diff --git a/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/printing/printUI.ftl b/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/printing/printUI.ftl
new file mode 100644
index 0000000000..15be50e479
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/printing/printUI.ftl
@@ -0,0 +1,97 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+printui-title = Drukowanie
+# Dialog title to prompt the user for a filename to save print to PDF.
+printui-save-to-pdf-title = Zapisz jako
+# Variables
+# $sheetCount (integer) - Number of paper sheets
+printui-sheets-count =
+ { $sheetCount ->
+ [one] { $sheetCount } kartka papieru
+ [few] { $sheetCount } kartki papieru
+ *[many] { $sheetCount } kartek papieru
+ }
+printui-page-range-all = Wszystkie
+printui-page-range-custom = Wybrane
+printui-page-range-label = Strony
+printui-page-range-picker =
+ .aria-label = Wybierz zakres stron
+printui-page-custom-range =
+ .aria-label = Wprowadź inny zakres stron
+# This label is displayed before the first input field indicating
+# the start of the range to print.
+printui-range-start = Od
+# This label is displayed between the input fields indicating
+# the start and end page of the range to print.
+printui-range-end = do
+# Section title for the number of copies to print
+printui-copies-label = Kopie
+printui-orientation = Orientacja
+printui-landscape = Pozioma
+printui-portrait = Pionowa
+# Section title for the printer or destination device to target
+printui-destination-label = Drukarka
+printui-destination-pdf-label = Zapisz jako PDF
+printui-more-settings = Więcej ustawień
+printui-less-settings = Mniej ustawień
+printui-paper-size-label = Rozmiar papieru
+# Section title (noun) for the print scaling options
+printui-scale = Skalowanie
+printui-scale-fit-to-page-width = Dopasuj do szerokości strony
+# Label for input control where user can set the scale percentage
+printui-scale-pcent = Skala
+# Section title (noun) for the two-sided print options
+printui-two-sided-printing = Druk dwustronny
+printui-duplex-checkbox = Drukuj na obu stronach
+# Section title for miscellaneous print options
+printui-options = Opcje
+printui-headers-footers-checkbox = Drukuj nagłówki i stopki
+printui-backgrounds-checkbox = Drukuj tła
+printui-color-mode-label = Tryb kolorów
+printui-color-mode-color = Kolorowy
+printui-color-mode-bw = Czarno-biały
+printui-margins = Marginesy
+printui-margins-default = Domyślne
+printui-margins-min = Minimalne
+printui-margins-none = Bez
+printui-margins-custom = Niestandardowe
+printui-margins-custom-top = Górny
+printui-margins-custom-bottom = Dolny
+printui-margins-custom-left = Lewy
+printui-margins-custom-right = Prawy
+printui-system-dialog-link = Drukuj za pomocą okna systemowego…
+printui-primary-button = Drukuj
+printui-primary-button-save = Zapisz
+printui-cancel-button = Anuluj
+printui-loading = Przygotowywanie podglądu
+# Reported by screen readers and other accessibility tools to indicate that
+# the print preview has focus.
+printui-preview-label =
+ .aria-label = Podgląd wydruku
+
+## Paper sizes that may be supported by the Save to PDF destination:
+
+printui-paper-a5 = A5
+printui-paper-a4 = A4
+printui-paper-a3 = A3
+printui-paper-a2 = A2
+printui-paper-a1 = A1
+printui-paper-a0 = A0
+printui-paper-b5 = B5
+printui-paper-b4 = B4
+printui-paper-jis-b5 = JIS-B5
+printui-paper-jis-b4 = JIS-B4
+printui-paper-letter = US Letter
+printui-paper-legal = US Legal
+printui-paper-tabloid = Tabloid
+
+## Error messages shown when a user has an invalid input
+
+printui-error-invalid-scale = Skala musi być liczbą między 10 a 200.
+printui-error-invalid-margin = Wprowadź prawidłowy margines dla wybranego rozmiaru papieru.
+# Variables
+# $numPages (integer) - Number of pages
+printui-error-invalid-range = Zakres musi być liczbą między 1 a { $numPages }.
+printui-error-invalid-start-overflow = Numer strony „od” musi być mniejszy niż numer strony „do”.
diff --git a/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/updates/history.ftl b/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/updates/history.ftl
new file mode 100644
index 0000000000..a1734fdcce
--- /dev/null
+++ b/third_party/rust/fluent-testing/resources/toolkit/pl/toolkit/updates/history.ftl
@@ -0,0 +1,27 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+history-title = Historia aktualizacji
+# history-intro = Zainstalowane aktualizacje:
+
+close-button-label =
+ .buttonlabelcancel = Zamknij
+ .title = Historia aktualizacji
+
+no-updates-label = Jeszcze nie zainstalowano żadnych aktualizacji
+name-header = Nazwa aktualizacji
+date-header = Data instalacji
+type-header = Rodzaj
+state-header = Stan
+
+# Used to display update history
+#
+# Variables:
+# $name (String): name of the update
+# $buildID (String): build identifier from the local updates.xml
+update-full-build-name = { $name } ({ $buildID })
+
+update-details = Szczegóły
+update-installed-on = Zainstalowano: { $date }
+update-status = Stan: { $status }
diff --git a/third_party/rust/fluent-testing/src/fs.rs b/third_party/rust/fluent-testing/src/fs.rs
new file mode 100644
index 0000000000..1efd6a90cb
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/fs.rs
@@ -0,0 +1,41 @@
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::path::PathBuf;
+
+#[derive(Default)]
+pub struct MockFileSystem {
+ files: RefCell<HashMap<String, std::io::Result<String>>>,
+}
+
+impl MockFileSystem {
+ pub fn clear(&self) {
+ self.files.borrow_mut().clear();
+ }
+
+ fn get_test_file_path() -> PathBuf {
+ PathBuf::from(std::env!("CARGO_MANIFEST_DIR")).join("resources")
+ }
+
+ fn get_file(&self, path: &str) -> std::io::Result<String> {
+ let mut tmp = self.files.borrow_mut();
+ let result = tmp.entry(path.to_string()).or_insert_with(|| {
+ let root_path = Self::get_test_file_path();
+ let full_path = root_path.join(path);
+ std::fs::read_to_string(full_path)
+ });
+ match result {
+ Ok(s) => Ok(s.to_string()),
+ Err(e) => Err(std::io::Error::new(e.kind(), "Error")),
+ }
+ }
+
+ #[cfg(feature = "sync")]
+ pub fn get_test_file_sync(&self, path: &str) -> std::io::Result<String> {
+ self.get_file(path)
+ }
+
+ #[cfg(feature = "async")]
+ pub async fn get_test_file_async(&self, path: &str) -> std::io::Result<String> {
+ self.get_file(path)
+ }
+}
diff --git a/third_party/rust/fluent-testing/src/lib.rs b/third_party/rust/fluent-testing/src/lib.rs
new file mode 100644
index 0000000000..3e71952eef
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/lib.rs
@@ -0,0 +1,5 @@
+mod fs;
+pub mod scenarios;
+
+pub use fs::MockFileSystem;
+pub use scenarios::get_scenarios;
diff --git a/third_party/rust/fluent-testing/src/scenarios/browser.rs b/third_party/rust/fluent-testing/src/scenarios/browser.rs
new file mode 100644
index 0000000000..76e2acb7c8
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/browser.rs
@@ -0,0 +1,389 @@
+use super::structs::*;
+use crate::queries;
+
+pub fn get_scenario() -> Scenario {
+ Scenario::new(
+ "browser",
+ vec![
+ FileSource::new("toolkit", "toolkit/{locale}/", vec!["en-US", "pl"]),
+ FileSource::new("browser", "browser/{locale}/", vec!["en-US", "pl"]),
+ ],
+ vec!["en-US"],
+ vec![
+ "branding/brand.ftl",
+ "browser/branding/sync-brand.ftl",
+ "browser/branding/brandings.ftl",
+ "toolkit/global/textActions.ftl",
+ "browser/browser.ftl",
+ "browser/browserContext.ftl",
+ "browser/browserSets.ftl",
+ "browser/menubar.ftl",
+ "browser/protectionsPanel.ftl",
+ "browser/appmenu.ftl",
+ "preview/interventions.ftl",
+ "browser/sidebarMenu.ftl",
+ "browser/allTabsMenu.ftl",
+ "browser/places.ftl",
+ "toolkit/printing/printUI.ftl",
+ "browser/downloads.ftl",
+ ],
+ queries![
+ (L10nKey::new("browser-main-window", Some(vec![L10nArgument::new("content-title", "CONTENTTITLE")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("data-title-default", "Nightly"), L10nAttribute::new("data-title-private", "Nightly (Private Browsing)"), L10nAttribute::new("data-content-title-default", "CONTENTTITLE \\u2014 Nightly"), L10nAttribute::new("data-content-title-private", "CONTENTTITLE \\u2014 Nightly (Private Browsing)")]))),
+ ("browser-main-window-title", "Nightly"),
+ ("window-new-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "N")]))),
+ ("tab-new-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "T")]))),
+ ("location-open-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "L")]))),
+ ("location-open-shortcut-alt", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "D")]))),
+ ("search-focus-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "K")]))),
+ ("search-focus-shortcut-alt", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "J")]))),
+ ("downloads-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "Y")]))),
+ ("addons-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "A")]))),
+ ("file-open-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "O")]))),
+ ("save-page-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "S")]))),
+ ("print-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "P")]))),
+ ("close-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "W")]))),
+ ("close-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "W")]))),
+ ("mute-toggle-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "M")]))),
+ ("text-action-undo-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "Z")]))),
+ ("text-action-undo-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "Z")]))),
+ ("text-action-cut-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "X")]))),
+ ("text-action-copy-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "C")]))),
+ ("text-action-paste-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "V")]))),
+ ("text-action-select-all-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "A")]))),
+ ("nav-back-shortcut-alt", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "[")]))),
+ ("nav-fwd-shortcut-alt", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "]")]))),
+ ("history-show-all-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "H")]))),
+ ("reader-mode-toggle-shortcut-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "R")]))),
+ ("picture-in-picture-toggle-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "]")]))),
+ ("picture-in-picture-toggle-shortcut-alt", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "}")]))),
+ ("nav-reload-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "R")]))),
+ ("nav-reload-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "R")]))),
+ ("page-source-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "U")]))),
+ ("page-info-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "I")]))),
+ ("find-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "F")]))),
+ ("search-find-again-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "G")]))),
+ ("search-find-again-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "G")]))),
+ ("search-find-again-shortcut-alt", L10nMessage::new(None, Some(vec![L10nAttribute::new("keycode", "VK_F3")]))),
+ ("search-find-again-shortcut-alt", L10nMessage::new(None, Some(vec![L10nAttribute::new("keycode", "VK_F3")]))),
+ ("bookmark-this-page-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "D")]))),
+ ("bookmark-this-page-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "D")]))),
+ ("bookmark-show-library-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "O")]))),
+ ("bookmark-show-sidebar-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "B")]))),
+ ("bookmark-show-toolbar-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "B")]))),
+ ("history-sidebar-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "H")]))),
+ ("full-zoom-reduce-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "-")]))),
+ ("full-zoom-reduce-shortcut-alt-a", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "_")]))),
+ ("full-zoom-reduce-shortcut-alt-b", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "")]))),
+ ("full-zoom-enlarge-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "+")]))),
+ ("full-zoom-enlarge-shortcut-alt", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "=")]))),
+ ("full-zoom-enlarge-shortcut-alt2", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "")]))),
+ ("full-zoom-reset-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "0")]))),
+ ("full-zoom-reset-shortcut-alt", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "")]))),
+ ("bidi-switch-direction-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "X")]))),
+ ("private-browsing-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "P")]))),
+ ("quit-app-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "Q")]))),
+ ("tab-new-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "T")]))),
+ ("window-new-shortcut", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "N")]))),
+ ("sidebar-menu-bookmarks", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmarks")]))),
+ ("sidebar-menu-history", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "History")]))),
+ ("sidebar-menu-synced-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Synced Tabs")]))),
+ ("sidebar-menu-close", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Close Sidebar")]))),
+ ("full-screen-autohide", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Hide Toolbars"), L10nAttribute::new("accesskey", "H")]))),
+ ("full-screen-exit", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Exit Full Screen Mode"), L10nAttribute::new("accesskey", "F")]))),
+ ("main-context-menu-back", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Go back one page"), L10nAttribute::new("aria-label", "Back"), L10nAttribute::new("accesskey", "B")]))),
+ ("main-context-menu-forward", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Go forward one page"), L10nAttribute::new("aria-label", "Forward"), L10nAttribute::new("accesskey", "F")]))),
+ ("main-context-menu-reload", L10nMessage::new(None, Some(vec![L10nAttribute::new("aria-label", "Reload"), L10nAttribute::new("accesskey", "R")]))),
+ ("main-context-menu-stop", L10nMessage::new(None, Some(vec![L10nAttribute::new("aria-label", "Stop"), L10nAttribute::new("accesskey", "S")]))),
+ ("main-context-menu-bookmark-add", L10nMessage::new(None, Some(vec![L10nAttribute::new("aria-label", "Bookmark This Page"), L10nAttribute::new("accesskey", "m"), L10nAttribute::new("tooltiptext", "Bookmark this page")]))),
+ ("main-context-menu-open-link", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open Link"), L10nAttribute::new("accesskey", "O")]))),
+ ("main-context-menu-open-link-new-tab", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open Link in New Tab"), L10nAttribute::new("accesskey", "T")]))),
+ ("main-context-menu-open-link-container-tab", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open Link in New Container Tab"), L10nAttribute::new("accesskey", "b")]))),
+ ("main-context-menu-open-link-new-window", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open Link in New Window"), L10nAttribute::new("accesskey", "W")]))),
+ ("main-context-menu-open-link-new-private-window", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open Link in New Private Window"), L10nAttribute::new("accesskey", "P")]))),
+ ("main-context-menu-bookmark-this-link", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmark This Link"), L10nAttribute::new("accesskey", "L")]))),
+ ("main-context-menu-save-link", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Link As…"), L10nAttribute::new("accesskey", "k")]))),
+ ("main-context-menu-save-link-to-pocket", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Link to Pocket"), L10nAttribute::new("accesskey", "o")]))),
+ ("main-context-menu-copy-email", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy Email Address"), L10nAttribute::new("accesskey", "A")]))),
+ ("main-context-menu-copy-link", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy Link Location"), L10nAttribute::new("accesskey", "a")]))),
+ ("main-context-menu-media-play", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Play"), L10nAttribute::new("accesskey", "P")]))),
+ ("main-context-menu-media-pause", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Pause"), L10nAttribute::new("accesskey", "P")]))),
+ ("main-context-menu-media-mute", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Mute"), L10nAttribute::new("accesskey", "M")]))),
+ ("main-context-menu-media-unmute", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Unmute"), L10nAttribute::new("accesskey", "m")]))),
+ ("main-context-menu-media-play-speed", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Play Speed"), L10nAttribute::new("accesskey", "d")]))),
+ ("main-context-menu-media-play-speed-slow", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Slow (0.5)"), L10nAttribute::new("accesskey", "S")]))),
+ ("main-context-menu-media-play-speed-normal", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Normal"), L10nAttribute::new("accesskey", "N")]))),
+ ("main-context-menu-media-play-speed-fast", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Fast (1.25)"), L10nAttribute::new("accesskey", "F")]))),
+ ("main-context-menu-media-play-speed-faster", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Faster (1.5)"), L10nAttribute::new("accesskey", "a")]))),
+ ("main-context-menu-media-play-speed-fastest", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Ludicrous (2)"), L10nAttribute::new("accesskey", "L")]))),
+ ("main-context-menu-media-loop", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Loop"), L10nAttribute::new("accesskey", "L")]))),
+ ("main-context-menu-media-show-controls", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show Controls"), L10nAttribute::new("accesskey", "C")]))),
+ ("main-context-menu-media-hide-controls", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Hide Controls"), L10nAttribute::new("accesskey", "C")]))),
+ ("main-context-menu-media-video-fullscreen", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Full Screen"), L10nAttribute::new("accesskey", "F")]))),
+ ("main-context-menu-media-video-leave-fullscreen", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Exit Full Screen"), L10nAttribute::new("accesskey", "u")]))),
+ ("main-context-menu-media-pip", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Picture-in-Picture"), L10nAttribute::new("accesskey", "u")]))),
+ ("main-context-menu-image-reload", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Reload Image"), L10nAttribute::new("accesskey", "R")]))),
+ ("main-context-menu-image-view", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Image"), L10nAttribute::new("accesskey", "I")]))),
+ ("main-context-menu-video-view", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Video"), L10nAttribute::new("accesskey", "i")]))),
+ ("main-context-menu-image-copy", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy Image"), L10nAttribute::new("accesskey", "y")]))),
+ ("main-context-menu-image-copy-location", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy Image Location"), L10nAttribute::new("accesskey", "o")]))),
+ ("main-context-menu-video-copy-location", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy Video Location"), L10nAttribute::new("accesskey", "o")]))),
+ ("main-context-menu-audio-copy-location", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy Audio Location"), L10nAttribute::new("accesskey", "o")]))),
+ ("main-context-menu-image-save-as", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Image As…"), L10nAttribute::new("accesskey", "v")]))),
+ ("main-context-menu-image-email", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Email Image…"), L10nAttribute::new("accesskey", "g")]))),
+ ("main-context-menu-image-set-as-background", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Set As Desktop Background…"), L10nAttribute::new("accesskey", "S")]))),
+ ("main-context-menu-image-info", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Image Info"), L10nAttribute::new("accesskey", "f")]))),
+ ("main-context-menu-image-desc", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Description"), L10nAttribute::new("accesskey", "D")]))),
+ ("main-context-menu-video-save-as", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Video As…"), L10nAttribute::new("accesskey", "v")]))),
+ ("main-context-menu-audio-save-as", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Audio As…"), L10nAttribute::new("accesskey", "v")]))),
+ ("main-context-menu-video-image-save-as", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Snapshot As…"), L10nAttribute::new("accesskey", "S")]))),
+ ("main-context-menu-video-email", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Email Video…"), L10nAttribute::new("accesskey", "a")]))),
+ ("main-context-menu-audio-email", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Email Audio…"), L10nAttribute::new("accesskey", "a")]))),
+ ("main-context-menu-plugin-play", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Activate this plugin"), L10nAttribute::new("accesskey", "c")]))),
+ ("main-context-menu-plugin-hide", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Hide this plugin"), L10nAttribute::new("accesskey", "H")]))),
+ ("main-context-menu-page-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Page As…"), L10nAttribute::new("accesskey", "P")]))),
+ ("main-context-menu-save-to-pocket", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Page to Pocket"), L10nAttribute::new("accesskey", "k")]))),
+ ("main-context-menu-send-to-device", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Send Page to Device"), L10nAttribute::new("accesskey", "n")]))),
+ ("main-context-menu-view-background-image", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Background Image"), L10nAttribute::new("accesskey", "w")]))),
+ ("main-context-menu-generate-new-password", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Generated Password…"), L10nAttribute::new("accesskey", "G")]))),
+ ("text-action-undo", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Undo"), L10nAttribute::new("accesskey", "U")]))),
+ ("text-action-cut", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cut"), L10nAttribute::new("accesskey", "t")]))),
+ ("text-action-copy", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy"), L10nAttribute::new("accesskey", "C")]))),
+ ("text-action-paste", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Paste"), L10nAttribute::new("accesskey", "P")]))),
+ ("text-action-delete", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Delete"), L10nAttribute::new("accesskey", "D")]))),
+ ("text-action-select-all", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Select All"), L10nAttribute::new("accesskey", "A")]))),
+ ("main-context-menu-keyword", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Add a Keyword for this Search…"), L10nAttribute::new("accesskey", "K")]))),
+ ("main-context-menu-link-send-to-device", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Send Link to Device"), L10nAttribute::new("accesskey", "n")]))),
+ ("main-context-menu-frame", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "This Frame"), L10nAttribute::new("accesskey", "h")]))),
+ ("main-context-menu-frame-show-this", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show Only This Frame"), L10nAttribute::new("accesskey", "S")]))),
+ ("main-context-menu-frame-open-tab", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open Frame in New Tab"), L10nAttribute::new("accesskey", "T")]))),
+ ("main-context-menu-frame-open-window", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open Frame in New Window"), L10nAttribute::new("accesskey", "W")]))),
+ ("main-context-menu-frame-reload", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Reload Frame"), L10nAttribute::new("accesskey", "R")]))),
+ ("main-context-menu-frame-bookmark", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmark This Frame"), L10nAttribute::new("accesskey", "m")]))),
+ ("main-context-menu-frame-save-as", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Frame As…"), L10nAttribute::new("accesskey", "F")]))),
+ ("main-context-menu-frame-print", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Print Frame…"), L10nAttribute::new("accesskey", "P")]))),
+ ("main-context-menu-frame-view-source", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Frame Source"), L10nAttribute::new("accesskey", "V")]))),
+ ("main-context-menu-frame-view-info", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Frame Info"), L10nAttribute::new("accesskey", "I")]))),
+ ("main-context-menu-print-selection", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Print Selection"), L10nAttribute::new("accesskey", "r")]))),
+ ("main-context-menu-view-selection-source", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Selection Source"), L10nAttribute::new("accesskey", "e")]))),
+ ("main-context-menu-view-page-source", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Page Source"), L10nAttribute::new("accesskey", "V")]))),
+ ("main-context-menu-view-page-info", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Page Info"), L10nAttribute::new("accesskey", "I")]))),
+ ("main-context-menu-bidi-switch-text", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Switch Text Direction"), L10nAttribute::new("accesskey", "w")]))),
+ ("main-context-menu-bidi-switch-page", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Switch Page Direction"), L10nAttribute::new("accesskey", "D")]))),
+ ("main-context-menu-inspect-a11y-properties", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Inspect Accessibility Properties")]))),
+ ("main-context-menu-inspect-element", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Inspect Element"), L10nAttribute::new("accesskey", "Q")]))),
+ ("main-context-menu-eme-learn-more", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Learn more about DRM…"), L10nAttribute::new("accesskey", "D")]))),
+ ("places-open", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open"), L10nAttribute::new("accesskey", "O")]))),
+ ("places-open-tab", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in a New Tab"), L10nAttribute::new("accesskey", "w")]))),
+ ("places-open-all-in-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open All in Tabs"), L10nAttribute::new("accesskey", "O")]))),
+ ("places-open-all-in-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open All in Tabs"), L10nAttribute::new("accesskey", "O")]))),
+ ("places-open-window", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in a New Window"), L10nAttribute::new("accesskey", "N")]))),
+ ("places-open-private-window", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in a New Private Window"), L10nAttribute::new("accesskey", "P")]))),
+ ("places-new-bookmark", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "New Bookmark…"), L10nAttribute::new("accesskey", "B")]))),
+ ("places-new-folder-contextmenu", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "New Folder…"), L10nAttribute::new("accesskey", "F")]))),
+ ("places-new-separator", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "New Separator"), L10nAttribute::new("accesskey", "S")]))),
+ ("text-action-cut", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cut"), L10nAttribute::new("accesskey", "t")]))),
+ ("text-action-copy", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy"), L10nAttribute::new("accesskey", "C")]))),
+ ("text-action-paste", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Paste"), L10nAttribute::new("accesskey", "P")]))),
+ ("text-action-delete", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Delete"), L10nAttribute::new("accesskey", "D")]))),
+ ("places-delete-domain-data", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Forget About This Site"), L10nAttribute::new("accesskey", "F")]))),
+ ("places-sortby-name", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Sort By Name"), L10nAttribute::new("accesskey", "r")]))),
+ ("places-properties", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Properties"), L10nAttribute::new("accesskey", "i")]))),
+ ("page-action-add-to-urlbar", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Add to Address Bar")]))),
+ ("page-action-remove-from-urlbar", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove from Address Bar")]))),
+ ("page-action-add-to-urlbar", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Add to Address Bar")]))),
+ ("page-action-remove-from-urlbar", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove from Address Bar")]))),
+ ("page-action-manage-extension", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Manage Extension…")]))),
+ ("page-action-remove-extension", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove Extension")]))),
+ ("navbar-tooltip-back", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Go back one page")]))),
+ ("navbar-tooltip-instruction", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Right-click or pull down to show history")]))),
+ ("navbar-tooltip-forward", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Go forward one page")]))),
+ ("navbar-tooltip-instruction", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Right-click or pull down to show history")]))),
+ ("popup-select-camera", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Camera to share:"), L10nAttribute::new("accesskey", "C")]))),
+ ("popup-all-windows-shared", "All visible windows on your screen will be shared."),
+ ("popup-select-microphone", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Microphone to share:"), L10nAttribute::new("accesskey", "M")]))),
+ ("downloads-panel", L10nMessage::new(None, Some(vec![L10nAttribute::new("aria-label", "Downloads")]))),
+ ("downloads-cmd-pause", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Pause"), L10nAttribute::new("accesskey", "P")]))),
+ ("downloads-cmd-resume", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Resume"), L10nAttribute::new("accesskey", "R")]))),
+ ("downloads-cmd-unblock", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Allow Download"), L10nAttribute::new("accesskey", "o")]))),
+ ("downloads-cmd-use-system-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open In System Viewer"), L10nAttribute::new("accesskey", "V")]))),
+ ("downloads-cmd-always-use-system-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always Open In System Viewer"), L10nAttribute::new("accesskey", "w")]))),
+ ("downloads-cmd-show-menuitem", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open Containing Folder"), L10nAttribute::new("accesskey", "F")]))),
+ ("downloads-cmd-go-to-download-page", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Go To Download Page"), L10nAttribute::new("accesskey", "G")]))),
+ ("downloads-cmd-copy-download-link", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy Download Link"), L10nAttribute::new("accesskey", "L")]))),
+ ("downloads-cmd-remove-from-history", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove From History"), L10nAttribute::new("accesskey", "e")]))),
+ ("downloads-cmd-clear-list", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Clear Preview Panel"), L10nAttribute::new("accesskey", "a")]))),
+ ("downloads-cmd-clear-downloads", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Clear Downloads"), L10nAttribute::new("accesskey", "D")]))),
+ ("downloads-panel-list", L10nMessage::new(None, Some(vec![L10nAttribute::new("style", "width: 70ch")]))),
+ ("downloads-panel-empty", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "No downloads for this session.")]))),
+ ("downloads-history", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show All Downloads"), L10nAttribute::new("accesskey", "S")]))),
+ ("downloads-details", L10nMessage::new(None, Some(vec![L10nAttribute::new("title", "Download Details")]))),
+ ("downloads-cmd-show-downloads", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show Downloads Folder")]))),
+ ("downloads-history", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show All Downloads"), L10nAttribute::new("accesskey", "S")]))),
+ ("text-action-undo", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Undo"), L10nAttribute::new("accesskey", "U")]))),
+ ("text-action-cut", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cut"), L10nAttribute::new("accesskey", "t")]))),
+ ("text-action-copy", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy"), L10nAttribute::new("accesskey", "C")]))),
+ ("text-action-paste", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Paste"), L10nAttribute::new("accesskey", "P")]))),
+ ("text-action-delete", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Delete"), L10nAttribute::new("accesskey", "D")]))),
+ ("text-action-select-all", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Select All"), L10nAttribute::new("accesskey", "A")]))),
+ ("menu-file", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "File"), L10nAttribute::new("accesskey", "F")]))),
+ ("menu-file-new-tab", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "New Tab"), L10nAttribute::new("accesskey", "T")]))),
+ ("menu-file-new-container-tab", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "New Container Tab"), L10nAttribute::new("accesskey", "b")]))),
+ ("menu-file-new-window", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "New Window"), L10nAttribute::new("accesskey", "N")]))),
+ ("menu-file-new-private-window", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "New Private Window"), L10nAttribute::new("accesskey", "W")]))),
+ ("menu-file-open-location", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open Location…")]))),
+ ("menu-file-open-file", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open File…"), L10nAttribute::new("accesskey", "O")]))),
+ ("menu-file-close", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Close"), L10nAttribute::new("accesskey", "C")]))),
+ ("menu-file-close-window", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Close Window"), L10nAttribute::new("accesskey", "d")]))),
+ ("menu-file-save-page", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save Page As…"), L10nAttribute::new("accesskey", "A")]))),
+ ("menu-file-email-link", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Email Link…"), L10nAttribute::new("accesskey", "E")]))),
+ ("menu-file-print-preview", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Print Preview"), L10nAttribute::new("accesskey", "v")]))),
+ ("menu-file-print", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Print…"), L10nAttribute::new("accesskey", "P")]))),
+ ("menu-file-import-from-another-browser", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Import From Another Browser…"), L10nAttribute::new("accesskey", "I")]))),
+ ("menu-file-go-offline", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Work Offline"), L10nAttribute::new("accesskey", "k")]))),
+ ("menu-edit", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Edit"), L10nAttribute::new("accesskey", "E")]))),
+ ("text-action-undo", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Undo"), L10nAttribute::new("accesskey", "U")]))),
+ ("text-action-redo", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Redo"), L10nAttribute::new("accesskey", "R")]))),
+ ("text-action-cut", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cut"), L10nAttribute::new("accesskey", "t")]))),
+ ("text-action-copy", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy"), L10nAttribute::new("accesskey", "C")]))),
+ ("text-action-paste", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Paste"), L10nAttribute::new("accesskey", "P")]))),
+ ("text-action-delete", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Delete"), L10nAttribute::new("accesskey", "D")]))),
+ ("text-action-select-all", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Select All"), L10nAttribute::new("accesskey", "A")]))),
+ ("menu-edit-find-on", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Find in This Page…"), L10nAttribute::new("accesskey", "F")]))),
+ ("menu-edit-find-again", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Find Again"), L10nAttribute::new("accesskey", "g")]))),
+ ("menu-edit-bidi-switch-text-direction", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Switch Text Direction"), L10nAttribute::new("accesskey", "w")]))),
+ ("menu-preferences", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Preferences"), L10nAttribute::new("accesskey", "n")]))),
+ ("menu-view", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View"), L10nAttribute::new("accesskey", "V")]))),
+ ("menu-view-toolbars-menu", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Toolbars"), L10nAttribute::new("accesskey", "T")]))),
+ ("menu-view-customize-toolbar", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Customize…"), L10nAttribute::new("accesskey", "C")]))),
+ ("menu-view-sidebar", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Sidebar"), L10nAttribute::new("accesskey", "e")]))),
+ ("menu-view-bookmarks", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmarks")]))),
+ ("menu-view-history-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "History")]))),
+ ("menu-view-synced-tabs-sidebar", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Synced Tabs")]))),
+ ("menu-view-full-zoom", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Zoom"), L10nAttribute::new("accesskey", "Z")]))),
+ ("menu-view-full-zoom-enlarge", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Zoom In"), L10nAttribute::new("accesskey", "I")]))),
+ ("menu-view-full-zoom-reduce", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Zoom Out"), L10nAttribute::new("accesskey", "O")]))),
+ ("menu-view-full-zoom-actual-size", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Actual Size"), L10nAttribute::new("accesskey", "A")]))),
+ ("menu-view-full-zoom-toggle", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Zoom Text Only"), L10nAttribute::new("accesskey", "T")]))),
+ ("menu-view-page-style-menu", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Page Style"), L10nAttribute::new("accesskey", "y")]))),
+ ("menu-view-page-style-no-style", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "No Style"), L10nAttribute::new("accesskey", "n")]))),
+ ("menu-view-page-basic-style", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Basic Page Style"), L10nAttribute::new("accesskey", "B")]))),
+ ("menu-view-charset", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Text Encoding"), L10nAttribute::new("accesskey", "c")]))),
+ ("menu-view-full-screen", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Full Screen"), L10nAttribute::new("accesskey", "F")]))),
+ ("menu-view-show-all-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show All Tabs"), L10nAttribute::new("accesskey", "A")]))),
+ ("menu-view-bidi-switch-page-direction", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Switch Page Direction"), L10nAttribute::new("accesskey", "D")]))),
+ ("menu-history", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "History"), L10nAttribute::new("accesskey", "s")]))),
+ ("menu-history-show-all-history", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show All History")]))),
+ ("menu-history-clear-recent-history", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Clear Recent History…")]))),
+ ("menu-history-synced-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Synced Tabs")]))),
+ ("menu-history-restore-last-session", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Restore Previous Session")]))),
+ ("menu-history-hidden-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Hidden Tabs")]))),
+ ("menu-history-undo-menu", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Recently Closed Tabs")]))),
+ ("menu-history-undo-window-menu", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Recently Closed Windows")]))),
+ ("menu-bookmarks-menu", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmarks"), L10nAttribute::new("accesskey", "B")]))),
+ ("menu-bookmarks-show-all", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show All Bookmarks")]))),
+ ("menu-bookmark-this-page", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmark This Page")]))),
+ ("menu-bookmarks-all-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmark All Tabs…")]))),
+ ("menu-bookmarks-toolbar", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmarks Toolbar")]))),
+ ("menu-bookmarks-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Other Bookmarks")]))),
+ ("menu-bookmarks-mobile", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Mobile Bookmarks")]))),
+ ("menu-tools", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Tools"), L10nAttribute::new("accesskey", "T")]))),
+ ("menu-tools-downloads", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Downloads"), L10nAttribute::new("accesskey", "D")]))),
+ ("menu-tools-addons", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Add-ons"), L10nAttribute::new("accesskey", "A")]))),
+ ("menu-tools-fxa-sign-in", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Sign In To Firefox…"), L10nAttribute::new("accesskey", "g")]))),
+ ("menu-tools-turn-on-sync", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Turn on Sync…"), L10nAttribute::new("accesskey", "n")]))),
+ ("menu-tools-fxa-sign-in", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Sign In To Firefox…"), L10nAttribute::new("accesskey", "g")]))),
+ ("menu-tools-sync-now", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Sync Now"), L10nAttribute::new("accesskey", "S")]))),
+ ("menu-tools-fxa-re-auth", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Reconnect to Firefox…"), L10nAttribute::new("accesskey", "R")]))),
+ ("menu-tools-web-developer", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web Developer"), L10nAttribute::new("accesskey", "W")]))),
+ ("menu-tools-page-source", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Page Source"), L10nAttribute::new("accesskey", "o")]))),
+ ("menu-tools-page-info", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Page Info"), L10nAttribute::new("accesskey", "I")]))),
+ ("menu-help", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Help"), L10nAttribute::new("accesskey", "H")]))),
+ ("menu-help-product", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Nightly Help"), L10nAttribute::new("accesskey", "H")]))),
+ ("menu-help-show-tour", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Nightly Tour"), L10nAttribute::new("accesskey", "o")]))),
+ ("menu-help-import-from-another-browser", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Import From Another Browser…"), L10nAttribute::new("accesskey", "I")]))),
+ ("menu-help-keyboard-shortcuts", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Keyboard Shortcuts"), L10nAttribute::new("accesskey", "K")]))),
+ ("menu-help-troubleshooting-info", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Troubleshooting Information"), L10nAttribute::new("accesskey", "T")]))),
+ ("menu-help-feedback-page", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Submit Feedback…"), L10nAttribute::new("accesskey", "S")]))),
+ ("menu-help-safe-mode-without-addons", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Restart With Add-ons Disabled…"), L10nAttribute::new("accesskey", "R")]))),
+ ("menu-help-report-deceptive-site", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Report Deceptive Site…"), L10nAttribute::new("accesskey", "D")]))),
+ ("menu-help-not-deceptive", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "This Isn\\u2019t a Deceptive Site…"), L10nAttribute::new("accesskey", "D")]))),
+ ("browser-window-minimize-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Minimize")]))),
+ ("browser-window-maximize-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Maximize")]))),
+ ("browser-window-restore-down-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Restore Down")]))),
+ ("browser-window-close-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Close")]))),
+ ("browser-window-minimize-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Minimize")]))),
+ ("browser-window-maximize-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Maximize")]))),
+ ("browser-window-restore-down-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Restore Down")]))),
+ ("browser-window-close-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Close")]))),
+ ("toolbar-button-back", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Back")]))),
+ ("toolbar-button-forward", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Forward")]))),
+ ("toolbar-button-stop-reload", L10nMessage::new(None, Some(vec![L10nAttribute::new("title", "Reload")]))),
+ ("toolbar-button-reload", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Reload")]))),
+ ("toolbar-button-stop", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Stop")]))),
+ ("urlbar-identity-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("aria-label", "View site information")]))),
+ ("urlbar-permissions-granted", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have granted this website additional permissions.")]))),
+ ("urlbar-geolocation-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked location information for this website.")]))),
+ ("urlbar-xr-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked virtual reality device access for this website.")]))),
+ ("urlbar-web-notifications-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked notifications for this website.")]))),
+ ("urlbar-camera-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked your camera for this website.")]))),
+ ("urlbar-microphone-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked your microphone for this website.")]))),
+ ("urlbar-screen-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked this website from sharing your screen.")]))),
+ ("urlbar-persistent-storage-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked persistent storage for this website.")]))),
+ ("urlbar-popup-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked pop-ups for this website.")]))),
+ ("urlbar-autoplay-media-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked autoplay media with sound for this website.")]))),
+ ("urlbar-canvas-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked canvas data extraction for this website.")]))),
+ ("urlbar-midi-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked MIDI access for this website.")]))),
+ ("urlbar-install-blocked", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "You have blocked add-on installation for this website.")]))),
+ ("urlbar-default-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open message panel")]))),
+ ("urlbar-geolocation-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open location request panel")]))),
+ ("urlbar-xr-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open virtual reality permission panel")]))),
+ ("urlbar-autoplay-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open autoplay panel")]))),
+ ("urlbar-addons-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open add-on installation message panel")]))),
+ ("urlbar-canvas-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Manage canvas extraction permission")]))),
+ ("urlbar-indexed-db-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open offline storage message panel")]))),
+ ("urlbar-password-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open save password message panel")]))),
+ ("urlbar-plugins-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Manage plug-in use")]))),
+ ("urlbar-web-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Change whether you can receive notifications from the site")]))),
+ ("urlbar-web-rtc-share-devices-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Manage sharing your camera and/or microphone with the site")]))),
+ ("urlbar-web-rtc-share-microphone-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Manage sharing your microphone with the site")]))),
+ ("urlbar-web-rtc-share-screen-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Manage sharing your windows or screen with the site")]))),
+ ("urlbar-services-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open install message panel")]))),
+ ("urlbar-translate-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Translate this page")]))),
+ ("urlbar-translated-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Manage page translation")]))),
+ ("urlbar-eme-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Manage use of DRM software")]))),
+ ("urlbar-persistent-storage-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Store data in Persistent Storage")]))),
+ ("urlbar-midi-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open MIDI panel")]))),
+ ("urlbar-web-authn-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open Web Authentication panel")]))),
+ ("urlbar-storage-access-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Open browsing activity permission panel")]))),
+ ("urlbar-remote-control-notification-anchor", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Browser is under remote control")]))),
+ ("urlbar-switch-to-tab", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Switch to tab:")]))),
+ ("urlbar-extension", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Extension:")]))),
+ ("urlbar-placeholder", L10nMessage::new(None, Some(vec![L10nAttribute::new("placeholder", "Search or enter address")]))),
+ ("urlbar-go-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Go to the address in the Location Bar")]))),
+ ("urlbar-page-action-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Page actions")]))),
+ ("urlbar-pocket-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Save to Pocket")]))),
+ ("browser-window-minimize-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Minimize")]))),
+ ("browser-window-restore-down-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Restore Down")]))),
+ ("browser-window-close-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Close")]))),
+ ("bookmarks-toolbar", L10nMessage::new(None, Some(vec![L10nAttribute::new("toolbarname", "Bookmarks Toolbar"), L10nAttribute::new("accesskey", "B"), L10nAttribute::new("aria-label", "Bookmarks")]))),
+ ("bookmarks-toolbar-empty-message", "For quick access, place your bookmarks here on the bookmarks toolbar. <a data-l10n-name=\"manage-bookmarks\">Manage bookmarks…</a>"),
+ ("bookmarks-toolbar-placeholder", L10nMessage::new(None, Some(vec![L10nAttribute::new("title", "Bookmarks Toolbar Items")]))),
+ ("bookmarks-toolbar-placeholder-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmarks Toolbar Items")]))),
+ ("bookmarks-toolbar-placeholder-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmarks Toolbar Items")]))),
+ ("bookmarks-toolbar-chevron", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Show more bookmarks")]))),
+ ("fullscreen-warning-no-domain", "This document is now full screen"),
+ ("fullscreen-exit-button", "Exit Full Screen (Esc)"),
+ ("pointerlock-warning-no-domain", "This document has control of your pointer. Press Esc to take back control."),
+ ("text-action-undo", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Undo"), L10nAttribute::new("accesskey", "U")]))),
+ ("text-action-cut", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cut"), L10nAttribute::new("accesskey", "t")]))),
+ ("text-action-copy", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Copy"), L10nAttribute::new("accesskey", "C")]))),
+ ("text-action-paste", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Paste"), L10nAttribute::new("accesskey", "P")]))),
+ ("text-action-delete", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Delete"), L10nAttribute::new("accesskey", "D")]))),
+ ("text-action-select-all", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Select All"), L10nAttribute::new("accesskey", "A")]))),
+ (L10nKey::new("urlbar-pocket-button", Some(vec![L10nArgument::new("tabCount", "1")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Save to Pocket")]))),
+ ("menu-bookmark-this-page", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmark This Page")]))),
+ (L10nKey::new("urlbar-star-add-bookmark", Some(vec![L10nArgument::new("shortcut", "Ctrl+D")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Bookmark this page (Ctrl+D)")]))),
+ (L10nKey::new("main-context-menu-bookmark-add-with-shortcut", Some(vec![L10nArgument::new("shortcut", "Ctrl+D")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("aria-label", "Bookmark This Page"), L10nAttribute::new("accesskey", "m"), L10nAttribute::new("tooltiptext", "Bookmark this page (Ctrl+D)")])))
+ ],
+ )
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/empty_resource_all_locales.rs b/third_party/rust/fluent-testing/src/scenarios/empty_resource_all_locales.rs
new file mode 100644
index 0000000000..81b4049d89
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/empty_resource_all_locales.rs
@@ -0,0 +1,25 @@
+use super::structs::*;
+use crate::queries;
+
+/// Tests bundle generation with a queried value that is missing in all resources
+/// in all locales. Despite the fact that the value is missing, this should have no
+/// effect on the ability to generate a bundle and look up other present values.
+pub fn get_scenario() -> Scenario {
+ Scenario::new(
+ "empty_resource_all_locales",
+ vec![
+ FileSource::new("browser", "browser/{locale}/", vec!["en-US", "pl"]),
+ FileSource::new("empty", "empty-resource/{locale}/", vec!["en-US", "pl"]),
+ ],
+ vec!["en-US", "pl"],
+ vec!["browser/sanitize.ftl", "empty/empty-all.ftl"],
+ queries![
+ ("history-section-label", "History", ExceptionalContext::None),
+ (
+ "empty-all",
+ "empty-all",
+ ExceptionalContext::ValueMissingFromAllResources,
+ )
+ ],
+ )
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/empty_resource_one_locale.rs b/third_party/rust/fluent-testing/src/scenarios/empty_resource_one_locale.rs
new file mode 100644
index 0000000000..2f8a58e8a6
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/empty_resource_one_locale.rs
@@ -0,0 +1,25 @@
+use super::structs::*;
+use crate::queries;
+
+/// Tests bundle generation with a queried value that is missing in only one resource
+/// in the primary locale. This should cause the bundle to fallback to another locale
+/// only for that value.
+pub fn get_scenario() -> Scenario {
+ Scenario::new(
+ "empty_resource_one_locale",
+ vec![
+ FileSource::new("browser", "browser/{locale}/", vec!["en-US", "pl"]),
+ FileSource::new("empty", "empty-resource/{locale}/", vec!["en-US", "pl"]),
+ ],
+ vec!["en-US", "pl"],
+ vec!["browser/sanitize.ftl", "empty/empty-one.ftl"],
+ queries![
+ ("history-section-label", "History", ExceptionalContext::None),
+ (
+ "empty-one",
+ "pusty",
+ ExceptionalContext::ValueMissingFromResource,
+ )
+ ],
+ )
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/missing_optional_all_locales.rs b/third_party/rust/fluent-testing/src/scenarios/missing_optional_all_locales.rs
new file mode 100644
index 0000000000..8e59177971
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/missing_optional_all_locales.rs
@@ -0,0 +1,36 @@
+use fluent_fallback::types::{ResourceType, ToResourceId};
+
+use super::structs::*;
+use crate::queries;
+
+/// Tests bundle generation with an optional resource that is missing from all locales.
+/// Since the resource is optional, we should still be able to generate a bundle and
+/// look up other present values, but we will fail to look up a value from the missing resource.
+pub fn get_scenario() -> Scenario {
+ Scenario::new(
+ "missing_optional_all_locales",
+ vec![
+ FileSource::new("browser", "browser/{locale}/", vec!["en-US", "pl"]),
+ FileSource::new("missing", "missing-resource/{locale}/", vec!["en-US", "pl"]),
+ ],
+ vec!["en-US", "pl"],
+ vec![
+ "browser/sanitize.ftl".into(),
+ "missing/missing-one.ftl".to_resource_id(ResourceType::Optional),
+ "missing/missing-all.ftl".to_resource_id(ResourceType::Optional),
+ ],
+ queries![
+ ("history-section-label", "History", ExceptionalContext::None),
+ (
+ "missing-one",
+ "zaginiony",
+ ExceptionalContext::OptionalResourceMissingFromLocale,
+ ),
+ (
+ "missing-all",
+ "missing-all",
+ ExceptionalContext::OptionalResourceMissingFromAllLocales,
+ )
+ ],
+ )
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/missing_optional_one_locale.rs b/third_party/rust/fluent-testing/src/scenarios/missing_optional_one_locale.rs
new file mode 100644
index 0000000000..3afcec7529
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/missing_optional_one_locale.rs
@@ -0,0 +1,30 @@
+use fluent_fallback::types::{ResourceType, ToResourceId};
+
+use super::structs::*;
+use crate::queries;
+
+/// Tests bundle generation with an optional resource that is missing from only the primary locale.
+/// Since the resource is optional, we should only fallback to another locale for values in the missing
+/// optional resource. We should still be able to use the primary locale for other present values/resources.
+pub fn get_scenario() -> Scenario {
+ Scenario::new(
+ "missing_optional_one_locale",
+ vec![
+ FileSource::new("browser", "browser/{locale}/", vec!["en-US", "pl"]),
+ FileSource::new("missing", "missing-resource/{locale}/", vec!["en-US", "pl"]),
+ ],
+ vec!["en-US", "pl"],
+ vec![
+ "browser/sanitize.ftl".into(),
+ "missing/missing-one.ftl".to_resource_id(ResourceType::Optional),
+ ],
+ queries![
+ ("history-section-label", "History", ExceptionalContext::None),
+ (
+ "missing-one",
+ "zaginiony",
+ ExceptionalContext::OptionalResourceMissingFromLocale,
+ )
+ ],
+ )
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/missing_required_all_locales.rs b/third_party/rust/fluent-testing/src/scenarios/missing_required_all_locales.rs
new file mode 100644
index 0000000000..1d603c4ffc
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/missing_required_all_locales.rs
@@ -0,0 +1,38 @@
+use super::structs::*;
+use crate::queries;
+
+/// Tests bundle generation with a required resource that is missing from all locales.
+/// Since the resource is required, we cannot generate a bundle because no solution exists.
+/// Lookups for all values should fail, because no bundle will be generated.
+pub fn get_scenario() -> Scenario {
+ Scenario::new(
+ "missing_required_all_locales",
+ vec![
+ FileSource::new("browser", "browser/{locale}/", vec!["en-US", "pl"]),
+ FileSource::new("missing", "missing-resource/{locale}/", vec!["en-US", "pl"]),
+ ],
+ vec!["en-US", "pl"],
+ vec![
+ "browser/sanitize.ftl",
+ "missing/missing-one.ftl",
+ "missing/missing-all.ftl",
+ ],
+ queries![
+ (
+ "history-section-label",
+ "history-section-label",
+ ExceptionalContext::None,
+ ),
+ (
+ "missing-one",
+ "missing-one",
+ ExceptionalContext::RequiredResourceMissingFromLocale,
+ ),
+ (
+ "missing-all",
+ "missing-all",
+ ExceptionalContext::RequiredResourceMissingFromAllLocales,
+ )
+ ],
+ )
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/missing_required_one_locale.rs b/third_party/rust/fluent-testing/src/scenarios/missing_required_one_locale.rs
new file mode 100644
index 0000000000..affe791c5c
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/missing_required_one_locale.rs
@@ -0,0 +1,28 @@
+use super::structs::*;
+use crate::queries;
+
+/// Tests bundle generation with a required resource that is missing from only the primary locale.
+/// Since the resource is required, we should only fallback entirely to the next locale for all resources.
+pub fn get_scenario() -> Scenario {
+ Scenario::new(
+ "missing_required_one_locale",
+ vec![
+ FileSource::new("browser", "browser/{locale}/", vec!["en-US", "pl"]),
+ FileSource::new("missing", "missing-resource/{locale}/", vec!["en-US", "pl"]),
+ ],
+ vec!["en-US", "pl"],
+ vec!["browser/sanitize.ftl", "missing/missing-one.ftl"],
+ queries![
+ (
+ "history-section-label",
+ "Historia",
+ ExceptionalContext::None
+ ),
+ (
+ "missing-one",
+ "zaginiony",
+ ExceptionalContext::RequiredResourceMissingFromLocale,
+ )
+ ],
+ )
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/mod.rs b/third_party/rust/fluent-testing/src/scenarios/mod.rs
new file mode 100644
index 0000000000..f715468c14
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/mod.rs
@@ -0,0 +1,39 @@
+mod browser;
+mod empty_resource_all_locales;
+mod empty_resource_one_locale;
+mod missing_optional_all_locales;
+mod missing_optional_one_locale;
+mod missing_required_all_locales;
+mod missing_required_one_locale;
+mod preferences;
+mod simple;
+pub mod structs;
+
+use structs::*;
+
+#[macro_export]
+macro_rules! queries {
+ ( $( $x:expr ),* ) => {
+ {
+ Queries(vec![
+ $(
+ $x.into(),
+ )*
+ ])
+ }
+ };
+}
+
+pub fn get_scenarios() -> Vec<Scenario> {
+ vec![
+ simple::get_scenario(),
+ browser::get_scenario(),
+ preferences::get_scenario(),
+ empty_resource_one_locale::get_scenario(),
+ empty_resource_all_locales::get_scenario(),
+ missing_optional_one_locale::get_scenario(),
+ missing_optional_all_locales::get_scenario(),
+ missing_required_one_locale::get_scenario(),
+ missing_required_all_locales::get_scenario(),
+ ]
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/preferences.rs b/third_party/rust/fluent-testing/src/scenarios/preferences.rs
new file mode 100644
index 0000000000..887bfb7389
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/preferences.rs
@@ -0,0 +1,630 @@
+use super::structs::*;
+use crate::queries;
+
+pub fn get_scenario() -> Scenario {
+ Scenario::new(
+ "preferences",
+ vec![
+ FileSource::new("toolkit", "toolkit/{locale}/", vec!["en-US", "pl"]),
+ FileSource::new("browser", "browser/{locale}/", vec!["en-US", "pl"]),
+ ],
+ vec![
+ "en-US",
+ ],
+ vec![
+ "branding/brand.ftl",
+ "browser/branding/brandings.ftl",
+ "browser/branding/sync-brand.ftl",
+ "browser/browser.ftl",
+ "browser/preferences/preferences.ftl",
+ "browser/preferences/fonts.ftl",
+ "toolkit/featuregates/features.ftl",
+ "browser/preferences/addEngine.ftl",
+ "browser/preferences/blocklists.ftl",
+ "browser/preferences/clearSiteData.ftl",
+ "browser/preferences/colors.ftl",
+ "browser/preferences/connection.ftl",
+ "browser/preferences/languages.ftl",
+ "browser/preferences/permissions.ftl",
+ "browser/preferences/selectBookmark.ftl",
+ "browser/preferences/siteDataSettings.ftl",
+ "browser/aboutDialog.ftl",
+ "browser/sanitize.ftl",
+ "toolkit/updates/history.ftl",
+ "security/certificates/deviceManager.ftl",
+ "security/certificates/certManager.ftl",
+ ],
+ queries![
+ ("pref-page-title", "Preferences"),
+ ("category-list", L10nMessage::new(None, Some(vec![L10nAttribute::new("aria-label", "Categories")]))),
+ ("category-general", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "General")]))),
+ ("pane-general-title", "General"),
+ ("category-home", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Home")]))),
+ ("pane-home-title", "Home"),
+ ("category-search", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Search")]))),
+ ("pane-search-title", "Search"),
+ ("category-privacy", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Privacy & Security")]))),
+ ("pane-privacy-title", "Privacy & Security"),
+ ("category-sync2", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Sync")]))),
+ ("pane-sync-title2", "Sync"),
+ ("category-experimental", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Nightly Experiments")]))),
+ ("pane-experimental-title", "Nightly Experiments"),
+ ("addons-button-label", "Extensions & Themes"),
+ ("help-button-label", "Nightly Support"),
+ ("focus-search", L10nMessage::new(None, Some(vec![L10nAttribute::new("key", "f")]))),
+ ("managed-notice", "Your browser is being managed by your organization."),
+ ("search-input-box", L10nMessage::new(None, Some(vec![L10nAttribute::new("style", "width: 15.4em"), L10nAttribute::new("placeholder", "Find in Preferences")]))),
+ ("search-results-header", "Search Results"),
+ ("search-results-empty-message", "Sorry! There are no results in Preferences for “<span data-l10n-name=\"query\"></span>”."),
+ ("search-results-help-link", "Need help? Visit <a data-l10n-name=\"url\">Nightly Support</a>"),
+ ("containers-back-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("aria-label", "Back to Preferences")]))),
+ ("containers-header", "Container Tabs"),
+ ("containers-add-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Add New Container"), L10nAttribute::new("accesskey", "A")]))),
+ ("containers-new-tab-check", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Select a container for each new tab"), L10nAttribute::new("accesskey", "S")]))),
+ ("close-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("aria-label", "Close")]))),
+ ("pane-general-title", "General"),
+ ("startup-header", "Startup"),
+ ("startup-restore-previous-session", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Restore previous session"), L10nAttribute::new("accesskey", "s")]))),
+ ("startup-restore-warn-on-quit", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Warn you when quitting the browser")]))),
+ ("always-check-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always check if Nightly is your default browser"), L10nAttribute::new("accesskey", "y")]))),
+ ("is-not-default", "Nightly is not your default browser"),
+ ("set-as-my-default-browser", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Make Default…"), L10nAttribute::new("accesskey", "D")]))),
+ ("is-default", "Nightly is currently your default browser"),
+ ("tabs-group-header", "Tabs"),
+ ("ctrl-tab-recently-used-order", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Ctrl+Tab cycles through tabs in recently used order"), L10nAttribute::new("accesskey", "T")]))),
+ ("open-new-link-as-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open links in tabs instead of new windows"), L10nAttribute::new("accesskey", "w")]))),
+ ("warn-on-close-multiple-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Warn you when closing multiple tabs"), L10nAttribute::new("accesskey", "m")]))),
+ ("warn-on-open-many-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Warn you when opening multiple tabs might slow down Nightly"), L10nAttribute::new("accesskey", "d")]))),
+ ("switch-links-to-new-tabs", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "When you open a link in a new tab, switch to it immediately"), L10nAttribute::new("accesskey", "h")]))),
+ ("disable-extension", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Disable Extension")]))),
+ ("browser-containers-enabled", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Enable Container Tabs"), L10nAttribute::new("accesskey", "n")]))),
+ ("browser-containers-learn-more", "Learn more"),
+ ("browser-containers-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Settings…"), L10nAttribute::new("accesskey", "i")]))),
+ ("language-and-appearance-header", "Language and Appearance"),
+ ("fonts-and-colors-header", "Fonts and Colors"),
+ ("default-font", L10nMessage::new(Some("Default font"), Some(vec![L10nAttribute::new("accesskey", "D")]))),
+ ("default-font-size", L10nMessage::new(Some("Size"), Some(vec![L10nAttribute::new("accesskey", "S")]))),
+ ("advanced-fonts", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Advanced…"), L10nAttribute::new("accesskey", "A")]))),
+ ("colors-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Colors…"), L10nAttribute::new("accesskey", "C")]))),
+ ("preferences-zoom-header", "Zoom"),
+ ("preferences-default-zoom", L10nMessage::new(Some("Default zoom"), Some(vec![L10nAttribute::new("accesskey", "z")]))),
+ ("preferences-zoom-text-only", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Zoom text only"), L10nAttribute::new("accesskey", "t")]))),
+ ("language-header", "Language"),
+ ("choose-browser-language-description", "Choose the languages used to display menus, messages, and notifications from Nightly."),
+ ("manage-browser-languages-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Set Alternatives…"), L10nAttribute::new("accesskey", "l")]))),
+ ("choose-language-description", "Choose your preferred language for displaying pages"),
+ ("choose-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Choose…"), L10nAttribute::new("accesskey", "o")]))),
+ (L10nKey::new("use-system-locale", Some(vec![L10nArgument::new("localeName", "und")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use your operating system settings for “und” to format dates, times, numbers, and measurements.")]))),
+ ("translate-web-pages", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Translate web content"), L10nAttribute::new("accesskey", "T")]))),
+ ("translate-attribution", "Translations by <img data-l10n-name=\"logo\"/>"),
+ ("translate-exceptions", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Exceptions…"), L10nAttribute::new("accesskey", "x")]))),
+ ("check-user-spelling", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check your spelling as you type"), L10nAttribute::new("accesskey", "t")]))),
+ ("files-and-applications-title", "Files and Applications"),
+ ("download-header", "Downloads"),
+ ("download-save-to", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save files to"), L10nAttribute::new("accesskey", "v")]))),
+ ("download-choose-folder", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Browse…"), L10nAttribute::new("accesskey", "o")]))),
+ ("download-always-ask-where", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask you where to save files"), L10nAttribute::new("accesskey", "A")]))),
+ ("applications-header", "Applications"),
+ ("applications-description", "Choose how Nightly handles the files you download from the web or the applications you use while browsing."),
+ ("applications-filter", L10nMessage::new(None, Some(vec![L10nAttribute::new("placeholder", "Search file types or applications")]))),
+ ("applications-type-column", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Content Type"), L10nAttribute::new("accesskey", "T")]))),
+ ("applications-action-column", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Action"), L10nAttribute::new("accesskey", "A")]))),
+ ("drm-content-header", "Digital Rights Management (DRM) Content"),
+ ("play-drm-content", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Play DRM-controlled content"), L10nAttribute::new("accesskey", "P")]))),
+ ("play-drm-content-learn-more", "Learn more"),
+ ("update-application-title", "Nightly Updates"),
+ ("update-application-title", "Nightly Updates"),
+ ("update-application-description", "Keep Nightly up to date for the best performance, stability, and security."),
+ ("update-history", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show Update History…"), L10nAttribute::new("accesskey", "p")]))),
+ ("update-checkForUpdatesButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check for updates"), L10nAttribute::new("accesskey", "C")]))),
+ ("update-updateButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Restart to Update Nightly"), L10nAttribute::new("accesskey", "R")]))),
+ ("update-checkingForUpdates", "Checking for updates…"),
+ ("update-checkForUpdatesButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check for updates"), L10nAttribute::new("accesskey", "C")]))),
+ ("update-downloading", "<img data-l10n-name=\"icon\"/>Downloading update — <label data-l10n-name=\"download-status\"/>"),
+ ("update-applying", "Applying update…"),
+ ("update-failed-main", "Update failed. <a data-l10n-name=\"failed-link-main\">Download the latest version</a>"),
+ ("update-checkForUpdatesButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check for updates"), L10nAttribute::new("accesskey", "C")]))),
+ ("update-adminDisabled", "Updates disabled by your system administrator"),
+ ("update-checkForUpdatesButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check for updates"), L10nAttribute::new("accesskey", "C")]))),
+ ("update-noUpdatesFound", "Nightly is up to date"),
+ ("update-checkForUpdatesButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check for updates"), L10nAttribute::new("accesskey", "C")]))),
+ ("update-otherInstanceHandlingUpdates", "Nightly is being updated by another instance"),
+ ("update-checkForUpdatesButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check for updates"), L10nAttribute::new("accesskey", "C")]))),
+ ("update-manual", "Updates available at <label data-l10n-name=\"manual-link\"/>"),
+ ("update-checkForUpdatesButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check for updates"), L10nAttribute::new("accesskey", "C")]))),
+ ("update-unsupported", "You can not perform further updates on this system. <label data-l10n-name=\"unsupported-link\">Learn more</label>"),
+ ("update-checkForUpdatesButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check for updates"), L10nAttribute::new("accesskey", "C")]))),
+ ("update-restarting", "Restarting…"),
+ ("update-updateButton", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Restart to Update Nightly"), L10nAttribute::new("accesskey", "R")]))),
+ ("update-application-allow-description", "Allow Nightly to"),
+ ("update-application-auto", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Automatically install updates (recommended)"), L10nAttribute::new("accesskey", "A")]))),
+ ("update-application-check-choose", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Check for updates but let you choose to install them"), L10nAttribute::new("accesskey", "C")]))),
+ ("update-application-warning-cross-user-setting", "This setting will apply to all Windows accounts and Nightly profiles using this installation of Nightly."),
+ ("performance-title", "Performance"),
+ ("performance-title", "Performance"),
+ ("performance-use-recommended-settings-checkbox", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use recommended performance settings"), L10nAttribute::new("accesskey", "U")]))),
+ ("performance-settings-learn-more", "Learn more"),
+ ("performance-use-recommended-settings-desc", "These settings are tailored to your computer’s hardware and operating system."),
+ ("performance-allow-hw-accel", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use hardware acceleration when available"), L10nAttribute::new("accesskey", "r")]))),
+ ("performance-limit-content-process-option", L10nMessage::new(Some("Content process limit"), Some(vec![L10nAttribute::new("accesskey", "l")]))),
+ ("performance-limit-content-process-enabled-desc", "Additional content processes can improve performance when using multiple tabs, but will also use more memory."),
+ ("performance-limit-content-process-blocked-desc", "Modifying the number of content processes is only possible with multiprocess Nightly. <a data-l10n-name=\"learn-more\">Learn how to check if multiprocess is enabled</a>"),
+ ("browsing-title", "Browsing"),
+ ("browsing-title", "Browsing"),
+ ("browsing-use-autoscroll", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use autoscrolling"), L10nAttribute::new("accesskey", "a")]))),
+ ("browsing-use-smooth-scrolling", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use smooth scrolling"), L10nAttribute::new("accesskey", "m")]))),
+ ("browsing-use-cursor-navigation", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always use the cursor keys to navigate within pages"), L10nAttribute::new("accesskey", "k")]))),
+ ("browsing-search-on-start-typing", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Search for text when you start typing"), L10nAttribute::new("accesskey", "x")]))),
+ ("browsing-picture-in-picture-toggle-enabled", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Enable picture-in-picture video controls"), L10nAttribute::new("accesskey", "E")]))),
+ ("browsing-picture-in-picture-learn-more", "Learn more"),
+ ("browsing-media-control", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Control media via keyboard, headset, or virtual interface"), L10nAttribute::new("accesskey", "v")]))),
+ ("browsing-media-control-learn-more", "Learn more"),
+ ("browsing-cfr-recommendations", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Recommend extensions as you browse"), L10nAttribute::new("accesskey", "R")]))),
+ ("browsing-cfr-recommendations-learn-more", "Learn more"),
+ ("browsing-cfr-features", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Recommend features as you browse"), L10nAttribute::new("accesskey", "f")]))),
+ ("browsing-cfr-recommendations-learn-more", "Learn more"),
+ ("network-settings-title", "Network Settings"),
+ ("network-settings-title", "Network Settings"),
+ ("network-proxy-connection-learn-more", "Learn more"),
+ ("network-proxy-connection-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Settings…"), L10nAttribute::new("accesskey", "e")]))),
+ (L10nKey::new("performance-default-content-process-count", Some(vec![L10nArgument::new("num", "8")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "8 (default)")]))),
+ (L10nKey::new("update-application-version", Some(vec![L10nArgument::new("version", "86.0a1 (2020-12-27) (64-bit)")])), L10nMessage::new(Some("Version 86.0a1 (2020-12-27) (64-bit) <a data-l10n-name=\"learn-more\">What’s new</a>"), None)),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "30")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "30%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "50")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "50%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "67")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "67%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "80")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "80%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "90")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "90%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "100")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "100%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "110")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "110%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "120")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "120%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "133")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "133%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "150")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "150%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "170")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "170%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "200")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "200%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "240")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "240%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "300")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "300%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "400")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "400%")]))),
+ (L10nKey::new("preferences-default-zoom-value", Some(vec![L10nArgument::new("percentage", "500")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "500%")]))),
+ ("network-proxy-connection-description", "Configure how Nightly connects to the internet."),
+ (L10nKey::new("fonts-label-default", Some(vec![L10nArgument::new("name", "DejaVu Serif")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Default (DejaVu Serif)")]))),
+ ("applications-open-inapp-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-action-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save File")]))),
+ ("applications-use-os-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use system default application")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-open-inapp-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-action-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save File")]))),
+ ("applications-use-os-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use system default application")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-open-inapp-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-action-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save File")]))),
+ ("applications-use-os-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use system default application")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-open-inapp-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-action-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save File")]))),
+ ("applications-use-os-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use system default application")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-always-ask-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Always ask")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ (L10nKey::new("applications-use-app-default", Some(vec![L10nArgument::new("app-name", "Polari")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Polari (default)")]))),
+ (L10nKey::new("applications-use-app", Some(vec![L10nArgument::new("app-name", "Mibbit")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Mibbit")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-manage-app", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Application Details…")]))),
+ ("applications-always-ask-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Always ask")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ (L10nKey::new("applications-use-app", Some(vec![L10nArgument::new("app-name", "Mibbit")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Mibbit")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-manage-app", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Application Details…")]))),
+ (L10nKey::new("applications-use-app-default-label", Some(vec![L10nArgument::new("app-name", "Evolution")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Use Evolution (default)")]))),
+ (L10nKey::new("applications-use-app-default", Some(vec![L10nArgument::new("app-name", "Evolution")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Evolution (default)")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ (L10nKey::new("applications-use-app-default", Some(vec![L10nArgument::new("app-name", "Evolution")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Evolution (default)")]))),
+ (L10nKey::new("applications-use-app", Some(vec![L10nArgument::new("app-name", "Yahoo! Mail")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Yahoo! Mail")]))),
+ (L10nKey::new("applications-use-app", Some(vec![L10nArgument::new("app-name", "Gmail")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Gmail")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-manage-app", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Application Details…")]))),
+
+
+ ("applications-open-inapp-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-action-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save File")]))),
+ ("applications-use-os-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use system default application")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-open-inapp-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-action-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save File")]))),
+ ("applications-use-os-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use system default application")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-open-inapp-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-action-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save File")]))),
+ ("applications-use-os-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use system default application")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-open-inapp-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-open-inapp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open in Nightly")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-action-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save File")]))),
+ ("applications-use-os-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use system default application")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-always-ask-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Always ask")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ (L10nKey::new("applications-use-app-default", Some(vec![L10nArgument::new("app-name", "Polari")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Polari (default)")]))),
+ (L10nKey::new("applications-use-app", Some(vec![L10nArgument::new("app-name", "Mibbit")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Mibbit")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-manage-app", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Application Details…")]))),
+ ("applications-always-ask-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Always ask")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ (L10nKey::new("applications-use-app", Some(vec![L10nArgument::new("app-name", "Mibbit")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Mibbit")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-manage-app", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Application Details…")]))),
+ (L10nKey::new("applications-use-app-default-label", Some(vec![L10nArgument::new("app-name", "Evolution")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("value", "Use Evolution (default)")]))),
+ (L10nKey::new("applications-use-app-default", Some(vec![L10nArgument::new("app-name", "Evolution")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Evolution (default)")]))),
+ ("applications-always-ask", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always ask")]))),
+ (L10nKey::new("applications-use-app-default", Some(vec![L10nArgument::new("app-name", "Evolution")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Evolution (default)")]))),
+ (L10nKey::new("applications-use-app", Some(vec![L10nArgument::new("app-name", "Yahoo! Mail")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Yahoo! Mail")]))),
+ (L10nKey::new("applications-use-app", Some(vec![L10nArgument::new("app-name", "Gmail")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Gmail")]))),
+ ("applications-use-other", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use other…")]))),
+ ("applications-manage-app", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Application Details…")]))),
+ ("pane-home-title", "Home"),
+ ("home-restore-defaults", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Restore Defaults"), L10nAttribute::new("accesskey", "R")]))),
+ ("home-new-windows-tabs-header", "New Windows and Tabs"),
+ ("home-new-windows-tabs-description2", "Choose what you see when you open your homepage, new windows, and new tabs."),
+ ("home-homepage-mode-label", "Homepage and new windows"),
+ ("home-mode-choice-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Firefox Home (Default)")]))),
+ ("home-mode-choice-custom", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Custom URLs…")]))),
+ ("home-mode-choice-blank", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Blank Page")]))),
+ ("home-homepage-custom-url", L10nMessage::new(None, Some(vec![L10nAttribute::new("placeholder", "Paste a URL…")]))),
+ (L10nKey::new("use-current-pages", Some(vec![L10nArgument::new("tabCount", "0")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Current Pages"), L10nAttribute::new("accesskey", "C")]))),
+ ("choose-bookmark", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Bookmark…"), L10nAttribute::new("accesskey", "B")]))),
+ ("home-newtabs-mode-label", "New tabs"),
+ ("home-mode-choice-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Firefox Home (Default)")]))),
+ ("home-mode-choice-blank", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Blank Page")]))),
+ ("pane-search-title", "Search"),
+ ("search-bar-header", "Search Bar"),
+ ("search-bar-hidden", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use the address bar for search and navigation")]))),
+ ("search-bar-shown", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Add search bar in toolbar")]))),
+ ("search-engine-default-header", "Default Search Engine"),
+ ("search-engine-default-desc-2", "This is your default search engine in the address bar and search bar. You can switch it at any time."),
+ ("search-separate-default-engine", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use this search engine in Private Windows"), L10nAttribute::new("accesskey", "U")]))),
+ ("search-engine-default-private-desc-2", "Choose a different default search engine for Private Windows only"),
+ ("search-suggestions-header", "Search Suggestions"),
+ ("search-suggestions-desc", "Choose how suggestions from search engines appear."),
+ ("search-suggestions-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Provide search suggestions"), L10nAttribute::new("accesskey", "s")]))),
+ ("search-show-suggestions-url-bar-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show search suggestions in address bar results"), L10nAttribute::new("accesskey", "l")]))),
+ ("search-show-suggestions-above-history-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show search suggestions ahead of browsing history in address bar results")]))),
+ ("search-show-suggestions-private-windows", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show search suggestions in Private Windows")]))),
+ ("search-suggestions-cant-show", "Search suggestions will not be shown in location bar results because you have configured Nightly to never remember history."),
+ ("suggestions-addressbar-settings-generic", "Change preferences for other address bar suggestions"),
+ ("search-one-click-header2", "Search Shortcuts"),
+ ("search-one-click-desc", "Choose the alternative search engines that appear below the address bar and search bar when you start to enter a keyword."),
+ ("search-choose-engine-column", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Search Engine")]))),
+ ("search-choose-keyword-column", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Keyword")]))),
+ ("search-restore-default", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Restore Default Search Engines"), L10nAttribute::new("accesskey", "D")]))),
+ ("search-remove-engine", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove"), L10nAttribute::new("accesskey", "R")]))),
+ ("search-add-engine", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Add"), L10nAttribute::new("accesskey", "A")]))),
+ ("search-find-more-link", "Find more search engines"),
+ ("privacy-header", "Browser Privacy"),
+ ("content-blocking-enhanced-tracking-protection", "Enhanced Tracking Protection"),
+ ("content-blocking-section-top-level-description", "Trackers follow you around online to collect information about your browsing habits and interests. Nightly blocks many of these trackers and other malicious scripts."),
+ ("content-blocking-learn-more", "Learn more"),
+ ("tracking-manage-exceptions", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Manage Exceptions…"), L10nAttribute::new("accesskey", "x")]))),
+ ("content-blocking-fpi-incompatibility-warning", "You are using First Party Isolation (FPI), which overrides some of Nightly’s cookie settings."),
+ ("enhanced-tracking-protection-setting-standard", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Standard"), L10nAttribute::new("accesskey", "d")]))),
+ ("content-blocking-expand-section", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "More information")]))),
+ ("content-blocking-etp-standard-desc", "Balanced for protection and performance. Pages will load normally."),
+ ("content-blocking-social-media-trackers", "Social media trackers"),
+ ("content-blocking-cross-site-cookies", "Cross-site cookies"),
+ ("content-blocking-cross-site-tracking-cookies", "Cross-site tracking cookies"),
+ ("content-blocking-cross-site-tracking-cookies-plus-isolate", "Cross-site tracking cookies, and isolate remaining cookies"),
+ ("content-blocking-private-windows", "Tracking content in Private Windows"),
+ ("content-blocking-all-windows-tracking-content", "Tracking content in all windows"),
+ ("content-blocking-all-third-party-cookies", "All third-party cookies"),
+ ("content-blocking-all-cookies", "All cookies"),
+ ("content-blocking-unvisited-cookies", "Cookies from unvisited sites"),
+ ("content-blocking-cryptominers", "Cryptominers"),
+ ("content-blocking-fingerprinters", "Fingerprinters"),
+ ("content-blocking-reload-description", "You will need to reload your tabs to apply these changes."),
+ ("content-blocking-reload-tabs-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Reload All Tabs"), L10nAttribute::new("accesskey", "R")]))),
+ ("enhanced-tracking-protection-setting-strict", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Strict"), L10nAttribute::new("accesskey", "r")]))),
+ ("content-blocking-expand-section", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "More information")]))),
+ ("content-blocking-etp-strict-desc", "Stronger protection, but may cause some sites or content to break."),
+ ("content-blocking-social-media-trackers", "Social media trackers"),
+ ("content-blocking-cross-site-tracking-cookies", "Cross-site tracking cookies"),
+ ("content-blocking-cross-site-cookies", "Cross-site cookies"),
+ ("content-blocking-private-windows", "Tracking content in Private Windows"),
+ ("content-blocking-all-windows-tracking-content", "Tracking content in all windows"),
+ ("content-blocking-all-third-party-cookies", "All third-party cookies"),
+ ("content-blocking-all-cookies", "All cookies"),
+ ("content-blocking-unvisited-cookies", "Cookies from unvisited sites"),
+ ("content-blocking-cross-site-tracking-cookies", "Cross-site tracking cookies"),
+ ("content-blocking-cross-site-tracking-cookies-plus-isolate", "Cross-site tracking cookies, and isolate remaining cookies"),
+ ("content-blocking-cryptominers", "Cryptominers"),
+ ("content-blocking-fingerprinters", "Fingerprinters"),
+ ("content-blocking-reload-description", "You will need to reload your tabs to apply these changes."),
+ ("content-blocking-reload-tabs-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Reload All Tabs"), L10nAttribute::new("accesskey", "R")]))),
+ ("content-blocking-warning-title", "Heads up!"),
+ ("content-blocking-and-isolating-etp-warning-description", "Blocking trackers and isolating cookies could impact the functionality of some sites. Reload a page with trackers to load all content."),
+ ("content-blocking-warning-learn-how", "Learn how"),
+ ("enhanced-tracking-protection-setting-custom", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Custom"), L10nAttribute::new("accesskey", "C")]))),
+ ("content-blocking-expand-section", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "More information")]))),
+ ("content-blocking-etp-custom-desc", "Choose which trackers and scripts to block."),
+ ("content-blocking-cookies-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cookies"), L10nAttribute::new("accesskey", "C")]))),
+ ("sitedata-option-block-cross-site-trackers", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cross-site trackers")]))),
+ ("sitedata-option-block-cross-site-and-social-media-trackers-plus-isolate", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cross-site and social media trackers, and isolate remaining cookies")]))),
+ ("sitedata-option-block-unvisited", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cookies from unvisited websites")]))),
+ ("sitedata-option-block-all-third-party", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "All third-party cookies (may cause websites to break)")]))),
+ ("sitedata-option-block-all", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "All cookies (will cause websites to break)")]))),
+ ("disable-extension", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Disable Extension")]))),
+ ("content-blocking-tracking-content-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Tracking content"), L10nAttribute::new("accesskey", "T")]))),
+ ("content-blocking-option-private", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Only in Private Windows"), L10nAttribute::new("accesskey", "p")]))),
+ ("content-blocking-tracking-protection-option-all-windows", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "In all windows"), L10nAttribute::new("accesskey", "A")]))),
+ ("content-blocking-tracking-protection-change-block-list", "Change block list"),
+ ("content-blocking-cryptominers-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cryptominers"), L10nAttribute::new("accesskey", "y")]))),
+ ("content-blocking-fingerprinters-label", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Fingerprinters"), L10nAttribute::new("accesskey", "F")]))),
+ ("content-blocking-reload-description", "You will need to reload your tabs to apply these changes."),
+ ("content-blocking-reload-tabs-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Reload All Tabs"), L10nAttribute::new("accesskey", "R")]))),
+ ("content-blocking-warning-title", "Heads up!"),
+ ("content-blocking-and-isolating-etp-warning-description", "Blocking trackers and isolating cookies could impact the functionality of some sites. Reload a page with trackers to load all content."),
+ ("content-blocking-warning-learn-how", "Learn how"),
+ ("do-not-track-description", "Send websites a “Do Not Track” signal that you don’t want to be tracked"),
+ ("do-not-track-learn-more", "Learn more"),
+ ("do-not-track-option-always", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always")]))),
+ ("do-not-track-option-default-content-blocking-known", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Only when Nightly is set to block known trackers")]))),
+ ("sitedata-header", "Cookies and Site Data"),
+ ("sitedata-learn-more", "Learn more"),
+ ("sitedata-delete-on-close-private-browsing", "In permanent private browsing mode, cookies and site data will always be cleared when Nightly is closed."),
+ ("sitedata-delete-on-close", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Delete cookies and site data when Nightly is closed"), L10nAttribute::new("accesskey", "c")]))),
+ ("sitedata-clear", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Clear Data…"), L10nAttribute::new("accesskey", "l")]))),
+ ("sitedata-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Manage Data…"), L10nAttribute::new("accesskey", "M")]))),
+ ("sitedata-cookies-exceptions", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Manage Exceptions…"), L10nAttribute::new("accesskey", "x")]))),
+ ("pane-privacy-logins-and-passwords-header", L10nMessage::new(Some("Logins and Passwords"), Some(vec![L10nAttribute::new("searchkeywords", "Lockwise")]))),
+ ("disable-extension", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Disable Extension")]))),
+ ("forms-ask-to-save-logins", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Ask to save logins and passwords for websites"), L10nAttribute::new("accesskey", "r")]))),
+ ("forms-fill-logins-and-passwords", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Autofill logins and passwords"), L10nAttribute::new("accesskey", "i")]))),
+ ("forms-generate-passwords", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Suggest and generate strong passwords"), L10nAttribute::new("accesskey", "u")]))),
+ ("forms-exceptions", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Exceptions…"), L10nAttribute::new("accesskey", "x")]))),
+ ("forms-saved-logins", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Saved Logins…"), L10nAttribute::new("accesskey", "L")]))),
+ ("forms-breach-alerts", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Show alerts about passwords for breached websites"), L10nAttribute::new("accesskey", "b")]))),
+ ("forms-breach-alerts-learn-more-link", "Learn more"),
+ ("forms-primary-pw-use", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use a Primary Password"), L10nAttribute::new("accesskey", "U")]))),
+ ("forms-primary-pw-learn-more-link", "Learn more"),
+ ("forms-primary-pw-change", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Change Primary Password…"), L10nAttribute::new("accesskey", "P")]))),
+ ("forms-primary-pw-former-name", "Formerly known as Master Password"),
+ ("forms-primary-pw-fips-title", "You are currently in FIPS mode. FIPS requires a non-empty Primary Password."),
+ ("forms-master-pw-fips-desc", "Password Change Failed"),
+ ("history-header", "History"),
+ ("history-remember-label", L10nMessage::new(Some("Nightly will"), Some(vec![L10nAttribute::new("accesskey", "w")]))),
+ ("history-remember-option-all", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remember history")]))),
+ ("history-remember-option-never", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Never remember history")]))),
+ ("history-remember-option-custom", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use custom settings for history")]))),
+ ("history-remember-description", "Nightly will remember your browsing, download, form, and search history."),
+ ("history-dontremember-description", "Nightly will use the same settings as private browsing, and will not remember any history as you browse the Web."),
+ ("history-private-browsing-permanent", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Always use private browsing mode"), L10nAttribute::new("accesskey", "p")]))),
+ ("history-remember-browser-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remember browsing and download history"), L10nAttribute::new("accesskey", "b")]))),
+ ("history-remember-search-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remember search and form history"), L10nAttribute::new("accesskey", "f")]))),
+ ("history-clear-on-close-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Clear history when Nightly closes"), L10nAttribute::new("accesskey", "r")]))),
+ ("history-clear-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Clear History…"), L10nAttribute::new("accesskey", "s")]))),
+ ("history-clear-on-close-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Settings…"), L10nAttribute::new("accesskey", "t")]))),
+ ("addressbar-header", "Address Bar"),
+ ("addressbar-suggest", "When using the address bar, suggest"),
+ ("addressbar-locbar-history-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Browsing history"), L10nAttribute::new("accesskey", "h")]))),
+ ("addressbar-locbar-bookmarks-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmarks"), L10nAttribute::new("accesskey", "k")]))),
+ ("addressbar-locbar-openpage-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Open tabs"), L10nAttribute::new("accesskey", "O")]))),
+ ("addressbar-locbar-topsites-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Top sites"), L10nAttribute::new("accesskey", "T")]))),
+ ("addressbar-locbar-engines-option", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Search engines"), L10nAttribute::new("accesskey", "a")]))),
+ ("addressbar-suggestions-settings", "Change preferences for search engine suggestions"),
+ ("permissions-header", "Permissions"),
+ ("permissions-header", "Permissions"),
+ ("permissions-location", "Location"),
+ ("permissions-location-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Settings…"), L10nAttribute::new("accesskey", "t")]))),
+ ("permissions-camera", "Camera"),
+ ("permissions-camera-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Settings…"), L10nAttribute::new("accesskey", "t")]))),
+ ("permissions-microphone", "Microphone"),
+ ("permissions-microphone-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Settings…"), L10nAttribute::new("accesskey", "t")]))),
+ ("permissions-notification", "Notifications"),
+ ("permissions-notification-link", "Learn more"),
+ ("permissions-notification-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Settings…"), L10nAttribute::new("accesskey", "t")]))),
+ ("permissions-autoplay", "Autoplay"),
+ ("permissions-autoplay-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Settings…"), L10nAttribute::new("accesskey", "t")]))),
+ ("permissions-xr", "Virtual Reality"),
+ ("permissions-xr-settings", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Settings…"), L10nAttribute::new("accesskey", "t")]))),
+ ("permissions-block-popups", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Block pop-up windows"), L10nAttribute::new("accesskey", "B")]))),
+ ("permissions-block-popups-exceptions", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Exceptions…"), L10nAttribute::new("accesskey", "E")]))),
+ ("permissions-addon-install-warning", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Warn you when websites try to install add-ons"), L10nAttribute::new("accesskey", "W")]))),
+ ("permissions-addon-exceptions", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Exceptions…"), L10nAttribute::new("accesskey", "E")]))),
+ ("collection-header", "Nightly Data Collection and Use"),
+ ("collection-header", "Nightly Data Collection and Use"),
+ ("collection-description", "We strive to provide you with choices and collect only what we need to provide and improve Nightly for everyone. We always ask permission before receiving personal information."),
+ ("collection-privacy-notice", "Privacy Notice"),
+ ("collection-health-report-telemetry-disabled", "You’re no longer allowing Mozilla to capture technical and interaction data. All past data will be deleted within 30 days."),
+ ("collection-health-report-telemetry-disabled-link", "Learn more"),
+ ("collection-health-report", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Allow Nightly to send technical and interaction data to Mozilla"), L10nAttribute::new("accesskey", "r")]))),
+ ("collection-health-report-link", "Learn more"),
+ ("addon-recommendations", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Allow Nightly to make personalized extension recommendations")]))),
+ ("addon-recommendations-link", "Learn more"),
+ ("collection-health-report-disabled", "Data reporting is disabled for this build configuration"),
+ ("collection-studies", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Allow Nightly to install and run studies")]))),
+ ("collection-studies-link", "View Nightly studies"),
+ ("collection-backlogged-crash-reports", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Allow Nightly to send backlogged crash reports on your behalf"), L10nAttribute::new("accesskey", "c")]))),
+ ("collection-backlogged-crash-reports-link", "Learn more"),
+ ("security-header", "Security"),
+ ("security-browsing-protection", "Deceptive Content and Dangerous Software Protection"),
+ ("security-enable-safe-browsing", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Block dangerous and deceptive content"), L10nAttribute::new("accesskey", "B")]))),
+ ("security-enable-safe-browsing-link", "Learn more"),
+ ("security-block-downloads", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Block dangerous downloads"), L10nAttribute::new("accesskey", "d")]))),
+ ("security-block-uncommon-software", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Warn you about unwanted and uncommon software"), L10nAttribute::new("accesskey", "c")]))),
+ ("certs-header", "Certificates"),
+ ("certs-enable-ocsp", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Query OCSP responder servers to confirm the current validity of certificates"), L10nAttribute::new("accesskey", "Q")]))),
+ ("certs-view", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "View Certificates…"), L10nAttribute::new("accesskey", "C")]))),
+ ("certs-devices", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Security Devices…"), L10nAttribute::new("accesskey", "D")]))),
+ ("httpsonly-header", "HTTPS-Only Mode"),
+ ("httpsonly-description", "HTTPS provides a secure, encrypted connection between Nightly and the websites you visit. Most websites support HTTPS, and if HTTPS-Only Mode is enabled, then Nightly will upgrade all connections to HTTPS."),
+ ("httpsonly-learn-more", "Learn more"),
+ ("httpsonly-radio-enabled", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Enable HTTPS-Only Mode in all windows")]))),
+ ("httpsonly-radio-enabled-pbm", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Enable HTTPS-Only Mode in private windows only")]))),
+ ("httpsonly-radio-disabled", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Don’t enable HTTPS-Only Mode")]))),
+ ("pane-experimental-title", "Nightly Experiments"),
+ ("pane-experimental-subtitle", "Proceed with Caution"),
+ ("pane-experimental-reset", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Restore Defaults"), L10nAttribute::new("accesskey", "R")]))),
+ ("pane-experimental-search-results-header", "Nightly Experiments: Proceed with Caution"),
+ ("pane-experimental-description", "Changing advanced configuration preferences can impact Nightly performance or security."),
+ ("pane-sync-title2", "Sync"),
+ ("sync-signedout-caption", "Take Your Web With You"),
+ ("sync-signedout-description", "Synchronize your bookmarks, history, tabs, passwords, add-ons, and preferences across all your devices."),
+ ("sync-signedout-account-signin2", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Sign in to Sync…"), L10nAttribute::new("accesskey", "i")]))),
+ ("sync-mobile-promo", "Download Firefox for <img data-l10n-name=\"android-icon\"/> <a data-l10n-name=\"android-link\">Android</a> or <img data-l10n-name=\"ios-icon\"/> <a data-l10n-name=\"ios-link\">iOS</a> to sync with your mobile device."),
+ ("pane-sync-title2", "Sync"),
+ ("sync-profile-picture", L10nMessage::new(None, Some(vec![L10nAttribute::new("tooltiptext", "Change profile picture")]))),
+ ("sync-sign-out", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Sign Out…"), L10nAttribute::new("accesskey", "g")]))),
+ ("sync-manage-account", L10nMessage::new(Some("Manage account"), Some(vec![L10nAttribute::new("accesskey", "o")]))),
+ (L10nKey::new("sync-signedin-unverified", Some(vec![L10nArgument::new("email", "")])), L10nMessage::new(Some(" is not verified."), None)),
+ ("sync-resend-verification", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Resend Verification"), L10nAttribute::new("accesskey", "d")]))),
+ ("sync-remove-account", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove Account"), L10nAttribute::new("accesskey", "R")]))),
+ (L10nKey::new("sync-signedin-login-failure", Some(vec![L10nArgument::new("email", "")])), L10nMessage::new(Some("Please sign in to reconnect "), None)),
+ ("sync-sign-in", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Sign in"), L10nAttribute::new("accesskey", "g")]))),
+ ("sync-remove-account", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove Account"), L10nAttribute::new("accesskey", "R")]))),
+ ("sync-device-name-header", "Device Name"),
+ ("sync-device-name-change", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Change Device Name…"), L10nAttribute::new("accesskey", "h")]))),
+ ("sync-device-name-cancel", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cancel"), L10nAttribute::new("accesskey", "n")]))),
+ ("sync-device-name-save", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Save"), L10nAttribute::new("accesskey", "v")]))),
+ ("prefs-syncing-off", "Syncing: OFF"),
+ ("prefs-sync-offer-setup-label", "Synchronize your bookmarks, history, tabs, passwords, add-ons, and preferences across all your devices."),
+ ("prefs-sync-setup", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Set Up Sync…"), L10nAttribute::new("accesskey", "S")]))),
+ ("prefs-syncing-on", "Syncing: ON"),
+ ("prefs-sync-now", L10nMessage::new(None, Some(vec![L10nAttribute::new("labelnotsyncing", "Sync Now"), L10nAttribute::new("accesskeynotsyncing", "N"), L10nAttribute::new("labelsyncing", "Syncing…")]))),
+ ("sync-currently-syncing-heading", "You are currently syncing these items:"),
+ ("sync-currently-syncing-bookmarks", "Bookmarks"),
+ ("sync-currently-syncing-history", "History"),
+ ("sync-currently-syncing-tabs", "Open tabs"),
+ ("sync-currently-syncing-logins-passwords", "Logins and passwords"),
+ ("sync-currently-syncing-addresses", "Addresses"),
+ ("sync-currently-syncing-creditcards", "Credit cards"),
+ ("sync-currently-syncing-addons", "Add-ons"),
+ ("sync-currently-syncing-prefs", "Preferences"),
+ ("sync-change-options", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Change…"), L10nAttribute::new("accesskey", "C")]))),
+ ("sync-connect-another-device", "Connect another device"),
+ ("experimental-features-abouthome-startup-cache", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "about:home startup cache")]))),
+ ("experimental-features-cookie-samesite-lax-by-default2", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cookies: SameSite=Lax by default")]))),
+ ("experimental-features-cookie-samesite-none-requires-secure2", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cookies: SameSite=None requires secure attribute")]))),
+ ("experimental-features-cookie-samesite-schemeful", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cookies: Schemeful SameSite")]))),
+ ("experimental-features-css-constructable-stylesheets", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "CSS: Constructable Stylesheets")]))),
+ ("experimental-features-css-focus-visible", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "CSS: Pseudo-class: :focus-visible")]))),
+ ("experimental-features-css-masonry2", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "CSS: Masonry Layout")]))),
+ ("experimental-features-devtools-color-scheme-simulation", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Developer Tools: Color Scheme Simulation")]))),
+ ("experimental-features-devtools-compatibility-panel", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Developer Tools: Compatibility Panel")]))),
+ ("experimental-features-devtools-execution-context-selector", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Developer Tools: Execution Context Selector")]))),
+ ("experimental-features-devtools-serviceworker-debugger-support", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Developer Tools: Service Worker debugging")]))),
+ ("experimental-features-fission", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Fission (Site Isolation)")]))),
+ ("experimental-features-http3", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "HTTP/3 protocol")]))),
+ ("experimental-features-media-avif", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Media: AVIF")]))),
+ ("experimental-features-multi-pip", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Multiple Picture-in-Picture Support")]))),
+ ("experimental-features-print-preview-tab-modal", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Print Preview Redesign")]))),
+ ("experimental-features-web-api-beforeinput", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web API: beforeinput Event")]))),
+ ("experimental-features-web-api-inputmode", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web API: inputmode")]))),
+ ("experimental-features-web-api-link-preload", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web API: <link rel=\"preload\">")]))),
+ ("experimental-features-web-gpu2", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web API: WebGPU")]))),
+ ("experimental-features-webrtc-global-mute-toggles", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "WebRTC Global Mute Toggles")]))),
+ (L10nKey::new("use-current-pages", Some(vec![L10nArgument::new("tabCount", "1")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Use Current Page"), L10nAttribute::new("accesskey", "C")]))),
+ ("home-prefs-content-header", "Firefox Home Content"),
+ ("home-prefs-content-description", "Choose what content you want on your Firefox Home screen."),
+ ("home-prefs-search-header", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web Search")]))),
+ ("home-prefs-topsites-header", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Top Sites")]))),
+ ("home-prefs-topsites-description", "The sites you visit most"),
+ (L10nKey::new("home-prefs-sections-rows-option", Some(vec![L10nArgument::new("num", "1")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "1 row")]))),
+ (L10nKey::new("home-prefs-sections-rows-option", Some(vec![L10nArgument::new("num", "2")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "2 rows")]))),
+ (L10nKey::new("home-prefs-sections-rows-option", Some(vec![L10nArgument::new("num", "3")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "3 rows")]))),
+ (L10nKey::new("home-prefs-sections-rows-option", Some(vec![L10nArgument::new("num", "4")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "4 rows")]))),
+ (L10nKey::new("home-prefs-recommended-by-header", Some(vec![L10nArgument::new("provider", "Pocket")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Recommended by Pocket")]))),
+ ("home-prefs-recommended-by-learn-more", "How it works"),
+ (L10nKey::new("home-prefs-recommended-by-description-update", Some(vec![L10nArgument::new("provider", "Pocket")])), L10nMessage::new(Some("Exceptional content from across the web, curated by Pocket"), None)),
+ ("home-prefs-recommended-by-option-sponsored-stories", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Sponsored Stories")]))),
+ ("home-prefs-highlights-header", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Highlights")]))),
+ ("home-prefs-highlights-description", "A selection of sites that you’ve saved or visited"),
+ (L10nKey::new("home-prefs-sections-rows-option", Some(vec![L10nArgument::new("num", "1")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "1 row")]))),
+ (L10nKey::new("home-prefs-sections-rows-option", Some(vec![L10nArgument::new("num", "2")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "2 rows")]))),
+ (L10nKey::new("home-prefs-sections-rows-option", Some(vec![L10nArgument::new("num", "3")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "3 rows")]))),
+ (L10nKey::new("home-prefs-sections-rows-option", Some(vec![L10nArgument::new("num", "4")])), L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "4 rows")]))),
+ ("home-prefs-highlights-option-visited-pages", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Visited Pages")]))),
+ ("home-prefs-highlights-options-bookmarks", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Bookmarks")]))),
+ ("home-prefs-highlights-option-most-recent-download", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Most Recent Download")]))),
+ ("home-prefs-highlights-option-saved-to-pocket", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Pages Saved to Pocket")]))),
+ ("home-prefs-snippets-header", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Snippets")]))),
+ ("home-prefs-snippets-description", "Updates from Mozilla and Firefox"),
+ ("sitedata-option-block-cross-site-and-social-media-trackers", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cross-site and social media trackers")]))),
+ ("sitedata-total-size-calculating", "Calculating site data and cache size…"),
+ ("containers-preferences-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Preferences")]))),
+ ("containers-remove-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove")]))),
+ ("containers-preferences-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Preferences")]))),
+ ("containers-remove-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove")]))),
+ ("containers-preferences-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Preferences")]))),
+ ("containers-remove-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove")]))),
+ ("containers-preferences-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Preferences")]))),
+ ("containers-remove-button", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Remove")]))),
+ ("experimental-features-abouthome-startup-cache", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "about:home startup cache")]))),
+ ("experimental-features-abouthome-startup-cache-description", "A cache for the initial about:home document that is loaded by default at startup. The purpose of the cache is to improve startup performance."),
+ ("experimental-features-cookie-samesite-lax-by-default2", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cookies: SameSite=Lax by default")]))),
+ ("experimental-features-cookie-samesite-lax-by-default2-description", "Treat cookies as “SameSite=Lax” by default if no “SameSite” attribute is specified. Developers must opt-in to the current status quo of unrestricted use by explicitly asserting “SameSite=None”."),
+ ("experimental-features-cookie-samesite-none-requires-secure2", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cookies: SameSite=None requires secure attribute")]))),
+ ("experimental-features-cookie-samesite-none-requires-secure2-description", "Cookies with “SameSite=None” attribute require the secure attribute. This feature requires “Cookies: SameSite=Lax by default”."),
+ ("experimental-features-cookie-samesite-schemeful", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Cookies: Schemeful SameSite")]))),
+ ("experimental-features-cookie-samesite-schemeful-description", "Treat cookies from the same domain, but with different schemes (e.g. http://example.com and https://example.com) as cross-site instead of same-site. Improves security, but potentially introduces breakage."),
+ ("experimental-features-css-constructable-stylesheets", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "CSS: Constructable Stylesheets")]))),
+ ("experimental-features-css-constructable-stylesheets-description", "The addition of a constructor to the <a data-l10n-name=\"mdn-cssstylesheet\">CSSStyleSheet</a> interface as well as a variety of related changes makes it possible to directly create new stylesheets without having to add the sheet to the HTML. This makes it much easier to create reusable stylesheets for use with <a data-l10n-name=\"mdn-shadowdom\">Shadow DOM</a>. See <a data-l10n-name=\"bugzilla\">bug 1520690</a> for more details."),
+ ("experimental-features-css-masonry2", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "CSS: Masonry Layout")]))),
+ ("experimental-features-css-masonry-description", "Enables support for the experimental CSS Masonry Layout feature. See the <a data-l10n-name=\"explainer\">explainer</a> for a high level description of the feature. To provide feedback, please comment in <a data-l10n-name=\"w3c-issue\">this GitHub issue</a> or <a data-l10n-name=\"bug\">this bug</a>."),
+ ("experimental-features-css-focus-visible", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "CSS: Pseudo-class: :focus-visible")]))),
+ ("experimental-features-css-focus-visible-description", "Allows focus styles to be applied to elements like buttons and form controls, only when they are focused using the keyboard (e.g. when tabbing between elements), and not when they are focused using a mouse or other pointing device. See <a data-l10n-name=\"bugzilla\">bug 1617600</a> for more details."),
+ ("experimental-features-devtools-color-scheme-simulation", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Developer Tools: Color Scheme Simulation")]))),
+ ("experimental-features-devtools-color-scheme-simulation-description", "Adds an option to simulate different color schemes allowing you to test <a data-l10n-name=\"mdn-preferscolorscheme\">@prefers-color-scheme</a> media queries. Using this media query lets your stylesheet respond to whether the user prefers a light or dark user interface. This feature lets you test your code without having to change settings in your browser (or operating system, if the browser follows a system-wide color scheme setting). See <a data-l10n-name=\"bugzilla1\">bug 1550804</a> and <a data-l10n-name=\"bugzilla2\">bug 1137699</a> for more details."),
+ ("experimental-features-devtools-compatibility-panel", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Developer Tools: Compatibility Panel")]))),
+ ("experimental-features-devtools-compatibility-panel-description", "A side panel for the Page Inspector that shows you information detailing your app’s cross-browser compatibility status. See <a data-l10n-name=\"bugzilla\">bug 1584464</a> for more details."),
+ ("experimental-features-devtools-execution-context-selector", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Developer Tools: Execution Context Selector")]))),
+ ("experimental-features-devtools-execution-context-selector-description", "This feature displays a button on the console’s command line that lets you change the context in which the expression you enter will be executed. See <a data-l10n-name=\"bugzilla1\">bug 1605154</a> and <a data-l10n-name=\"bugzilla2\">bug 1605153</a> for more details."),
+ ("experimental-features-devtools-serviceworker-debugger-support", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Developer Tools: Service Worker debugging")]))),
+ ("experimental-features-devtools-serviceworker-debugger-support-description", "Enables experimental support for Service Workers in the Debugger panel. This feature may slow the Developer Tools down and increase memory consumption."),
+ ("experimental-features-fission", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Fission (Site Isolation)")]))),
+ ("experimental-features-fission-description", "Fission (site isolation) is an experimental feature in Nightly to provide an additional layer of defense against security bugs. By isolating each site into a separate process, Fission makes it harder for malicious websites to get access to information from other pages you are visiting. This is a major architectural change in Nightly and we appreciate you testing and reporting any issues you might encounter. For more details, see <a data-l10n-name=\"wiki\">the wiki</a>."),
+ ("experimental-features-http3", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "HTTP/3 protocol")]))),
+ ("experimental-features-http3-description", "Experimental support for the HTTP/3 protocol."),
+ ("experimental-features-media-avif", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Media: AVIF")]))),
+ ("experimental-features-media-avif-description", "With this feature enabled, Nightly supports the AV1 Image File (AVIF) format. This is a still image file format that leverages the capabilities of the AV1 video compression algorithms to reduce image size. See <a data-l10n-name=\"bugzilla\">bug 1443863</a> for more details."),
+ ("experimental-features-multi-pip", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Multiple Picture-in-Picture Support")]))),
+ ("experimental-features-multi-pip-description", "Experimental support for allowing multiple Picture-in-Picture windows to be open at the same time."),
+ ("experimental-features-print-preview-tab-modal", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Print Preview Redesign")]))),
+ ("experimental-features-print-preview-tab-modal-description", "Introduces the redesigned print preview and makes print preview available on macOS. This potentially introduces breakage and does not include all print-related settings. To access all print-related settings, select “Print using the system dialog…” from within the Print panel."),
+ ("experimental-features-web-api-link-preload", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web API: <link rel=\"preload\">")]))),
+ ("experimental-features-web-api-link-preload-description", "The <a data-l10n-name=\"rel\">rel</a> attribute with value <code>\"preload\"</code> on a <a data-l10n-name=\"link\">&lt;link&gt;</a> element is intended to help provide performance gains by letting you download resources earlier in the page lifecycle, ensuring that they’re available earlier and are less likely to block page rendering. Read <a data-l10n-name=\"readmore\">“Preloading content with <code>rel=\"preload\"</code>”</a> or see <a data-l10n-name=\"bugzilla\">bug 1583604</a> for more details."),
+ ("experimental-features-web-api-beforeinput", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web API: beforeinput Event")]))),
+ ("experimental-features-web-api-beforeinput-description", "The global <a data-l10n-name=\"mdn-beforeinput\">beforeinput</a> event is fired on an <a data-l10n-name=\"mdn-input\">&lt;input&gt;</a> and <a data-l10n-name=\"mdn-textarea\">&lt;textarea&gt;</a> elements, or any element whose <a data-l10n-name=\"mdn-contenteditable\">contenteditable</a> attribute is enabled, immediately before the element’s value changes. The event allows web apps to override the browser’s default behavior for user interaction, e.g., web apps can cancel user input only for specific characters or can modify pasting styled text only with approved styles."),
+ ("experimental-features-web-api-inputmode", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web API: inputmode")]))),
+ ("experimental-features-web-api-inputmode-description", "Our implementation of the <a data-l10n-name=\"mdn-inputmode\">inputmode</a> global attribute has been updated as per <a data-l10n-name=\"whatwg\">the WHATWG specification</a>, but we still need to make other changes too, like making it available on contenteditable content. See <a data-l10n-name=\"bugzilla\">bug 1205133</a> for more details."),
+ ("experimental-features-web-gpu2", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "Web API: WebGPU")]))),
+ ("experimental-features-web-gpu-description2", "This new API provides low-level support for performing computation and graphics rendering using the <a data-l10n-name=\"wikipedia\">Graphics Processing Unit (GPU)</a> of the user’s device or computer. The <a data-l10n-name=\"spec\">specification</a> is still a work-in-progress. See <a data-l10n-name=\"bugzilla\">bug 1602129</a> for more details."),
+ ("experimental-features-webrtc-global-mute-toggles", L10nMessage::new(None, Some(vec![L10nAttribute::new("label", "WebRTC Global Mute Toggles")]))),
+ ("experimental-features-webrtc-global-mute-toggles-description", "Add controls to the WebRTC global sharing indicator that allow users to globally mute their microphone and camera feeds."),
+ (L10nKey::new("sitedata-total-size", Some(vec![L10nArgument::new("value", "2.0"), L10nArgument::new("unit", "MB")])), L10nMessage::new(Some("Your stored cookies, site data, and cache are currently using 2.0 MB of disk space."), None))
+ ],
+ )
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/simple.rs b/third_party/rust/fluent-testing/src/scenarios/simple.rs
new file mode 100644
index 0000000000..aff4c0d313
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/simple.rs
@@ -0,0 +1,16 @@
+use super::structs::*;
+use crate::queries;
+
+pub fn get_scenario() -> Scenario {
+ Scenario::new(
+ "simple",
+ vec![FileSource::new(
+ "browser",
+ "browser/{locale}/",
+ vec!["en-US", "pl"],
+ )],
+ vec!["en-US"],
+ vec!["browser/sanitize.ftl"],
+ queries![("history-section-label", "History")],
+ )
+}
diff --git a/third_party/rust/fluent-testing/src/scenarios/structs.rs b/third_party/rust/fluent-testing/src/scenarios/structs.rs
new file mode 100644
index 0000000000..7ba2551c13
--- /dev/null
+++ b/third_party/rust/fluent-testing/src/scenarios/structs.rs
@@ -0,0 +1,291 @@
+use fluent_fallback::types::ResourceId;
+
+pub struct FileSource {
+ pub name: String,
+ pub locales: Vec<String>,
+ pub path_scheme: String,
+}
+
+impl Default for FileSource {
+ fn default() -> Self {
+ Self {
+ name: "default".to_string(),
+ path_scheme: "{locale}/".to_string(),
+ locales: vec!["en-US".to_string()],
+ }
+ }
+}
+
+impl FileSource {
+ pub fn new<S: ToString>(name: S, path_scheme: S, locales: Vec<S>) -> Self {
+ Self {
+ name: name.to_string(),
+ path_scheme: path_scheme.to_string(),
+ locales: locales
+ .iter()
+ .map(|l| l.to_string().parse().unwrap())
+ .collect(),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct L10nAttribute {
+ pub name: String,
+ pub value: String,
+}
+
+impl L10nAttribute {
+ pub fn new<S: ToString>(name: S, value: S) -> Self {
+ Self {
+ name: name.to_string(),
+ value: value.to_string(),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct L10nMessage {
+ pub value: Option<String>,
+ pub attributes: Option<Vec<L10nAttribute>>,
+}
+
+impl L10nMessage {
+ pub fn new(value: Option<&str>, attributes: Option<Vec<L10nAttribute>>) -> Self {
+ Self {
+ value: value.map(|v| v.to_string()),
+ attributes,
+ }
+ }
+}
+
+impl From<&str> for L10nMessage {
+ fn from(value: &str) -> Self {
+ Self {
+ value: Some(value.to_string()),
+ attributes: None,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct L10nArgument {
+ pub id: String,
+ pub value: String,
+}
+
+impl L10nArgument {
+ pub fn new<S: ToString>(id: S, value: S) -> Self {
+ Self {
+ id: id.to_string(),
+ value: value.to_string(),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct L10nKey {
+ pub id: String,
+ pub args: Option<Vec<L10nArgument>>,
+}
+
+impl L10nKey {
+ pub fn new<S: ToString>(id: S, args: Option<Vec<L10nArgument>>) -> Self {
+ Self {
+ id: id.to_string(),
+ args,
+ }
+ }
+}
+
+impl From<&str> for L10nKey {
+ fn from(input: &str) -> Self {
+ Self {
+ id: input.to_string(),
+ args: None,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum ExceptionalContext {
+ /// There is no exceptional context for this query (happy path).
+ None,
+ /// A value is missing from a resource and should cause a fallback.
+ ValueMissingFromResource,
+ /// A value is missing from all resources in all locales.
+ ValueMissingFromAllResources,
+ /// An optional resource is missing from the top locale.
+ OptionalResourceMissingFromLocale,
+ /// An optional resource is missing from all locales.
+ OptionalResourceMissingFromAllLocales,
+ /// A required resource is missing from the top locale.
+ RequiredResourceMissingFromLocale,
+ /// A required resource is missing from all locales.
+ RequiredResourceMissingFromAllLocales,
+}
+
+impl ExceptionalContext {
+ /// This is a query for a value in a missing required resource.
+ pub fn missing_required_resource(self) -> bool {
+ matches!(
+ self,
+ Self::RequiredResourceMissingFromLocale | Self::RequiredResourceMissingFromAllLocales,
+ )
+ }
+
+ /// This query should cause a format error to be appended to the errors Vec.
+ pub fn causes_reported_format_error(self) -> bool {
+ matches!(
+ self,
+ Self::ValueMissingFromResource
+ | Self::ValueMissingFromAllResources
+ | Self::OptionalResourceMissingFromLocale
+ | Self::OptionalResourceMissingFromAllLocales
+ | Self::RequiredResourceMissingFromAllLocales,
+ )
+ }
+
+ /// This query should cause a failed value lookup.
+ pub fn causes_failed_value_lookup(self) -> bool {
+ matches!(
+ self,
+ Self::ValueMissingFromAllResources
+ | Self::OptionalResourceMissingFromAllLocales
+ | Self::RequiredResourceMissingFromAllLocales,
+ )
+ }
+
+ /// This query should result in no bundles being generated.
+ pub fn blocks_bundle_generation(self) -> bool {
+ matches!(self, Self::RequiredResourceMissingFromAllLocales,)
+ }
+}
+
+#[derive(Debug)]
+pub struct Query {
+ pub input: L10nKey,
+ pub output: Option<L10nMessage>,
+ pub exceptional_context: ExceptionalContext,
+}
+
+impl Query {
+ pub fn new<K: Into<L10nKey>>(input: K, output: Option<L10nMessage>) -> Self {
+ Self {
+ input: input.into(),
+ output,
+ exceptional_context: ExceptionalContext::None,
+ }
+ }
+}
+
+impl From<(&str, &str)> for Query {
+ fn from(i: (&str, &str)) -> Self {
+ Self {
+ input: i.0.into(),
+ output: Some(i.1.into()),
+ exceptional_context: ExceptionalContext::None,
+ }
+ }
+}
+
+impl From<(&str, &str, ExceptionalContext)> for Query {
+ fn from(i: (&str, &str, ExceptionalContext)) -> Self {
+ Self {
+ input: i.0.into(),
+ output: Some(i.1.into()),
+ exceptional_context: i.2,
+ }
+ }
+}
+
+impl From<(&str, L10nMessage)> for Query {
+ fn from(i: (&str, L10nMessage)) -> Self {
+ Self {
+ input: i.0.into(),
+ output: Some(i.1),
+ exceptional_context: ExceptionalContext::None,
+ }
+ }
+}
+
+impl From<(L10nKey, L10nMessage)> for Query {
+ fn from(i: (L10nKey, L10nMessage)) -> Self {
+ Self {
+ input: i.0,
+ output: Some(i.1),
+ exceptional_context: ExceptionalContext::None,
+ }
+ }
+}
+
+impl From<&str> for Query {
+ fn from(i: &str) -> Self {
+ Self {
+ input: i.into(),
+ output: None,
+ exceptional_context: ExceptionalContext::None,
+ }
+ }
+}
+
+impl From<L10nKey> for Query {
+ fn from(key: L10nKey) -> Self {
+ Self {
+ input: key,
+ output: None,
+ exceptional_context: ExceptionalContext::None,
+ }
+ }
+}
+
+pub struct Queries(pub Vec<Query>);
+
+impl From<Vec<&str>> for Queries {
+ fn from(input: Vec<&str>) -> Self {
+ Self(input.into_iter().map(|q| q.into()).collect())
+ }
+}
+
+impl From<Vec<(&str, &str)>> for Queries {
+ fn from(input: Vec<(&str, &str)>) -> Self {
+ Self(input.into_iter().map(|q| q.into()).collect())
+ }
+}
+
+impl std::ops::Deref for Queries {
+ type Target = Vec<Query>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+pub struct Scenario {
+ pub name: String,
+ pub file_sources: Vec<FileSource>,
+ pub locales: Vec<String>,
+ pub res_ids: Vec<ResourceId>,
+ pub queries: Queries,
+}
+
+impl Scenario {
+ pub fn new<S: ToString, R: Into<ResourceId>, Q: Into<Queries>>(
+ name: S,
+ file_sources: Vec<FileSource>,
+ locales: Vec<S>,
+ res_ids: Vec<R>,
+ queries: Q,
+ ) -> Self {
+ Self {
+ name: name.to_string(),
+ file_sources,
+ locales: locales
+ .into_iter()
+ .map(|l| l.to_string().parse().unwrap())
+ .collect(),
+ res_ids: res_ids.into_iter().map(|id| id.into()).collect(),
+ queries: queries.into(),
+ }
+ }
+}
diff --git a/third_party/rust/fluent/.cargo-checksum.json b/third_party/rust/fluent/.cargo-checksum.json
new file mode 100644
index 0000000000..1cb95d6e9b
--- /dev/null
+++ b/third_party/rust/fluent/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"90672342000bb7f84bee3d9517ddf6d3f32f8e4b4fd38271c67855112456db05","LICENSE-APACHE":"5db2b182453ff32ed40f7da63589c9667a3f8bd8b16b1471b152caae56f77e45","LICENSE-MIT":"49c0b000c03731d9e3970dc059ad4ca345d773681f4a612b0024435b663e0220","README.md":"8b0515e111582a328d3f958af49c2bce55d70df0016edc50fe296e8dd318de32","src/lib.rs":"a64593add0850e6038cf63e1b805ea993835a7d822f5666422a2b8d22af9daa6"},"package":"61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7"} \ No newline at end of file
diff --git a/third_party/rust/fluent/Cargo.toml b/third_party/rust/fluent/Cargo.toml
new file mode 100644
index 0000000000..016f04bf4c
--- /dev/null
+++ b/third_party/rust/fluent/Cargo.toml
@@ -0,0 +1,34 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+edition = "2018"
+name = "fluent"
+version = "0.16.0"
+authors = ["Zibi Braniecki <gandalf@mozilla.com>", "Staś Małolepszy <stas@mozilla.com>"]
+include = ["src/**/*", "benches/*.rs", "Cargo.toml", "README.md", "LICENSE-APACHE", "LICENSE-MIT"]
+description = "A localization system designed to unleash the entire expressive power of\nnatural language translations.\n"
+homepage = "http://www.projectfluent.org"
+readme = "README.md"
+keywords = ["localization", "l10n", "i18n", "intl", "internationalization"]
+categories = ["localization", "internationalization"]
+license = "Apache-2.0/MIT"
+repository = "https://github.com/projectfluent/fluent-rs"
+[dependencies.fluent-bundle]
+version = "0.15"
+
+[dependencies.fluent-pseudo]
+version = "0.3"
+optional = true
+
+[dependencies.unic-langid]
+version = "0.9"
diff --git a/third_party/rust/fluent/LICENSE-APACHE b/third_party/rust/fluent/LICENSE-APACHE
new file mode 100644
index 0000000000..35582f166b
--- /dev/null
+++ b/third_party/rust/fluent/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017 Mozilla
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/third_party/rust/fluent/LICENSE-MIT b/third_party/rust/fluent/LICENSE-MIT
new file mode 100644
index 0000000000..5655fa311c
--- /dev/null
+++ b/third_party/rust/fluent/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright 2017 Mozilla
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/rust/fluent/README.md b/third_party/rust/fluent/README.md
new file mode 100644
index 0000000000..345db7c9a3
--- /dev/null
+++ b/third_party/rust/fluent/README.md
@@ -0,0 +1,120 @@
+# Fluent
+
+`fluent-rs` is a Rust implementation of [Project Fluent][], a localization
+framework designed to unleash the entire expressive power of natural language
+translations.
+
+[![crates.io](https://meritbadge.herokuapp.com/fluent)](https://crates.io/crates/fluent)
+[![Build and test](https://github.com/projectfluent/fluent-rs/workflows/Build%20and%20test/badge.svg)](https://github.com/projectfluent/fluent-rs/actions?query=branch%3Amaster+workflow%3A%22Build+and+test%22)
+[![Coverage Status](https://coveralls.io/repos/github/projectfluent/fluent-rs/badge.svg?branch=master)](https://coveralls.io/github/projectfluent/fluent-rs?branch=master)
+
+Project Fluent keeps simple things simple and makes complex things possible.
+The syntax used for describing translations is easy to read and understand. At
+the same time it allows, when necessary, to represent complex concepts from
+natural languages like gender, plurals, conjugations, and others.
+
+[Documentation][]
+
+[Project Fluent]: http://projectfluent.org
+[Documentation]: https://docs.rs/fluent/
+
+Usage
+-----
+
+```rust
+use fluent::{FluentBundle, FluentResource};
+use unic_langid::langid;
+
+fn main() {
+ let ftl_string = "hello-world = Hello, world!".to_owned();
+ let res = FluentResource::try_new(ftl_string)
+ .expect("Failed to parse an FTL string.");
+
+ let langid_en = langid!("en-US");
+ let mut bundle = FluentBundle::new(vec![langid_en]);
+
+ bundle.add_resource(&res)
+ .expect("Failed to add FTL resources to the bundle.");
+
+ let msg = bundle.get_message("hello-world")
+ .expect("Message doesn't exist.");
+ let mut errors = vec![];
+ let pattern = msg.value
+ .expect("Message has no value.");
+ let value = bundle.format_pattern(&pattern, None, &mut errors);
+
+ assert_eq!(&value, "Hello, world!");
+}
+```
+
+
+Status
+------
+
+The implementation is in pre-1.0 mode and supports Fluent Syntax 1.0, and
+Fluent API 0.14.. Consult the [list of milestones][] for more information about
+release planning and scope.
+
+`FluentBundle`, which is the main struct at the moment, is intended to remain
+a low level API.
+There is a number of higher level APIs like [fluent-resmgr][] and
+[fluent-fallback][] intended to wrap around it and provide better ergonomics
+for bindings and direct usage.
+Those higher level APIs are not mature yet enough to be included in this package,
+but will get added once they are.
+
+[list of milestones]: https://github.com/projectfluent/fluent-rs/milestones
+[fluent-resmgr]: https://crates.io/crates/fluent-resmgr
+[fluent-fallback]: https://crates.io/crates/fluent-fallback
+
+Local Development
+-----------------
+
+ cargo build
+ cargo test
+ cargo bench
+ cargo run --example simple-app
+
+When submitting a PR please use [`cargo fmt`][] (nightly).
+
+[`cargo fmt`]: https://github.com/rust-lang-nursery/rustfmt
+
+
+Learn the FTL syntax
+--------------------
+
+FTL is a localization file format used for describing translation resources.
+FTL stands for _Fluent Translation List_.
+
+FTL is designed to be simple to read, but at the same time allows to represent
+complex concepts from natural languages like gender, plurals, conjugations, and
+others.
+
+ hello-user = Hello, { $username }!
+
+[Read the Fluent Syntax Guide][] in order to learn more about the syntax. If
+you're a tool author you may be interested in the formal [EBNF grammar][].
+
+[Read the Fluent Syntax Guide]: http://projectfluent.org/fluent/guide/
+[EBNF grammar]: https://github.com/projectfluent/fluent/tree/master/spec
+
+
+Get Involved
+------------
+
+`fluent-rs` is open-source, licensed under the Apache License, Version 2.0. We
+encourage everyone to take a look at our code and we'll listen to your
+feedback.
+
+
+Discuss
+-------
+
+We'd love to hear your thoughts on Project Fluent! Whether you're a localizer
+looking for a better way to express yourself in your language, or a developer
+trying to make your app localizable and multilingual, or a hacker looking for
+a project to contribute to, please do get in touch on the mailing list and the
+IRC channel.
+
+ - Discourse: https://discourse.mozilla.org/c/fluent
+ - IRC channel: [irc://irc.mozilla.org/l20n](irc://irc.mozilla.org/l20n)
diff --git a/third_party/rust/fluent/src/lib.rs b/third_party/rust/fluent/src/lib.rs
new file mode 100644
index 0000000000..d041a09e60
--- /dev/null
+++ b/third_party/rust/fluent/src/lib.rs
@@ -0,0 +1,108 @@
+//! Fluent is a modern localization system designed to improve how software is translated.
+//!
+//! The Rust implementation provides the low level components for syntax operations, like parser
+//! and AST, and the core localization struct - [`FluentBundle`].
+//!
+//! [`FluentBundle`] is the low level container for storing and formatting localization messages
+//! in a single locale.
+//!
+//! This crate provides also a number of structures needed for a localization API such as [`FluentResource`],
+//! [`FluentMessage`], [`FluentArgs`], and [`FluentValue`].
+//!
+//! Together, they allow implementations to build higher-level APIs that use [`FluentBundle`]
+//! and add user friendly helpers, framework bindings, error fallbacking,
+//! language negotiation between user requested languages and available resources,
+//! and I/O for loading selected resources.
+//!
+//! # Example
+//!
+//! ```
+//! use fluent::{FluentBundle, FluentValue, FluentResource, FluentArgs};
+//!
+//! // Used to provide a locale for the bundle.
+//! use unic_langid::LanguageIdentifier;
+//!
+//! let ftl_string = String::from("
+//! hello-world = Hello, world!
+//! intro = Welcome, { $name }.
+//! ");
+//! let res = FluentResource::try_new(ftl_string)
+//! .expect("Failed to parse an FTL string.");
+//!
+//! let langid_en: LanguageIdentifier = "en-US".parse().expect("Parsing failed");
+//! let mut bundle = FluentBundle::new(vec![langid_en]);
+//!
+//! bundle
+//! .add_resource(res)
+//! .expect("Failed to add FTL resources to the bundle.");
+//!
+//! let msg = bundle.get_message("hello-world")
+//! .expect("Message doesn't exist.");
+//! let mut errors = vec![];
+//! let pattern = msg.value()
+//! .expect("Message has no value.");
+//! let value = bundle.format_pattern(&pattern, None, &mut errors);
+//!
+//! assert_eq!(&value, "Hello, world!");
+//!
+//! let mut args = FluentArgs::new();
+//! args.set("name", FluentValue::from("John"));
+//!
+//! let msg = bundle.get_message("intro")
+//! .expect("Message doesn't exist.");
+//! let mut errors = vec![];
+//! let pattern = msg.value().expect("Message has no value.");
+//! let value = bundle.format_pattern(&pattern, Some(&args), &mut errors);
+//!
+//! // The FSI/PDI isolation marks ensure that the direction of
+//! // the text from the variable is not affected by the translation.
+//! assert_eq!(value, "Welcome, \u{2068}John\u{2069}.");
+//! ```
+//!
+//! # Ergonomics & Higher Level APIs
+//!
+//! Reading the example, you may notice how verbose it feels.
+//! Many core methods are fallible, others accumulate errors, and there
+//! are intermediate structures used in operations.
+//!
+//! This is intentional as it serves as building blocks for variety of different
+//! scenarios allowing implementations to handle errors, cache and
+//! optimize results.
+//!
+//! At the moment it is expected that users will use
+//! the `fluent-bundle` crate directly, while the ecosystem
+//! matures and higher level APIs are being developed.
+//!
+//! [`FluentBundle`]: ./struct.FluentBundle.html
+//! [`FluentResource`]: ./struct.FluentResource.html
+//! [`FluentMessage`]: ./struct.FluentMessage.html
+//! [`FluentArgs`]: ./type.FluentArgs.html
+//! [`FluentValue`]: ./struct.FluentValue.html
+
+pub use fluent_bundle::*;
+
+/// A helper macro to simplify creation of FluentArgs.
+///
+/// # Example
+///
+/// ```
+/// use fluent::fluent_args;
+///
+/// let mut args = fluent_args![
+/// "name" => "John",
+/// "emailCount" => 5
+/// ];
+///
+/// ```
+#[macro_export]
+macro_rules! fluent_args {
+ ( $($key:expr => $value:expr),* ) => {
+ {
+ let mut args: $crate::FluentArgs = $crate::FluentArgs::new();
+ $(
+ args.set($key, $value);
+ )*
+ args
+ }
+ };
+}