summaryrefslogtreecommitdiffstats
path: root/third_party/rust/fluent-fallback
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/fluent-fallback')
-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
27 files changed, 3144 insertions, 0 deletions
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]