Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"a3c8e508cbb8e10b149222652774a2104b09f06fa26de25297d86b8e6cbf2b66",".github/workflows/release-plz.yaml":"e3cb69b0d3ce7ab2391df85c8b55c6e9170ffa1661a51fe066ca4ca3e4472b6c",".github/workflows/rust.yml":"51b555df6dc35fb37a710341afbb9c2099ed0f60841b3b58d93a74997b2def48",".release-plz.toml":"45795174368807268eef575308e5963882ddd9a8adba8fc2c30dd48838369c98","CHANGELOG.md":"7eb3fc574c19ee8a9221f870b5e47a2ee34494428100ad6ffd3fd4aeeb66ef6a","Cargo.lock":"c599a49a04b01e87729c521e71080238e87c57b3fddb5def7fcaa1ff90b62d7e","Cargo.toml":"3f158b4acd13ec754e4254a6cefafe92931e921aac6c1fd3adcc1c4ff7727b5a","Cargo.toml.orig":"41ea252ee3ca23a7dc5d497747036fe4c84fc6b9becae100d47df1c1d58a1d2c","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"15656cc11a8331f28c0986b8ab97220d3e76f98e60ed388b5ffad37dfac4710c","README.md":"39eb8f23e66a2b48327e8734e00b09ce3c7d56557d3ff574e83823191747be8a","src/error.rs":"04ad5442ad996ef0c8d07864087c6e878ed1ff401d9695a950b0ebd2878ee812","src/fs.rs":"b9b77f6f40a35dd1b0283d35f6489f75a17c39456579374a01311f878145a7cb","src/languages.rs":"b654c20d9b9e5cba574f8694447c0968c29ff3ace817597a19c261d9e938f992","src/lib.rs":"77169f8d3a32ce022d4a0ea9a103c32c9ea0e808a09d78ece27197ee10d0dce6","src/loader.rs":"7c77b5774d75ead86851662300a240a57422561584d185d8519a3d839ab65ba2","src/loader/arc_loader.rs":"7d54f46df7d58816769040c13312966a5efbddc63309fc19f73d4274fe048312","src/loader/handlebars.rs":"e48cb6c86cdbd7abf880f215f4778ced5d1f7998c63c151052978692652c6954","src/loader/multi_loader.rs":"d9b8c5b0327842382092c36546111967ca8e7e06732e05403102f19212483551","src/loader/shared.rs":"09120f3e95da2ec69909020720f462b39e916290bc7eac73effd768019f789a6","src/loader/static_loader.rs":"6030f513b61de017537b8146927a906f00d4bcd5857246a71aaac92c1c3b8b5e","src/loader/tera.rs":"11c9e5dc2d22eec9881d81566d1e05b117bb83b7ab4b8e1a2b2fb373c62cfb5b","tests/locales/core.ftl":"c6c2943f6099ddee98f67a8e8dfbd8ea39653a73cc87c8825c9cd89310e40664","tests/locales/de/main.ftl":"70d3020d1badb58ea0d087544a7135733a80af47f0141ed2acb1e14ef6fc2e62","tests/locales/en-US.ftl":"a4bb19353dced754dabf66608872dccc8c105a6e04ad3dbe6769b244105d3652","tests/locales/en-US/main.ftl":"779fb601bb36e25cd49e974f855287229f7203fd31dfb2a28ff43c529c5524b7","tests/locales/fr/main.ftl":"7830fab649687286e42cf68ff238eb6f9275f9c7ff5fce956ed97be286ae2a93","tests/locales/zh-CN/main.ftl":"f9f451114d9f486072bf988c85b6485be1ba1caab100ecad70c896b504ad9877","tests/locales/zh-TW/main.ftl":"e7f5716818df511a8c12b8bbe25b7cb8414645b0b791f99c71d98cc88ce17b08","tests/locales/zh/main.ftl":"6ecbf9b5a1744632da623de7aac265a628b04c87a0eac8e7b8e6f8f4e77f4be0","tests/multi_loader.rs":"ed9a60956ab07364de4d4f71ad483710354ede54df52e9c45ae5060fe64e7d0e","tests/template.rs":"893199e5cd18d9261fe18ce11e51064cef20e01868fa9558ed1774b6f3a441b1"},"package":"56264446a01f404469aef9cc5fd4a4d736f68a0f52482bf6d1a54d6e9bbd9476"}
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "6f1111b8bbec63b67434418d258adb554cb8b2d9"
},
"path_in_vcs": ""
}
@@ -0,0 +1,28 @@
name: Release-plz
permissions:
pull-requests: write
contents: write
on:
push:
branches:
- master
jobs:
release-plz:
name: Release-plz
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
@@ -0,0 +1,29 @@
name: Build & Test
on: [push, pull_request]
jobs:
no_and_all_features:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64-unknown-linux-gnu, wasm32-unknown-unknown]
steps:
- uses: actions/checkout@v2
- run: rustup target add ${{ matrix.target }}
- run: cargo build --workspace --target ${{ matrix.target }}
- run: cargo build --workspace --target ${{ matrix.target }} --all-features
- run: cargo test --workspace --target ${{ matrix.target }} && cargo test --workspace --target ${{ matrix.target }} --all-features
if: contains(matrix.target, 'linux')
feature_tests:
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
os: [ubuntu, macos, windows]
third-party-integration-feature: [handlebars, tera]
file-walker-feature: [walkdir, ignore]
steps:
- uses: actions/checkout@v2
- run: |
cargo build --features ${{ matrix.third-party-integration-feature }},${{ matrix.file-walker-feature }}
cargo test --verbose --features ${{ matrix.third-party-integration-feature }},${{ matrix.file-walker-feature }}
@@ -0,0 +1,3 @@
[[package]]
name = "fluent-template-macros"
publish_features = ["ignore"]
+278
View File
@@ -0,0 +1,278 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.13.1](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.13.0...fluent-templates-v0.13.1) - 2025-08-25
### Other
- improve the error messages for unknown localizations ([#94](https://github.com/XAMPPRocky/fluent-templates/pull/94))
- Bump Fluent crates to latest minor releases ([#92](https://github.com/XAMPPRocky/fluent-templates/pull/92))
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.13.3](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.13.2...fluent-templates-v0.13.3) - 2026-02-17
### Added
- support top-level language files ([#103](https://github.com/XAMPPRocky/fluent-templates/pull/103))
## [0.13.2](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.13.1...fluent-templates-v0.13.2) - 2025-10-23
### Other
- update thiserror to 2.0 ([#97](https://github.com/XAMPPRocky/fluent-templates/pull/97))
- make MultiLoader useable in statics ([#96](https://github.com/XAMPPRocky/fluent-templates/pull/96))
## [0.13.0](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.12.1...fluent-templates-v0.13.0) - 2025-01-12
### Added
- [**breaking**] Replace once_cell Lazy with standard lib LazyLock (#86)
## [0.12.1](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.12.0...fluent-templates-v0.12.1) - 2025-01-09
### Other
- Add a `MultiLoader` loader that consists of other loaders ([#84](https://github.com/XAMPPRocky/fluent-templates/pull/84))
## [0.12.0](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.11.0...fluent-templates-v0.12.0) - 2024-09-27
### Fixed
- [**breaking**] Make `Loader` object-safe ([#83](https://github.com/XAMPPRocky/fluent-templates/pull/83))
## [0.11.0](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.10.1...fluent-templates-v0.11.0) - 2024-09-22
### Added
- [**breaking**] upgrade to handlebars 6 ([#79](https://github.com/XAMPPRocky/fluent-templates/pull/79))
## [0.10.1](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.10.0...fluent-templates-v0.10.1) - 2024-08-19
### Fixed
- Drop `fluent` dependency ([#76](https://github.com/XAMPPRocky/fluent-templates/pull/76))
### Other
- Optimize fallback initialization on static loader macro ([#77](https://github.com/XAMPPRocky/fluent-templates/pull/77))
## [0.10.0](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.9.4...fluent-templates-v0.10.0) - 2024-08-16
### Added
- [**breaking**] Relax dependencies ([#74](https://github.com/XAMPPRocky/fluent-templates/pull/74))
- swap panic for error handling ([#63](https://github.com/XAMPPRocky/fluent-templates/pull/63))
### Other
- Make `flume` dependency optional and cleanup features ([#73](https://github.com/XAMPPRocky/fluent-templates/pull/73))
- Make `fallback` fields of `StaticLoader` and `ArcLoader` public ([#71](https://github.com/XAMPPRocky/fluent-templates/pull/71))
## [0.9.4](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.9.3...fluent-templates-v0.9.4) - 2024-04-22
### Other
- Bump `heck` to `v0.5.0` ([#67](https://github.com/XAMPPRocky/fluent-templates/pull/67))
## [0.9.3](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.9.2...fluent-templates-v0.9.3) - 2024-04-17
### Other
- Move common dependencies up to workspace level ([#64](https://github.com/XAMPPRocky/fluent-templates/pull/64))
## [0.9.2](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.9.1...fluent-templates-v0.9.2) - 2024-04-12
### Other
- Add follow_links to support builds with Bazel ([#62](https://github.com/XAMPPRocky/fluent-templates/pull/62))
- remove snafu ([#60](https://github.com/XAMPPRocky/fluent-templates/pull/60))
- Remove unused `lazy_static`, replace `snafu` with `thiserror`, bump `flume` to `0.11`
## [0.9.1](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.9.0...fluent-templates-v0.9.1) - 2024-03-10
### Other
- Check that `fallback_language` exists between locales building static bundles ([#58](https://github.com/XAMPPRocky/fluent-templates/pull/58))
## [0.9.0](https://github.com/XAMPPRocky/fluent-templates/compare/fluent-templates-v0.8.0...fluent-templates-v0.9.0) - 2024-03-04
### Fixed
- fix typo
- fixes [#10](https://github.com/XAMPPRocky/fluent-templates/pull/10) -- port info on templating language features from README to documentation ([#11](https://github.com/XAMPPRocky/fluent-templates/pull/11))
- fix reade
- fix missing link
### Other
- disable tera features
- Use resolve 2
- Use workspace.package
- Update release-plz.yaml
- Update to support handlebars v5 ([#55](https://github.com/XAMPPRocky/fluent-templates/pull/55))
- Create release-plz.yaml
- Sort the 'insert_resources' data to make static_loader! deterministic ([#56](https://github.com/XAMPPRocky/fluent-templates/pull/56))
- Bump syn to 2.0 ([#50](https://github.com/XAMPPRocky/fluent-templates/pull/50))
- Add ignore as optional feature ([#48](https://github.com/XAMPPRocky/fluent-templates/pull/48))
- Reverts changes from PR [#33](https://github.com/XAMPPRocky/fluent-templates/pull/33) and adds them with a "try_" prefix. ([#47](https://github.com/XAMPPRocky/fluent-templates/pull/47))
- Accept FnMut instead of fn pointer in ArcLoaderBuilder::customize ([#40](https://github.com/XAMPPRocky/fluent-templates/pull/40))
- Release 0.8.0
- Change the lookup function to be a better fallback chain ([#35](https://github.com/XAMPPRocky/fluent-templates/pull/35))
- Remove flume default features ([#36](https://github.com/XAMPPRocky/fluent-templates/pull/36))
- :lookup/lookup_complete/lookup_with_args return Option<String> ([#33](https://github.com/XAMPPRocky/fluent-templates/pull/33))
- Update rust.yml
- Release 0.7.1
- Release 0.7.0
- Update proc macro deps
- Update templating deps
- Update deps and file layout
- Release 0.6.1
- Updated fluent dependency to 0.14 ([#26](https://github.com/XAMPPRocky/fluent-templates/pull/26))
- Release 0.6.0
- Added attribute lookup support and refactored lookups to share code
- Borrowed argument keys support. ([#25](https://github.com/XAMPPRocky/fluent-templates/pull/25))
- add js feature to getrandom
- Update dependencies
- cfg out tempfile for wasm dev-dependencies
- Update README.md
- Release 0.5.16
- Point to readme in toml
- Release 0.5.15
- remove dead code
- Release 0.5.14
- re-export language ident and hide new method
- Release 0.5.13
- Add function that returns available locales ([#17](https://github.com/XAMPPRocky/fluent-templates/pull/17))
- Fix compile error with flume 0.7.2 ([#18](https://github.com/XAMPPRocky/fluent-templates/pull/18))
- Release 0.5.12
- Fallback to default when requested language is missing, rather than panicking ([#16](https://github.com/XAMPPRocky/fluent-templates/pull/16))
- Release 0.5.11
- Default lang argument in Tera templates ([#14](https://github.com/XAMPPRocky/fluent-templates/pull/14))
- Release 0.5.10
- Return errors from ArcLoaderBuilder.build(). ([#9](https://github.com/XAMPPRocky/fluent-templates/pull/9))
- Release 0.5.9
- Change core resource to use include_str
- Release 0.5.8
- Use include_str instead of reading file
- Update README.md ([#7](https://github.com/XAMPPRocky/fluent-templates/pull/7))
- Release 0.5.7
- minor nit
- Release 0.5.6
- Release 0.5.5
- Update toml
- Adding CARGO_MANIFEST_DIR to make paths consistent regardless of crate location within a workspace. ([#6](https://github.com/XAMPPRocky/fluent-templates/pull/6))
- Add proper message when failing to read locales
- Update README.md
- Update rust.yml
- Update rust.yml
- Update rust.yml
- Update rust.yml
- Update rust.yml
- Update rust.yml
- Update rust.yml
- Delete .travis.yml
- Update README
- Release 0.5.4
- Add visibility support for static loader
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update rust.yml
- Update rust.yml
- Update rust.yml
- Update rust.yml
- Update rust.yml
- Update CI
- Release 0.5.3
- Include macro by default
- Release 0.5.2
- Add docs.rs metadata
- Update README.md
- Update README.md
- Update rust.yml
- Release 0.5.1
- Update rust.yml
- fmt
- Update toml
- set version in toml
- Release 0.5.0
- Update README.md
- polish API
- Update documentation
- Fixed missing field not being handled
- fmt
- Renamed FluentHelper to FluentLoader
- Move static_loader to procedural macro
- Move to cargo workspace
- Move to using ignore for reading directories.
- Bump version
- Fixed multi word parametrs and changed args to take correct hashmap type.
- bump version
- Update dependencies
- Bump version
- Update top level documentation
- Add tera support
- Removed dbg statement
- Document everything and split handlebars into its own feature
- Format code
- Rename crate
- Remove unused imports
- Remove old read_from_file
- Switch to LoaderError as default error type
- Update github actions CI config
- Refactor fs into its own modules and add error type
- Version bump
- Update types and test code
- Version bump
- Update core to shared_resources and allow multiple files, and refactor out some unwraps
- Bump version
- Remove debug
- Version bump
- Switch from strs to paths
- Version bump
- Update description
- Set version to 0.4.0
- Update Handlebars
- Rename crate
- Add arc loader and refactor tests
- Update rust.yml
- Update rust.yml
- Update rust.yml
- Update README.md
- Update README.md
- Update rust.yml
- Update README.md
- Create rust.yml
- Make package template engine agnostic and add Tera support
- Update FluentBundle to use concurrent variant
- Bump to 0.3.0
- Merge pull request [#4](https://github.com/XAMPPRocky/fluent-templates/pull/4) from XAMPPRocky/upgrade-fluent
- Upgrade Fluent
- only load .ftl files as fluent files
- Release 0.2.0
- Mention constructor cost
- Macro fixes
- Add main docs
- Add documentation for loader
- Add customizer
- Clarify that this helper is for the Rust implementation, link JS
- Fill in tests
- test skeleton
- Add travis
- Fixes
- Move from identifier to string
- Remove incontext support
- text -> fluent
- Turn loader into a macro
- I18NHelper -> FluentHelper
- Turn loader into trait
- Split out loader
- Import I18nHelper from www.rust-lang.org
- Initial commit
+1098
View File
File diff suppressed because it is too large Load Diff
+147
View File
@@ -0,0 +1,147 @@
# 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"
rust-version = "1.70.0"
name = "fluent-templates"
version = "0.13.3"
authors = ["XAMPPRocky <xampprocky@icloud.com>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Templating for the Fluent localization framework"
documentation = "https://docs.rs/fluent-templates/"
readme = "README.md"
keywords = [
"handlebars",
"tera",
"fluent",
"internationalization",
"localization",
]
categories = [
"internationalization",
"localization",
"template-engine",
]
license = "MIT/Apache-2.0"
repository = "https://github.com/XAMPPRocky/fluent-templates"
resolver = "2"
[package.metadata.docs.rs]
all-features = true
targets = ["x86_64-unknown-linux-gnu"]
[features]
default = [
"macros",
"ignore",
]
handlebars = [
"dep:handlebars",
"dep:serde_json",
]
ignore = [
"dep:ignore",
"fluent-template-macros/ignore",
"dep:flume",
"dep:log",
]
macros = ["fluent-template-macros"]
tera = [
"dep:tera",
"dep:heck",
"dep:serde_json",
]
walkdir = [
"dep:walkdir",
"fluent-template-macros/walkdir",
"dep:log",
]
[lib]
name = "fluent_templates"
path = "src/lib.rs"
[[test]]
name = "multi_loader"
path = "tests/multi_loader.rs"
[[test]]
name = "template"
path = "tests/template.rs"
[dependencies.fluent-bundle]
version = "0.16"
[dependencies.fluent-langneg]
version = "0.13"
[dependencies.fluent-syntax]
version = "0.12"
[dependencies.fluent-template-macros]
version = "0.13.3"
optional = true
[dependencies.flume]
version = "0.11"
optional = true
default-features = false
[dependencies.handlebars]
version = "6"
optional = true
[dependencies.heck]
version = "0.5"
optional = true
[dependencies.ignore]
version = "0.4"
optional = true
[dependencies.intl-memoizer]
version = "0.5"
[dependencies.log]
version = "0.4"
optional = true
[dependencies.serde_json]
version = "1"
optional = true
[dependencies.tera]
version = "1.15"
optional = true
default-features = false
[dependencies.thiserror]
version = "2"
[dependencies.unic-langid]
version = "0.9"
features = [
"macros",
"macros",
]
[dependencies.walkdir]
version = "2"
optional = true
[dev-dependencies.tempfile]
version = "3.3"
+72
View File
@@ -0,0 +1,72 @@
[workspace]
members = [".", "macros"]
[package]
name = "fluent-templates"
authors.workspace = true
categories.workspace = true
description.workspace = true
documentation.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
resolver = "2"
rust-version = "1.70.0"
[workspace.package]
edition = "2021"
version = "0.13.3"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/fluent-templates/"
repository = "https://github.com/XAMPPRocky/fluent-templates"
authors = ["XAMPPRocky <xampprocky@icloud.com>"]
description = "Templating for the Fluent localization framework"
keywords = [
"handlebars",
"tera",
"fluent",
"internationalization",
"localization",
]
categories = ["internationalization", "localization", "template-engine"]
[workspace.dependencies]
unic-langid = { version = "0.9", features = ["macros"] }
ignore = "0.4"
flume = { version = "0.11", default-features = false }
walkdir = "2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[package.metadata.docs.rs]
all-features = true
targets = ["x86_64-unknown-linux-gnu"]
[features]
default = ["macros", "ignore"]
macros = ["fluent-template-macros"]
ignore = ["dep:ignore", "fluent-template-macros/ignore", "dep:flume", "dep:log"]
walkdir = ["dep:walkdir", "fluent-template-macros/walkdir", "dep:log"]
handlebars = ["dep:handlebars", "dep:serde_json"]
tera = ["dep:tera", "dep:heck", "dep:serde_json"]
[dependencies]
handlebars = { version = "6", optional = true }
fluent-bundle = "0.16"
fluent-syntax = "0.12"
fluent-langneg = "0.13"
serde_json = { version = "1", optional = true }
unic-langid = { workspace = true, features = ["macros"] }
thiserror = "2"
tera = { version = "1.15", optional = true, default-features = false }
heck = { version = "0.5", optional = true }
ignore = { workspace = true, optional = true }
flume = { workspace = true, optional = true }
log = { version = "0.4", optional = true }
fluent-template-macros = { path = "./macros", optional = true, version = "0.13.3" }
intl-memoizer = "0.5"
walkdir = { workspace = true, optional = true }
[dev-dependencies]
tempfile = "3.3"
+201
View File
@@ -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 [yyyy] [name of copyright owner]
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.
+27
View File
@@ -0,0 +1,27 @@
MIT License
Copyright (c) 2019 Manish Goregaokar
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.
+300
View File
@@ -0,0 +1,300 @@
# Fluent Templates: A High level Fluent API.
![Build & Test](https://github.com/XAMPPRocky/fluent-templates/workflows/Build%20&%20Test/badge.svg)
[![crates.io](https://img.shields.io/crates/d/fluent-templates.svg)](https://crates.io/crates/fluent-templates)
[![Help Wanted](https://img.shields.io/github/issues/XAMPPRocky/fluent-templates/help%20wanted?color=green)](https://github.com/XAMPPRocky/fluent-templates/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
[![Lines Of Code](https://tokei.rs/b1/github/XAMPPRocky/fluent-templates?category=code)](https://github.com/XAMPPRocky/tokei)
[![Documentation](https://docs.rs/fluent-templates/badge.svg)](https://docs.rs/fluent-templates/)
`fluent-templates` lets you to easily integrate [Fluent] localisation into
your Rust application or library. It does this by providing a high level
"loader" API that loads fluent strings based on simple language negotiation,
and the `FluentLoader` struct which is a `Loader` agnostic container type
that comes with optional trait implementations for popular templating
engines such as handlebars or tera that allow you to be able to use your
localisations in your templates with no boilerplate.
## Loaders
Currently this crate provides two different kinds of loaders that cover two
main use cases.
- [`static_loader!`] — A procedural macro that loads your fluent resources
at *compile-time* into your binary and creates a new [`StaticLoader`]
static variable that allows you to access the localisations.
`static_loader!` is most useful when you want to localise your
application and want to ship your fluent resources with your binary.
- [`ArcLoader`] — A struct that loads your fluent resources at *run-time*
using `Arc` as its backing storage. `ArcLoader` is most useful for when
you want to be able to change and/or update localisations at run-time, or
if you're writing a developer tool that wants to provide fluent
localisation in your own application such as a static site generator.
## `static_loader!`
The easiest way to use `fluent-templates` is to use the [`static_loader!`]
procedural macro that will create a new [`StaticLoader`] static variable.
### Basic Example
```rust
fluent_templates::static_loader! {
// Declare our `StaticLoader` named `LOCALES`.
static LOCALES = {
// The directory of localisations and fluent resources.
locales: "./tests/locales",
// The language to falback on if something is not present.
fallback_language: "en-US",
// Optional: A fluent resource that is shared with every locale.
core_locales: "./tests/locales/core.ftl",
};
}
```
### Customise Example
You can also modify each `FluentBundle` on initialisation to be able to
change configuration or add resources from Rust.
```rust
use std::sync::LazyLock;
use fluent_bundle::FluentResource;
use fluent_templates::static_loader;
static_loader! {
// Declare our `StaticLoader` named `LOCALES`.
static LOCALES = {
// The directory of localisations and fluent resources.
locales: "./tests/locales",
// The language to falback on if something is not present.
fallback_language: "en-US",
// Optional: A fluent resource that is shared with every locale.
core_locales: "./tests/locales/core.ftl",
// Optional: A function that is run over each fluent bundle.
customise: |bundle| {
// Since this will be called for each locale bundle and
// `FluentResource`s need to be either `&'static` or behind an
// `Arc` it's recommended you use lazily initialised
// static variables.
static CRATE_VERSION_FTL: LazyLock<FluentResource> = LazyLock::new(|| {
let ftl_string = String::from(
concat!("-crate-version = {}", env!("CARGO_PKG_VERSION"))
);
FluentResource::try_new(ftl_string).unwrap()
});
bundle.add_resource(&CRATE_VERSION_FTL);
}
};
}
```
## Locales Directory
`fluent-templates` will collect all subdirectories that match a valid
[Unicode Language Identifier][uli] and bundle all fluent files found in
those directories and map those resources to the respective identifier.
It also supports locale files named `locales/<lang>.ftl`, which are merged
with any resources found in `locales/<lang>/`.
`fluent-templates` will recurse through each language directory as needed
and will respect any `.gitignore` or `.ignore` files present.
[uli]: https://docs.rs/unic-langid/0.9.0/unic_langid/
### Example Layout
```text
locales
├── core.ftl
├── en-US.ftl
├── en-US
│   └── extra.ftl
├── fr
│   └── main.ftl
├── zh-CN
│   └── main.ftl
└── zh-TW
└── main.ftl
```
### Looking up fluent resources
You can use the [`Loader`] trait to `lookup` a given fluent resource, and
provide any additional arguments as needed with `lookup_with_args`.
#### Example
```fluent
# In `locales/en-US/main.ftl`
hello-world = Hello World!
greeting = Hello { $name }!
# In `locales/fr/main.ftl`
hello-world = Bonjour le monde!
greeting = Bonjour { $name }!
# In `locales/de/main.ftl`
hello-world = Hallo Welt!
greeting = Hallo { $name }!
```
```rust
use std::collections::HashMap;
use unic_langid::{LanguageIdentifier, langid};
use fluent_templates::{Loader, static_loader};
const US_ENGLISH: LanguageIdentifier = langid!("en-US");
const FRENCH: LanguageIdentifier = langid!("fr");
const GERMAN: LanguageIdentifier = langid!("de");
static_loader! {
static LOCALES = {
locales: "./tests/locales",
fallback_language: "en-US",
// Removes unicode isolating marks around arguments, you typically
// should only set to false when testing.
customise: |bundle| bundle.set_use_isolating(false),
};
}
fn main() {
assert_eq!("Hello World!", LOCALES.lookup(&US_ENGLISH, "hello-world"));
assert_eq!("Bonjour le monde!", LOCALES.lookup(&FRENCH, "hello-world"));
assert_eq!("Hallo Welt!", LOCALES.lookup(&GERMAN, "hello-world"));
let args = {
let mut map = HashMap::new();
map.insert(String::from("name"), "Alice".into());
map
};
assert_eq!("Hello Alice!", LOCALES.lookup_with_args(&US_ENGLISH, "greeting", &args));
assert_eq!("Bonjour Alice!", LOCALES.lookup_with_args(&FRENCH, "greeting", &args));
assert_eq!("Hallo Alice!", LOCALES.lookup_with_args(&GERMAN, "greeting", &args));
}
```
### Tera
With the `tera` feature you can use `FluentLoader` as a Tera function.
It accepts a `key` parameter pointing to a fluent resource and `lang` for
what language to get that key for. Optionally you can pass extra arguments
to the function as arguments to the resource. `fluent-templates` will
automatically convert argument keys from Tera's `snake_case` to the fluent's
preferred `kebab-case` arguments.
```toml
fluent-templates = { version = "*", features = ["tera"] }
```
```rust
use fluent_templates::{FluentLoader, static_loader};
static_loader! {
static LOCALES = {
locales: "./tests/locales",
fallback_language: "en-US",
// Removes unicode isolating marks around arguments, you typically
// should only set to false when testing.
customise: |bundle| bundle.set_use_isolating(false),
};
}
fn main() {
let mut tera = tera::Tera::default();
let ctx = tera::Context::default();
tera.register_function("fluent", FluentLoader::new(&*LOCALES));
assert_eq!(
"Hello World!",
tera.render_str(r#"{{ fluent(key="hello-world", lang="en-US") }}"#, &ctx).unwrap()
);
assert_eq!(
"Hello Alice!",
tera.render_str(r#"{{ fluent(key="greeting", lang="en-US", name="Alice") }}"#, &ctx).unwrap()
);
}
```
### Handlebars
In handlebars, `fluent-templates` will read the `lang` field in your
[`handlebars::Context`] while rendering.
```toml
fluent-templates = { version = "*", features = ["handlebars"] }
```
```rust
use fluent_templates::{FluentLoader, static_loader};
static_loader! {
static LOCALES = {
locales: "./tests/locales",
fallback_language: "en-US",
// Removes unicode isolating marks around arguments, you typically
// should only set to false when testing.
customise: |bundle| bundle.set_use_isolating(false),
};
}
fn main() {
let mut handlebars = handlebars::Handlebars::new();
handlebars.register_helper("fluent", Box::new(FluentLoader::new(&*LOCALES)));
let data = serde_json::json!({"lang": "zh-CN"});
assert_eq!("Hello World!", handlebars.render_template(r#"{{fluent "hello-world"}}"#, &data).unwrap());
assert_eq!("Hello Alice!", handlebars.render_template(r#"{{fluent "greeting" name="Alice"}}"#, &data).unwrap());
}
```
### Handlebars helper syntax.
The main helper provided is the `{{fluent}}` helper. If you have the
following Fluent file:
```fluent
foo-bar = "foo bar"
placeholder = this has a placeholder { $variable }
placeholder2 = this has { $variable1 } { $variable2 }
```
You can include the strings in your template with
```hbs
<!-- will render "foo bar" -->
{{fluent "foo-bar"}}
<!-- will render "this has a placeholder baz" -->
{{fluent "placeholder" variable="baz"}}
```
You may also use the `{{fluentparam}}` helper to specify [variables],
especially if you need them to be multiline.
```hbs
{{#fluent "placeholder2"}}
{{#fluentparam "variable1"}}
first line
second line
{{/fluentparam}}
{{#fluentparam "variable2"}}
first line
second line
{{/fluentparam}}
{{/fluent}}
```
### FAQ
#### Why is there extra characters around the values of arguments?
These are called "Unicode Isolating Marks" that used to allow the text to be bidirectional.
You can disable this with `FluentBundle::set_isolating_marks` being set to `false`.
```rust
static_loader! {
static LOCALES = {
locales: "./tests/locales",
fallback_language: "en-US",
// Removes unicode isolating marks around arguments.
customise: |bundle| bundle.set_use_isolating(false),
};
}
```
[variables]: https://projectfluent.org/fluent/guide/variables.html
[`static_loader!`]: https://docs.rs/fluent-templates/0.5.4/fluent_templates/macro.static_loader.html
[`StaticLoader`]: https://docs.rs/fluent-templates/0.5.4/fluent_templates/struct.StaticLoader.html
[`ArcLoader`]: https://docs.rs/fluent-templates/0.5.4/fluent_templates/struct.ArcLoader.html
[`Loader`]: https://docs.rs/fluent-templates/0.5.4/fluent_templates/trait.Loader.html
[`handlebars::Context`]: https://docs.rs/handlebars/3.1.0/handlebars/struct.Context.html
[fluent]: https://projectfluent.org
+73
View File
@@ -0,0 +1,73 @@
use std::fmt;
use unic_langid::LanguageIdentifier;
/// Errors that can occur when loading or parsing fluent resources.
#[derive(Debug, thiserror::Error)]
pub enum LoaderError {
/// An `io::Error` occurred while interacting with `path`.
#[error("Error with {}\n: {}", path.display(), source)]
Fs {
/// The path to file with the error.
path: std::path::PathBuf,
/// The error source.
source: std::io::Error,
},
/// An error was found in the fluent syntax.
#[error("Error parsing Fluent\n: {}", source)]
Fluent {
/// The original parse errors
#[from]
source: FluentError,
},
/// An error was found whilst loading a bundle at runtime.
#[error("Failed to add FTL resources to the bundle")]
FluentBundle {
/// The original bundle errors
errors: Vec<fluent_bundle::FluentError>,
},
}
/// A wrapper struct around `Vec<fluent_syntax::parser::ParserError>`.
#[derive(Debug)]
pub struct FluentError(Vec<fluent_syntax::parser::ParserError>);
impl From<Vec<fluent_syntax::parser::ParserError>> for FluentError {
fn from(errors: Vec<fluent_syntax::parser::ParserError>) -> Self {
Self(errors)
}
}
impl From<FluentError> for Vec<fluent_syntax::parser::ParserError> {
fn from(error: FluentError) -> Self {
error.0
}
}
impl fmt::Display for FluentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for error in &self.0 {
write!(f, "{:?}", error)?;
}
Ok(())
}
}
impl std::error::Error for FluentError {}
/// An error that happened while looking up messages
#[derive(Debug, thiserror::Error)]
pub enum LookupError {
#[error("Couldn't retrieve message with ID `{0}`")]
MessageRetrieval(String),
#[error("Couldn't find attribute `{attribute}` for message-id `{message_id}`")]
AttributeNotFound {
message_id: String,
attribute: String,
},
#[error("Language ID `{0}` has not been loaded")]
LangNotLoaded(LanguageIdentifier),
#[error("Fluent errors: {0:?}")]
FluentError(Vec<fluent_bundle::FluentError>),
}
+132
View File
@@ -0,0 +1,132 @@
use std::fs;
use std::path::Path;
use fluent_bundle::FluentResource;
pub use unic_langid::{langid, langids, LanguageIdentifier};
use crate::error;
pub fn read_from_file<P: AsRef<Path>>(path: P) -> crate::Result<FluentResource> {
let path = path.as_ref();
resource_from_str(
&fs::read_to_string(path).map_err(|source| error::LoaderError::Fs {
path: path.into(),
source,
})?,
)
}
pub fn resource_from_str(src: &str) -> crate::Result<FluentResource> {
FluentResource::try_new(src.to_owned())
.map_err(|(_, errs)| error::FluentError::from(errs).into())
}
pub fn resources_from_vec(srcs: &[String]) -> crate::Result<Vec<FluentResource>> {
let mut vec = Vec::with_capacity(srcs.len());
for src in srcs {
vec.push(resource_from_str(src)?);
}
Ok(vec)
}
pub(crate) fn read_from_dir<P: AsRef<Path>>(path: P) -> crate::Result<Vec<FluentResource>> {
#[cfg(not(any(feature = "ignore", feature = "walkdir")))]
compile_error!("one of the features `ignore` or `walkdir` must be enabled.");
#[cfg(feature = "ignore")]
{
let (tx, rx) = flume::unbounded();
ignore::WalkBuilder::new(path).build_parallel().run(|| {
let tx = tx.clone();
Box::new(move |result| {
if let Ok(entry) = result {
if entry
.file_type()
.as_ref()
.is_some_and(fs::FileType::is_file)
&& entry.path().extension().is_some_and(|e| e == "ftl")
{
if let Ok(string) = std::fs::read_to_string(entry.path()) {
let _ = tx.send(string);
} else {
log::warn!("Couldn't read {}", entry.path().display());
}
}
}
ignore::WalkState::Continue
})
});
resources_from_vec(&rx.drain().collect::<Vec<_>>())
}
#[cfg(all(not(feature = "ignore"), feature = "walkdir"))]
{
let mut srcs = Vec::new();
walkdir::WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.filter(|e| e.path().extension().map_or(false, |e| e == "ftl"))
.for_each(|e| {
if let Ok(string) = std::fs::read_to_string(e.path()) {
srcs.push(string);
} else {
log::warn!("Couldn't read {}", e.path().display());
}
});
return resources_from_vec(&srcs);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FluentBundle;
use std::error::Error;
#[test]
fn test_load_from_dir() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
std::fs::write(dir.path().join("core.ftl"), "foo = bar\n".as_bytes())?;
std::fs::write(dir.path().join("other.ftl"), "bar = baz\n".as_bytes())?;
std::fs::write(dir.path().join("invalid.txt"), "baz = foo\n".as_bytes())?;
std::fs::write(dir.path().join(".binary_file.swp"), [0, 1, 2, 3, 4, 5])?;
let result = read_from_dir(dir.path())?;
assert_eq!(2, result.len()); // Doesn't include the binary file or the txt file
let mut bundle = FluentBundle::new_concurrent(vec![unic_langid::langid!("en-US")]);
for resource in &result {
bundle.add_resource(resource).unwrap();
}
let mut errors = Vec::new();
// Ensure the correct files were loaded
assert_eq!(
"bar",
bundle.format_pattern(
bundle.get_message("foo").and_then(|m| m.value()).unwrap(),
None,
&mut errors
)
);
assert_eq!(
"baz",
bundle.format_pattern(
bundle.get_message("bar").and_then(|m| m.value()).unwrap(),
None,
&mut errors
)
);
assert_eq!(None, bundle.get_message("baz")); // The extension was txt
Ok(())
}
}
+134
View File
@@ -0,0 +1,134 @@
use unic_langid::LanguageIdentifier;
/// This is taken from fluent_langneg, but changed to return a list of language that match the available languages sorted by specificity
pub fn filter_matches<'a, R: 'a + AsRef<LanguageIdentifier>, A: 'a + AsRef<LanguageIdentifier>>(
requested: &[R],
available: &'a [A],
) -> Vec<&'a A> {
let mut supported_locales = vec![];
let mut available_locales: Vec<&A> = available.iter().collect();
for req in requested {
let req = req.as_ref().to_owned();
macro_rules! test_strategy {
($self_as_range:expr, $other_as_range:expr) => {{
let mut match_found = false;
available_locales.retain(|locale| {
if locale
.as_ref()
.matches(&req, $self_as_range, $other_as_range)
{
match_found = true;
supported_locales.push(*locale);
return false;
}
true
});
}};
}
// 1) Try to find a simple (case-insensitive) string match for the request.
test_strategy!(false, false);
// 2) Try to match against the available locales treated as ranges.
test_strategy!(true, false);
// Per Unicode TR35, 4.4 Locale Matching, we don't add likely subtags to
// requested locales, so we'll skip it from the rest of the steps.
if req.language.is_empty() {
continue;
}
}
supported_locales.sort_by(|x, y| {
let x_specificity = into_specificity(x.as_ref());
let y_specificity = into_specificity(y.as_ref());
x_specificity.cmp(&y_specificity).reverse()
});
supported_locales
}
fn into_specificity(lang: &LanguageIdentifier) -> usize {
// let parts = lang.into_parts();
let mut specificity = 0;
// Script
if lang.script.is_some() {
specificity += 1;
}
// Region
if lang.region.is_some() {
specificity += 1;
}
// variant
specificity += lang.variants().len();
specificity
}
pub fn negotiate_languages<
'a,
R: 'a + AsRef<LanguageIdentifier>,
A: 'a + AsRef<LanguageIdentifier> + PartialEq,
>(
requested: &[R],
available: &'a [A],
default: Option<&'a A>,
) -> Vec<&'a A> {
let mut supported = filter_matches(requested, available);
if let Some(default) = default {
if !supported.contains(&default) {
supported.push(default);
}
}
supported
}
#[cfg(test)]
mod test {
use super::*;
use fluent_langneg::convert_vec_str_to_langids;
macro_rules! test_negotiation {
($input:expr , $valid:expr => $output:expr) => {
assert_eq!(
filter_matches(
&convert_vec_str_to_langids($input).expect("Input"),
&convert_vec_str_to_langids($valid).expect("Valid")
),
convert_vec_str_to_langids($output)
.expect("result")
.iter()
.collect::<Vec<_>>()
);
};
}
#[test]
fn test_hirarchy() {
test_negotiation!(["de"], ["de", "de-DE-1996", "en-US", "de-DE", "de-CH"] => ["de"]);
test_negotiation!(["de-DE"], ["de", "de-DE-1996", "en-US", "de-DE", "de-CH"] => ["de-DE", "de"]);
test_negotiation!(["de-CH"], ["de", "de-DE-1996", "en-US", "de-DE", "de-CH"] => ["de-CH", "de"]);
test_negotiation!(["de-DE-1996"], ["de", "de-DE-1996", "en-US", "de-DE", "de-CH"] => ["de-DE-1996", "de-DE", "de"]);
}
#[test]
fn test_negotiate_languages() {
assert_eq!(
negotiate_languages(
&convert_vec_str_to_langids(["de-DE"]).unwrap(),
&convert_vec_str_to_langids(["de-DE", "de", "en-US", "de-CH"],).unwrap(),
None,
),
convert_vec_str_to_langids(["de-DE", "de"])
.expect("result")
.iter()
.collect::<Vec<_>>()
);
}
}
+364
View File
@@ -0,0 +1,364 @@
//! # Fluent Templates: A High level Fluent API.
//!
//! `fluent-templates` lets you to easily integrate Fluent localisation into
//! your Rust application or library. It does this by providing a high level
//! "loader" API that loads fluent strings based on simple language negotiation,
//! and the `FluentLoader` struct which is a `Loader` agnostic container type
//! that comes with optional trait implementations for popular templating
//! engines such as handlebars or tera that allow you to be able to use your
//! localisations in your templates with no boilerplate.
//!
//! ## Loaders
//! Currently this crate provides two different kinds of loaders that cover two
//! main use cases.
//!
//! - [`static_loader!`] — A procedural macro that loads your fluent resources
//! at *compile-time* into your binary and creates a new [`StaticLoader`]
//! static variable that allows you to access the localisations.
//! `static_loader!` is most useful when you want to localise your
//! application and want to ship your fluent resources with your binary.
//!
//! - [`ArcLoader`] — A struct that loads your fluent resources at *run-time*
//! using `Arc` as its backing storage. `ArcLoader` is most useful for when
//! you want to be able to change and/or update localisations at run-time, or
//! if you're writing a developer tool that wants to provide fluent
//! localisation in your own application such as a static site generator.
//!
//!
//! ## `static_loader!`
//! The easiest way to use `fluent-templates` is to use the [`static_loader!`]
//! procedural macro that will create a new [`StaticLoader`] static variable.
//!
//! ### Basic Example
//! ```
//! fluent_templates::static_loader! {
//! // Declare our `StaticLoader` named `LOCALES`.
//! static LOCALES = {
//! // The directory of localisations and fluent resources.
//! locales: "./tests/locales",
//! // The language to falback on if something is not present.
//! fallback_language: "en-US",
//! // Optional: A fluent resource that is shared with every locale.
//! core_locales: "./tests/locales/core.ftl",
//! };
//! }
//! # fn main() {}
//! ```
//!
//! ### Customise Example
//! You can also modify each `FluentBundle` on initialisation to be able to
//! change configuration or add resources from Rust.
//! ```
//! use std::sync::LazyLock;
//! use fluent_bundle::FluentResource;
//! use fluent_templates::static_loader;
//!
//! static_loader! {
//! // Declare our `StaticLoader` named `LOCALES`.
//! static LOCALES = {
//! // The directory of localisations and fluent resources.
//! locales: "./tests/locales",
//! // The language to falback on if something is not present.
//! fallback_language: "en-US",
//! // Optional: A fluent resource that is shared with every locale.
//! core_locales: "./tests/locales/core.ftl",
//! // Optional: A function that is run over each fluent bundle.
//! customise: |bundle| {
//! // Since this will be called for each locale bundle and
//! // `FluentResource`s need to be either `&'static` or behind an
//! // `Arc` it's recommended you use lazily initialised
//! // static variables.
//! static CRATE_VERSION_FTL: LazyLock<FluentResource> = LazyLock::new(|| {
//! let ftl_string = String::from(
//! concat!("-crate-version = {}", env!("CARGO_PKG_VERSION"))
//! );
//!
//! FluentResource::try_new(ftl_string).unwrap()
//! });
//!
//! bundle.add_resource(&CRATE_VERSION_FTL);
//! }
//! };
//! }
//! # fn main() {}
//! ```
//!
//! ## Locales Directory
//! `fluent-templates` will collect all subdirectories that match a valid
//! [Unicode Language Identifier][uli] and bundle all fluent files found in
//! those directories and map those resources to the respective identifier.
//! It also supports locale files named `locales/<lang>.ftl`, which are merged
//! with any resources found in `locales/<lang>/`.
//! `fluent-templates` will recurse through each language directory as needed
//! and will respect any `.gitignore` or `.ignore` files present.
//!
//! [uli]: https://docs.rs/unic-langid/0.9.0/unic_langid/
//!
//! ### Example Layout
//! ```text
//! locales
//! ├── core.ftl
//! ├── en-US.ftl
//! ├── en-US
//! │   └── extra.ftl
//! ├── fr
//! │   └── main.ftl
//! ├── zh-CN
//! │   └── main.ftl
//! └── zh-TW
//! └── main.ftl
//! ```
//!
//! ### Looking up fluent resources
//! You can use the [`Loader`] trait to `lookup` a given fluent resource, and
//! provide any additional arguments as needed with `lookup_with_args`. You
//! can also look up attributes by appending a `.` to the name of the message.
//!
//! #### Example
//! ```fluent
//! # In `locales/en-US/main.ftl`
//! hello-world = Hello World!
//! greeting = Hello { $name }!
//! .placeholder = Hello Friend!
//!
//! # In `locales/fr/main.ftl`
//! hello-world = Bonjour le monde!
//! greeting = Bonjour { $name }!
//! .placeholder = Salut l'ami!
//!
//! # In `locales/de/main.ftl`
//! hello-world = Hallo Welt!
//! greeting = Hallo { $name }!
//! .placeholder = Hallo Fruend!
//! ```
//!
//! ```
//! use std::{borrow::Cow, collections::HashMap};
//!
//! use unic_langid::{LanguageIdentifier, langid};
//! use fluent_templates::{Loader, static_loader};
//!
//!const US_ENGLISH: LanguageIdentifier = langid!("en-US");
//!const FRENCH: LanguageIdentifier = langid!("fr");
//!const GERMAN: LanguageIdentifier = langid!("de");
//!
//! static_loader! {
//! static LOCALES = {
//! locales: "./tests/locales",
//! fallback_language: "en-US",
//! // Removes unicode isolating marks around arguments, you typically
//! // should only set to false when testing.
//! customise: |bundle| bundle.set_use_isolating(false),
//! };
//! }
//!
//! fn main() {
//! assert_eq!("Hello World!", LOCALES.lookup(&US_ENGLISH, "hello-world"));
//! assert_eq!("Bonjour le monde!", LOCALES.lookup(&FRENCH, "hello-world"));
//! assert_eq!("Hallo Welt!", LOCALES.lookup(&GERMAN, "hello-world"));
//!
//! assert_eq!("Hello World!", LOCALES.try_lookup(&US_ENGLISH, "hello-world").unwrap());
//! assert_eq!("Bonjour le monde!", LOCALES.try_lookup(&FRENCH, "hello-world").unwrap());
//! assert_eq!("Hallo Welt!", LOCALES.try_lookup(&GERMAN, "hello-world").unwrap());
//!
//! let args = {
//! let mut map = HashMap::new();
//! map.insert(Cow::from("name"), "Alice".into());
//! map
//! };
//!
//! assert_eq!("Hello Friend!", LOCALES.lookup(&US_ENGLISH, "greeting.placeholder"));
//! assert_eq!("Hello Alice!", LOCALES.lookup_with_args(&US_ENGLISH, "greeting", &args));
//! assert_eq!("Salut l'ami!", LOCALES.lookup(&FRENCH, "greeting.placeholder"));
//! assert_eq!("Bonjour Alice!", LOCALES.lookup_with_args(&FRENCH, "greeting", &args));
//! assert_eq!("Hallo Fruend!", LOCALES.lookup(&GERMAN, "greeting.placeholder"));
//! assert_eq!("Hallo Alice!", LOCALES.lookup_with_args(&GERMAN, "greeting", &args));
//!
//! assert_eq!("Hello Friend!", LOCALES.try_lookup(&US_ENGLISH, "greeting.placeholder").unwrap());
//! assert_eq!("Hello Alice!", LOCALES.try_lookup_with_args(&US_ENGLISH, "greeting", &args).unwrap());
//! assert_eq!("Salut l'ami!", LOCALES.try_lookup(&FRENCH, "greeting.placeholder").unwrap());
//! assert_eq!("Bonjour Alice!", LOCALES.try_lookup_with_args(&FRENCH, "greeting", &args).unwrap());
//! assert_eq!("Hallo Fruend!", LOCALES.try_lookup(&GERMAN, "greeting.placeholder").unwrap());
//! assert_eq!("Hallo Alice!", LOCALES.try_lookup_with_args(&GERMAN, "greeting", &args).unwrap());
//!
//!
//! let args = {
//! let mut map = HashMap::new();
//! map.insert(Cow::Borrowed("param"), "1".into());
//! map.insert(Cow::Owned(format!("{}-param", "multi-word")), "2".into());
//! map
//! };
//!
//! assert_eq!("text one 1 second 2", LOCALES.lookup_with_args(&US_ENGLISH, "parameter2", &args));
//! assert_eq!("texte une 1 seconde 2", LOCALES.lookup_with_args(&FRENCH, "parameter2", &args));
//!
//! assert_eq!("text one 1 second 2", LOCALES.try_lookup_with_args(&US_ENGLISH, "parameter2", &args).unwrap());
//! assert_eq!("texte une 1 seconde 2", LOCALES.try_lookup_with_args(&FRENCH, "parameter2", &args).unwrap());
//! }
//! ```
//!
//! ### Tera
//! With the `tera` feature you can use `FluentLoader` as a Tera function.
//! It accepts a `key` parameter pointing to a fluent resource and `lang` for
//! what language to get that key for. Optionally you can pass extra arguments
//! to the function as arguments to the resource. `fluent-templates` will
//! automatically convert argument keys from Tera's `snake_case` to the fluent's
//! preferred `kebab-case` arguments.
//! The `lang` parameter is optional when the default language of the corresponding
//! `FluentLoader` is set (see [`FluentLoader::with_default_lang`]).
//!
//! ```toml
//!fluent-templates = { version = "*", features = ["tera"] }
//!```
//!
//! ```rust
//! use fluent_templates::{FluentLoader, static_loader};
//!
//! static_loader! {
//! static LOCALES = {
//! locales: "./tests/locales",
//! fallback_language: "en-US",
//! // Removes unicode isolating marks around arguments, you typically
//! // should only set to false when testing.
//! customise: |bundle| bundle.set_use_isolating(false),
//! };
//! }
//!
//! fn main() {
//! # #[cfg(feature = "tera")] {
//! let mut tera = tera::Tera::default();
//! let ctx = tera::Context::default();
//! tera.register_function("fluent", FluentLoader::new(&*LOCALES));
//! assert_eq!(
//! "Hello World!",
//! tera.render_str(r#"{{ fluent(key="hello-world", lang="en-US") }}"#, &ctx).unwrap()
//! );
//! assert_eq!(
//! "Hello Alice!",
//! tera.render_str(r#"{{ fluent(key="greeting", lang="en-US", name="Alice") }}"#, &ctx).unwrap()
//! );
//! }
//! # }
//! ```
//!
//! ### Handlebars
//! In handlebars, `fluent-templates` will read the `lang` field in your
//! [`handlebars::Context`] while rendering.
//!
//! ```toml
//!fluent-templates = { version = "*", features = ["handlebars"] }
//!```
//!
//! ```rust
//! use fluent_templates::{FluentLoader, static_loader};
//!
//! static_loader! {
//! static LOCALES = {
//! locales: "./tests/locales",
//! fallback_language: "en-US",
//! // Removes unicode isolating marks around arguments, you typically
//! // should only set to false when testing.
//! customise: |bundle| bundle.set_use_isolating(false),
//! };
//! }
//!
//! fn main() {
//! # #[cfg(feature = "handlebars")] {
//! let mut handlebars = handlebars::Handlebars::new();
//! handlebars.register_helper("fluent", Box::new(FluentLoader::new(&*LOCALES)));
//! let data = serde_json::json!({"lang": "zh-CN"});
//! assert_eq!("Hello World!", handlebars.render_template(r#"{{fluent "hello-world"}}"#, &data).unwrap());
//! assert_eq!("Hello Alice!", handlebars.render_template(r#"{{fluent "greeting" name="Alice"}}"#, &data).unwrap());
//! # }
//! }
//! ```
//!
//! ### Handlebars helper syntax.
//! The main helper provided is the `{{fluent}}` helper. If you have the
//! following Fluent file:
//!
//! ```fluent
//! foo-bar = "foo bar"
//! placeholder = this has a placeholder { $variable }
//! placeholder2 = this has { $variable1 } { $variable2 }
//! ```
//!
//! You can include the strings in your template with
//!
//! ```hbs
//! <!-- will render "foo bar" -->
//! {{fluent "foo-bar"}}
//! <!-- will render "this has a placeholder baz" -->
//! {{fluent "placeholder" variable="baz"}}
//!```
//!
//! You may also use the `{{fluentparam}}` helper to specify [variables],
//! especially if you need them to be multiline.
//!
//! ```hbs
//! {{#fluent "placeholder2"}}
//! {{#fluentparam "variable1"}}
//! first line
//! second line
//! {{/fluentparam}}
//! {{#fluentparam "variable2"}}
//! first line
//! second line
//! {{/fluentparam}}
//! {{/fluent}}
//! ```
//!
//!
//! [variables]: https://projectfluent.org/fluent/guide/variables.html
//! [`static_loader!`]: ./macro.static_loader.html
//! [`StaticLoader`]: ./struct.StaticLoader.html
//! [`ArcLoader`]: ./struct.ArcLoader.html
//! [`FluentLoader::with_default_lang`]: ./struct.FluentLoader.html#method.with_default_lang
//! [`handlebars::Context`]: https://docs.rs/handlebars/3.1.0/handlebars/struct.Context.html
#![warn(missing_docs)]
#[doc(hidden)]
pub extern crate fluent_bundle;
#[doc(hidden)]
pub type FluentBundle<R> =
fluent_bundle::bundle::FluentBundle<R, intl_memoizer::concurrent::IntlLangMemoizer>;
pub use error::LoaderError;
pub use loader::{ArcLoader, ArcLoaderBuilder, FluentLoader, Loader, MultiLoader, StaticLoader};
mod error;
#[doc(hidden)]
pub mod fs;
mod languages;
#[doc(hidden)]
pub mod loader;
#[cfg(feature = "macros")]
pub use fluent_template_macros::static_loader;
#[cfg(feature = "macros")]
pub use unic_langid::langid;
pub use unic_langid::LanguageIdentifier;
/// A convenience `Result` type that defaults to `error::Loader`.
pub type Result<T, E = error::LoaderError> = std::result::Result<T, E>;
#[cfg(test)]
mod tests {
use super::*;
use crate::Loader;
use unic_langid::{langid, LanguageIdentifier};
#[test]
fn check_if_loader_is_object_safe() {
const US_ENGLISH: LanguageIdentifier = langid!("en-US");
let loader = ArcLoader::builder("./tests/locales", US_ENGLISH)
.customize(|bundle| bundle.set_use_isolating(false))
.build()
.unwrap();
let loader: Box<dyn Loader> = Box::new(loader);
assert_eq!("Hello World!", loader.lookup(&US_ENGLISH, "hello-world"));
}
}
+259
View File
@@ -0,0 +1,259 @@
//! This modules contains both the `static_loader` and `ArcLoader`
//! implementations, as well as the `Loader` trait. Which provides a loader
//! agnostic interface.
#[cfg(feature = "handlebars")]
mod handlebars;
#[cfg(feature = "tera")]
mod tera;
mod multi_loader;
mod shared;
use std::borrow::Cow;
use std::collections::HashMap;
use crate::FluentBundle;
use fluent_bundle::{FluentArgs, FluentResource, FluentValue};
use fluent_langneg::negotiate_languages;
pub use unic_langid::{langid, langids, LanguageIdentifier};
mod arc_loader;
mod static_loader;
pub use arc_loader::{ArcLoader, ArcLoaderBuilder};
pub use multi_loader::MultiLoader;
pub use static_loader::StaticLoader;
/// A loader capable of looking up Fluent keys given a language.
pub trait Loader {
/// Look up `text_id` for `lang` in Fluent.
fn lookup(&self, lang: &LanguageIdentifier, text_id: &str) -> String {
self.lookup_complete(lang, text_id, None)
}
/// Look up `text_id` for `lang` with `args` in Fluent.
fn lookup_with_args(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: &HashMap<Cow<'static, str>, FluentValue>,
) -> String {
self.lookup_complete(lang, text_id, Some(args))
}
/// Look up `text_id` for `lang` in Fluent, using any `args` if provided.
fn lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> String;
/// Look up `text_id` for `lang` in Fluent.
fn try_lookup(&self, lang: &LanguageIdentifier, text_id: &str) -> Option<String> {
self.try_lookup_complete(lang, text_id, None)
}
/// Look up `text_id` for `lang` with `args` in Fluent.
fn try_lookup_with_args(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: &HashMap<Cow<'static, str>, FluentValue>,
) -> Option<String> {
self.try_lookup_complete(lang, text_id, Some(args))
}
/// Look up `text_id` for `lang` in Fluent, using any `args` if provided.
fn try_lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> Option<String>;
/// Returns an Iterator over the locales that are present.
fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_>;
}
impl<L> Loader for std::sync::Arc<L>
where
L: Loader,
{
fn lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> String {
L::lookup_complete(self, lang, text_id, args)
}
fn try_lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> Option<String> {
L::try_lookup_complete(self, lang, text_id, args)
}
fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
L::locales(self)
}
}
impl<L> Loader for &L
where
L: Loader + ?Sized,
{
fn lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> String {
L::lookup_complete(self, lang, text_id, args)
}
fn try_lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> Option<String> {
L::try_lookup_complete(self, lang, text_id, args)
}
fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
L::locales(self)
}
}
impl<T: Loader + ?Sized> Loader for Box<T> {
fn lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> String {
self.as_ref().lookup_complete(lang, text_id, args)
}
fn try_lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> Option<String> {
self.as_ref().try_lookup_complete(lang, text_id, args)
}
fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
self.as_ref().locales()
}
}
/// A `Loader` agnostic container type with optional trait implementations
/// for integrating with different libraries.
pub struct FluentLoader<L> {
loader: L,
#[allow(unused)]
default_lang: Option<LanguageIdentifier>,
}
impl<L> FluentLoader<L> {
/// Create a new `FluentLoader`.
pub fn new(loader: L) -> Self {
Self {
loader,
default_lang: None,
}
}
/// Set default language for this `FluentLoader`.
/// Template engines can use this value when rendering translations.
/// So far this feature is only implemented for Tera.
pub fn with_default_lang(self, lang: LanguageIdentifier) -> Self {
Self {
loader: self.loader,
default_lang: Some(lang),
}
}
}
/// Constructs a map of languages with a list of potential fallback languages.
pub fn build_fallbacks(
locales: &[LanguageIdentifier],
) -> HashMap<LanguageIdentifier, Vec<LanguageIdentifier>> {
let mut map = HashMap::new();
for locale in locales.iter() {
map.insert(
locale.to_owned(),
negotiate_languages(
&[locale],
locales,
None,
fluent_langneg::NegotiationStrategy::Filtering,
)
.into_iter()
.cloned()
.collect::<Vec<_>>(),
);
}
map
}
/// Creates a new static `FluentBundle` for `lang` using `resources`. Optionally
/// shared resources can be specified with `core_resource` and the bundle can
/// be customized with `customizer`.
fn create_bundle(
lang: LanguageIdentifier,
resources: &'static [FluentResource],
core_resource: Option<&'static FluentResource>,
customizer: &impl Fn(&mut FluentBundle<&'static FluentResource>),
) -> FluentBundle<&'static FluentResource> {
let mut bundle: FluentBundle<&'static FluentResource> =
FluentBundle::new_concurrent(vec![lang]);
if let Some(core) = core_resource {
bundle
.add_resource(core)
.expect("Failed to add core resource to bundle");
}
for res in resources {
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");
}
customizer(&mut bundle);
bundle
}
/// Maps from map of languages containing a list of resources to a map of
/// languages containing a `FluentBundle` of those resources.
pub fn build_bundles(
resources: &'static HashMap<LanguageIdentifier, Vec<FluentResource>>,
core_resource: Option<&'static FluentResource>,
customizer: impl Fn(&mut FluentBundle<&'static FluentResource>),
) -> HashMap<LanguageIdentifier, FluentBundle<&'static FluentResource>> {
let mut bundles = HashMap::new();
for (k, v) in resources.iter() {
bundles.insert(
k.clone(),
create_bundle(k.clone(), v, core_resource, &customizer),
);
}
bundles
}
fn map_to_fluent_args<'map, T: AsRef<str>>(map: &'map HashMap<T, FluentValue>) -> FluentArgs<'map> {
map.iter()
.map(|(key, value)| (key.as_ref(), value.clone()))
.collect()
}
@@ -0,0 +1,218 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::read_dir;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::languages::negotiate_languages;
use crate::FluentBundle;
use fluent_bundle::{FluentResource, FluentValue};
use crate::error::{LoaderError, LookupError};
pub use unic_langid::LanguageIdentifier;
type Customize = Option<Box<dyn FnMut(&mut FluentBundle<Arc<FluentResource>>)>>;
/// A builder pattern struct for constructing `ArcLoader`s.
pub struct ArcLoaderBuilder<'a, 'b> {
location: &'a Path,
fallback: LanguageIdentifier,
shared: Option<&'b [PathBuf]>,
customize: Customize,
}
impl<'a, 'b> ArcLoaderBuilder<'a, 'b> {
/// Adds Fluent resources that are shared across all localizations.
pub fn shared_resources<'b2>(
self,
shared: Option<&'b2 [PathBuf]>,
) -> ArcLoaderBuilder<'a, 'b2> {
ArcLoaderBuilder {
location: self.location,
fallback: self.fallback,
shared,
customize: self.customize,
}
}
/// Allows you to customise each `FluentBundle`.
pub fn customize(
mut self,
customize: impl FnMut(&mut FluentBundle<Arc<FluentResource>>) + 'static,
) -> Self {
self.customize = Some(Box::new(customize));
self
}
/// Constructs an `ArcLoader` from the settings provided.
pub fn build(mut self) -> Result<ArcLoader, Box<dyn std::error::Error>> {
let mut resources: HashMap<LanguageIdentifier, Vec<Arc<FluentResource>>> = HashMap::new();
for entry in read_dir(self.location)? {
let entry = entry?;
let file_type = entry.file_type()?;
if file_type.is_dir() {
if let Ok(lang) = entry.file_name().into_string() {
let lang = lang.parse::<LanguageIdentifier>()?;
let lang_resources = crate::fs::read_from_dir(entry.path())?
.into_iter()
.map(Arc::new)
.collect::<Vec<_>>();
resources.entry(lang).or_default().extend(lang_resources);
}
} else if file_type.is_file()
&& entry.path().extension().is_some_and(|e| e == "ftl")
{
if let Some(stem) = entry.path().file_stem().and_then(|s| s.to_str()) {
if let Ok(lang) = stem.parse::<LanguageIdentifier>() {
let res = Arc::new(crate::fs::read_from_file(entry.path())?);
resources.entry(lang).or_default().push(res);
}
}
}
}
let mut bundles = HashMap::new();
for (lang, v) in resources.iter() {
let mut bundle = FluentBundle::new_concurrent(vec![lang.clone()]);
for shared_resource in self.shared.unwrap_or(&[]) {
bundle
.add_resource(Arc::new(crate::fs::read_from_file(shared_resource)?))
.map_err(|errors| LoaderError::FluentBundle { errors })?;
}
for res in v {
bundle
.add_resource(res.clone())
.map_err(|errors| LoaderError::FluentBundle { errors })?;
}
if let Some(customize) = self.customize.as_mut() {
(customize)(&mut bundle);
}
bundles.insert(lang.clone(), bundle);
}
let fallbacks = super::build_fallbacks(&resources.keys().cloned().collect::<Vec<_>>());
Ok(ArcLoader {
bundles,
fallbacks,
fallback: self.fallback,
})
}
}
/// A loader that uses `Arc<FluentResource>` as its backing storage. This is
/// mainly useful for when you need to load fluent at run time. You can
/// configure the initialisation with `ArcLoaderBuilder`.
/// ```no_run
/// use fluent_templates::ArcLoader;
///
/// let loader = ArcLoader::builder("locales/", unic_langid::langid!("en-US"))
/// .shared_resources(Some(&["locales/core.ftl".into()]))
/// .customize(|bundle| bundle.set_use_isolating(false))
/// .build()
/// .unwrap();
/// ```
pub struct ArcLoader {
bundles: HashMap<LanguageIdentifier, FluentBundle<Arc<FluentResource>>>,
fallback: LanguageIdentifier,
fallbacks: HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
}
impl super::Loader for ArcLoader {
// Traverse the fallback chain,
fn lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> String {
for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
return val;
}
}
if *lang != self.fallback {
if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
return val;
}
}
format!("Unknown localization key: {text_id:?}")
}
// Traverse the fallback chain,
fn try_lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> Option<String> {
for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
return Some(val);
}
}
if *lang != self.fallback {
if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
return Some(val);
}
}
None
}
fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
Box::new(self.fallbacks.keys())
}
}
impl ArcLoader {
/// Creates a new `ArcLoaderBuilder`
pub fn builder<'a, P: AsRef<Path> + ?Sized>(
location: &'a P,
fallback: LanguageIdentifier,
) -> ArcLoaderBuilder<'a, 'static> {
ArcLoaderBuilder {
location: location.as_ref(),
fallback,
shared: None,
customize: None,
}
}
/// Convenience function to look up a string for a single language
pub fn lookup_single_language<T: AsRef<str>>(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<T, FluentValue>>,
) -> Result<String, LookupError> {
super::shared::lookup_single_language(&self.bundles, lang, text_id, args)
}
/// Convenience function to look up a string without falling back to the
/// default fallback language
pub fn lookup_no_default_fallback<S: AsRef<str>>(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<S, FluentValue>>,
) -> Option<String> {
super::shared::lookup_no_default_fallback(
&self.bundles,
&self.fallbacks,
lang,
text_id,
args,
)
}
/// Return the fallback language
pub fn fallback(&self) -> &LanguageIdentifier {
&self.fallback
}
}
@@ -0,0 +1,137 @@
use handlebars::{
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderErrorReason,
Renderable,
};
use fluent_bundle::FluentValue;
use handlebars::template::{Parameter, TemplateElement};
use serde_json::Value as Json;
use std::borrow::Cow;
use std::collections::HashMap;
use crate::{FluentLoader, Loader};
#[derive(Default)]
struct StringOutput {
pub s: String,
}
impl Output for StringOutput {
fn write(&mut self, seg: &str) -> Result<(), std::io::Error> {
self.s.push_str(seg);
Ok(())
}
}
impl<L: Loader + Send + Sync> HelperDef for FluentLoader<L> {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'rc>,
reg: &'reg Handlebars,
context: &'rc Context,
rcx: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> HelperResult {
let id = if let Some(id) = h.param(0) {
id
} else {
return Err(RenderErrorReason::ParamNotFoundForIndex("fluent", 0).into());
};
if id.relative_path().is_some() {
return Err(RenderErrorReason::ParamTypeMismatchForName(
"fluent",
"0".to_string(),
"takes a string parameter with no path".to_string(),
)
.into());
}
let id = if let Json::String(ref s) = *id.value() {
s
} else {
return Err(RenderErrorReason::ParamTypeMismatchForName(
"fluent",
"0".to_string(),
"string".to_string(),
)
.into());
};
let mut args: Option<HashMap<Cow<'static, str>, FluentValue>> = if h.hash().is_empty() {
None
} else {
let map = h
.hash()
.iter()
.filter_map(|(k, v)| {
let json = v.value();
let val = match json {
// `Number::as_f64` can't fail here because we haven't
// enabled `arbitrary_precision` feature
// in `serde_json`.
Json::Number(n) => n.as_f64().unwrap().into(),
Json::String(s) => s.to_owned().into(),
_ => return None,
};
Some((Cow::from(k.to_string()), val))
})
.collect();
Some(map)
};
if let Some(tpl) = h.template() {
if args.is_none() {
args = Some(HashMap::new());
}
let args = args.as_mut().unwrap();
for element in &tpl.elements {
if let TemplateElement::HelperBlock(ref block) = element {
if block.name != Parameter::Name("fluentparam".into()) {
return Err(RenderErrorReason::Other(format!(
"{{{{fluent}}}} can only contain {{{{fluentparam}}}} elements, not {}",
block.name.expand_as_name(reg, context, rcx).unwrap()
))
.into());
}
let id = if let Some(el) = block.params.first() {
if let Parameter::Literal(Json::String(ref s)) = *el {
s
} else {
return Err(RenderErrorReason::ParamTypeMismatchForName(
"fluentparam",
"0".into(),
"string".into(),
)
.into());
}
} else {
return Err(
RenderErrorReason::ParamNotFoundForIndex("fluentparam", 0).into()
);
};
if let Some(ref tpl) = block.template {
let mut s = StringOutput::default();
tpl.render(reg, context, rcx, &mut s)?;
args.insert(
Cow::Owned(String::from(id)),
FluentValue::String(s.s.into()),
);
}
}
}
}
let lang = context
.data()
.get("lang")
.expect("Language not set in context")
.as_str()
.expect("Language must be string")
.parse()
.expect("Language not valid identifier");
let response = self.loader.lookup_complete(&lang, id, args.as_ref());
out.write(&response)
.map_err(|error| RenderErrorReason::NestedError(Box::new(error)).into())
}
}
@@ -0,0 +1,130 @@
use crate::Loader;
use fluent_bundle::FluentValue;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
pub use unic_langid::LanguageIdentifier;
/// A loader comprised of other loaders.
///
/// This loader allows for loaders with multiple sources to be used
/// from a single one, instead of a multiple of them.
///
/// The idea behind this loader is to allow for the scenario where you depend
/// on crates that have their own loader (think of protocol crates which
/// are dependencies of a frontend -> the frontend needs to know each of
/// the protocol's messages and be able to display them). Using a multiloader
/// allows you to query multiple localization sources from one single source.
///
/// Note that a [`MultiLoader`] is most useful where each of your fluent modules
/// is specially namespaced to avoid name collisions.
///
/// # Usage
/// ```rust
/// use fluent_templates::{ArcLoader, StaticLoader, MultiLoader, Loader};
/// use unic_langid::{LanguageIdentifier, langid};
///
/// const US_ENGLISH: LanguageIdentifier = langid!("en-US");
/// const CHINESE: LanguageIdentifier = langid!("zh-CN");
///
/// fluent_templates::static_loader! {
/// static LOCALES = {
/// locales: "./tests/locales",
/// fallback_language: "en-US",
/// // Removes unicode isolating marks around arguments, you typically
/// // should only set to false when testing.
/// customise: |bundle| bundle.set_use_isolating(false),
/// };
/// }
///
/// fn main() {
/// let cn_loader = ArcLoader::builder("./tests/locales", CHINESE)
/// .customize(|bundle| bundle.set_use_isolating(false))
/// .build()
/// .unwrap();
///
/// let mut multiloader = MultiLoader::from_iter([
/// Box::new(&*LOCALES) as Box<dyn Loader>,
/// ]);
/// multiloader.push_back(Box::new(cn_loader) as Box<dyn Loader>);
/// assert_eq!("Hello World!", multiloader.lookup(&US_ENGLISH, "hello-world"));
/// assert_eq!("儿", multiloader.lookup(&CHINESE, "exists"));
/// }
/// ```
///
/// # Order of search
/// The one that is inserted first is also the one searched first.
pub struct MultiLoader<T = Box<dyn Loader + Sync + Send>> {
loaders: VecDeque<T>,
}
impl<T> MultiLoader<T> {
/// Creates a [`MultiLoader`] without any loaders.
pub fn new() -> Self {
Self::default()
}
/// Creates a [`MultiLoader`] from an iterator of loaders.
pub fn from_iter(iter: impl IntoIterator<Item = T>) -> Self {
Self {
loaders: iter.into_iter().collect(),
}
}
/// Pushes a loader in front of all the others in terms of precedence.
pub fn push_front(&mut self, loader: T) {
self.loaders.push_front(loader);
}
/// Pushes a loader at the back in terms of precedence.
pub fn push_back(&mut self, loader: T) {
self.loaders.push_back(loader);
}
/// Pushes a loader at the back in terms of precedence.
pub fn remove(&mut self, idx: usize) -> Option<T> {
self.loaders.remove(idx)
}
}
impl<T> Default for MultiLoader<T> {
fn default() -> Self {
Self {
loaders: VecDeque::default(),
}
}
}
impl<T: Loader> crate::Loader for MultiLoader<T> {
fn lookup_complete(
&self,
lang: &unic_langid::LanguageIdentifier,
text_id: &str,
args: Option<&std::collections::HashMap<Cow<'static, str>, fluent_bundle::FluentValue>>,
) -> String {
for loader in self.loaders.iter() {
if let Some(text) = loader.try_lookup_complete(lang, text_id, args) {
return text;
}
}
format!("Unknown localization key: {text_id:?}")
}
fn try_lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> Option<String> {
for loader in self.loaders.iter() {
if let Some(text) = loader.try_lookup_complete(lang, text_id, args) {
return Some(text);
}
}
None
}
fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
Box::new(self.loaders.iter().flat_map(|loader| loader.locales()))
}
}
@@ -0,0 +1,66 @@
use std::borrow::Borrow;
use std::collections::HashMap;
use crate::{error::LookupError, FluentBundle};
use fluent_bundle::{FluentResource, FluentValue};
pub use unic_langid::LanguageIdentifier;
pub fn lookup_single_language<T: AsRef<str>, R: Borrow<FluentResource>>(
bundles: &HashMap<LanguageIdentifier, FluentBundle<R>>,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<T, FluentValue>>,
) -> Result<String, LookupError> {
let bundle = bundles
.get(lang)
.ok_or_else(|| LookupError::LangNotLoaded(lang.clone()))?;
let mut errors = Vec::new();
let message_retrieve_error = || LookupError::MessageRetrieval(text_id.to_owned());
let pattern = if let Some((msg, attr)) = text_id.split_once('.') {
bundle
.get_message(msg)
.ok_or_else(message_retrieve_error)?
.attributes()
.find(|attribute| attribute.id() == attr)
.ok_or_else(|| LookupError::AttributeNotFound {
message_id: msg.to_owned(),
attribute: attr.to_owned(),
})?
.value()
} else {
bundle
.get_message(text_id)
.ok_or_else(message_retrieve_error)?
.value()
.ok_or_else(message_retrieve_error)?
};
let args = args.map(super::map_to_fluent_args);
let value = bundle.format_pattern(pattern, args.as_ref(), &mut errors);
if errors.is_empty() {
Ok(value.into())
} else {
Err(LookupError::FluentError(errors))
}
}
pub fn lookup_no_default_fallback<S: AsRef<str>, R: Borrow<FluentResource>>(
bundles: &HashMap<LanguageIdentifier, FluentBundle<R>>,
fallbacks: &HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<S, FluentValue>>,
) -> Option<String> {
let fallbacks = fallbacks.get(lang)?;
for l in fallbacks {
if let Ok(val) = lookup_single_language(bundles, l, text_id, args) {
return Some(val);
}
}
None
}
@@ -0,0 +1,110 @@
use std::borrow::Cow;
use std::collections::HashMap;
use crate::{error::LookupError, languages::negotiate_languages, FluentBundle};
use fluent_bundle::{FluentResource, FluentValue};
pub use unic_langid::LanguageIdentifier;
/// A simple Loader implementation, with statically-loaded fluent data.
/// Typically created with the [`static_loader!`] macro
///
/// [`static_loader!`]: ./macro.static_loader.html
pub struct StaticLoader {
bundles: &'static HashMap<LanguageIdentifier, FluentBundle<&'static FluentResource>>,
fallbacks: &'static HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
fallback: LanguageIdentifier,
}
impl StaticLoader {
/// Construct a new `StaticLoader`.
///
/// This is exposed as publicly so that it can be used inside the
/// `static_loader!` macro. it's not meant to be called directly.
#[doc(hidden)]
pub fn new(
bundles: &'static HashMap<LanguageIdentifier, FluentBundle<&'static FluentResource>>,
fallbacks: &'static HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
fallback: LanguageIdentifier,
) -> Self {
Self {
bundles,
fallbacks,
fallback,
}
}
/// Convenience function to look up a string for a single language
pub fn lookup_single_language<S: AsRef<str>>(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<S, FluentValue>>,
) -> Result<String, LookupError> {
super::shared::lookup_single_language(self.bundles, lang, text_id, args)
}
/// Convenience function to look up a string without falling back to the
/// default fallback language
pub fn lookup_no_default_fallback<S: AsRef<str>>(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<S, FluentValue>>,
) -> Option<String> {
super::shared::lookup_no_default_fallback(self.bundles, self.fallbacks, lang, text_id, args)
}
/// Return the fallback language
pub fn fallback(&self) -> &LanguageIdentifier {
&self.fallback
}
}
impl super::Loader for StaticLoader {
// Traverse the fallback chain,
fn lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> String {
for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
return val;
}
}
if *lang != self.fallback {
if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
return val;
}
}
format!("Unknown localization key: {text_id:?}")
}
// Traverse the fallback chain,
fn try_lookup_complete(
&self,
lang: &LanguageIdentifier,
text_id: &str,
args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
) -> Option<String> {
for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
return Some(val);
}
}
if *lang != self.fallback {
if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
return Some(val);
}
}
None
}
fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
Box::new(self.fallbacks.keys())
}
}
@@ -0,0 +1,78 @@
use fluent_bundle::FluentValue;
use serde_json::Value as Json;
use std::borrow::Cow;
use std::collections::HashMap;
use unic_langid::LanguageIdentifier;
use crate::Loader;
const LANG_KEY: &str = "lang";
const FLUENT_KEY: &str = "key";
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("No `lang` argument provided.")]
NoLangArgument,
#[error("`lang` must be a valid unicode language identifier.")]
LangArgumentInvalid,
#[error("No `id` argument provided.")]
NoFluentArgument,
#[error("Couldn't convert JSON to Fluent value.")]
JsonToFluentFail,
}
impl From<Error> for tera::Error {
fn from(error: Error) -> Self {
tera::Error::msg(error)
}
}
fn json_to_fluent(json: Json) -> crate::Result<FluentValue<'static>, Error> {
match json {
Json::Number(n) if n.is_u64() => Ok(FluentValue::from(n.as_u64().unwrap())),
Json::Number(n) if n.is_f64() => Ok(FluentValue::from(n.as_f64().unwrap())),
Json::String(s) => Ok(FluentValue::String(s.into())),
_ => Err(Error::JsonToFluentFail),
}
}
fn parse_language(arg: &Json) -> crate::Result<LanguageIdentifier, Error> {
arg.as_str()
.ok_or(Error::LangArgumentInvalid)?
.parse::<LanguageIdentifier>()
.ok()
.ok_or(Error::LangArgumentInvalid)
}
impl<L: Loader + Send + Sync> tera::Function for crate::FluentLoader<L> {
fn call(&self, args: &HashMap<String, Json>) -> Result<Json, tera::Error> {
let lang_arg = args.get(LANG_KEY).map(parse_language).transpose()?;
let lang = lang_arg
.as_ref()
.or(self.default_lang.as_ref())
.ok_or(Error::NoLangArgument)?;
let id = args
.get(FLUENT_KEY)
.and_then(Json::as_str)
.ok_or(Error::NoFluentArgument)?;
/// Filters kwargs to exclude ones used by this function and tera.
fn is_not_tera_key((k, _): &(&String, &Json)) -> bool {
let k = &**k;
!(k == LANG_KEY || k == FLUENT_KEY || k == "__tera_one_off")
}
let mut fluent_args = HashMap::new();
for (key, value) in args.iter().filter(is_not_tera_key) {
fluent_args.insert(
Cow::from(heck::ToKebabCase::to_kebab_case(&**key)),
json_to_fluent(value.clone())?,
);
}
let response = self.loader.lookup_with_args(lang, id, &fluent_args);
Ok(Json::String(response))
}
}
@@ -0,0 +1 @@
-something = foo
@@ -0,0 +1,3 @@
hello-world = Hallo Welt!
greeting = Hallo { $name }!
.placeholder = Hallo Fruend!
@@ -0,0 +1,2 @@
# Top-level locale file (no subdir)
top-level = top-level value
@@ -0,0 +1,12 @@
hello-world = Hello World!
greeting = Hello { $name }!
.placeholder = Hello Friend!
simple = simple text
reference = simple text with a reference: { -something }
parameter = text with a { $param }
parameter2 = text one { $param } second { $multi-word-param }
email = text with an EMAIL("example@example.org")
fallback = this should fall back
@@ -0,0 +1,12 @@
hello-world = Bonjour le monde!
greeting = Bonjour { $name }!
.placeholder = Salut l'ami!
simple = texte simple
reference = texte simple avec une référence: { -something }
parameter = texte avec une { $param }
parameter2 = texte une { $param } seconde { $multi-word-param }
email = text avec un EMAIL("example@example.org")
# no fallback
@@ -0,0 +1 @@
exists = 儿
@@ -0,0 +1,2 @@
exists = 兒
# expect to fall back to zh-CN
@@ -0,0 +1 @@
fallback-zh = 气
@@ -0,0 +1,62 @@
#![allow(clippy::incompatible_msrv)]
use std::{ops::Deref, sync::LazyLock};
use fluent_templates::{ArcLoader, Loader, MultiLoader};
use unic_langid::{langid, LanguageIdentifier};
fluent_templates::static_loader! {
// Declare our `StaticLoader` named `LOCALES`.
static LOCALES = {
// The directory of localisations and fluent resources.
locales: "./tests/locales",
// The language to falback on if something is not present.
fallback_language: "en-US",
// Optional: A fluent resource that is shared with every locale.
core_locales: "./tests/locales/core.ftl",
};
}
const US_ENGLISH: LanguageIdentifier = langid!("en-US");
#[test]
fn check_if_multiloader_works() {
const CHINESE: LanguageIdentifier = langid!("zh-CN");
let en_loader = ArcLoader::builder("./tests/locales", US_ENGLISH)
.customize(|bundle| bundle.set_use_isolating(false))
.build()
.unwrap();
let cn_loader = ArcLoader::builder("./tests/locales", CHINESE)
.customize(|bundle| bundle.set_use_isolating(false))
.build()
.unwrap();
let multiloader = MultiLoader::from_iter([
Box::new(LOCALES.deref()) as Box<dyn Loader>,
Box::new(en_loader) as Box<dyn Loader>,
Box::new(cn_loader) as Box<dyn Loader>,
]);
assert_eq!(
"Hello World!",
multiloader.lookup(&US_ENGLISH, "hello-world")
);
assert_eq!("儿", multiloader.lookup(&CHINESE, "exists"));
}
// test that MultiLoader can be used in a static
static _STATIC_MULTI_LOADER: LazyLock<MultiLoader> = LazyLock::new(|| {
let mut multi = MultiLoader::new();
multi.push_back(Box::new(&*LOCALES) as Box<dyn Loader + Send + Sync>);
match ArcLoader::builder("./tests/locales", US_ENGLISH)
.customize(|bundle| bundle.set_use_isolating(false))
.build()
{
Ok(en_loader) => multi.push_front(Box::new(en_loader)),
Err(err) => log::error!("Failed to load runtime localization overrides: {err}"),
}
multi
});
+190
View File
@@ -0,0 +1,190 @@
#![allow(unused)]
#![allow(clippy::incompatible_msrv)]
fluent_templates::static_loader! {
static LOCALES = {
locales: "./tests/locales",
fallback_language: "en-US",
core_locales: "./tests/locales/core.ftl",
customise: |bundle| bundle.set_use_isolating(false),
};
}
fluent_templates::static_loader! {
pub(crate) static _LOCALES = {
locales: "./tests/locales",
fallback_language: "en-US"
};
}
macro_rules! make_loaders {
() => {{
let arc = ArcLoader::builder("tests/locales", unic_langid::langid!("en-US"))
.shared_resources(Some(&["./tests/locales/core.ftl".into()]))
.customize(|bundle| bundle.set_use_isolating(false))
.build()
.unwrap();
(FluentLoader::new(&*crate::LOCALES), FluentLoader::new(arc))
}};
}
/// Generates tests for each loader in different locales.
macro_rules! generate_tests {
($(fn $locale_test_fn:ident ($template_engine:ident, $locale:expr) {
$($assert_macro:ident ! ( $lhs:expr , $rhs:expr ) );* $(;)?
})+) => {
use fluent_templates::*;
$(
#[test]
fn $locale_test_fn() {
let data = serde_json::json!({"lang": $locale});
#[cfg(feature = "handlebars")]
if stringify!($template_engine) == "handlebars" {
let (static_loader, arc_loader) = make_loaders!();
let mut static_handlebars = handlebars::Handlebars::new();
static_handlebars.register_helper("fluent", Box::new(static_loader));
let mut arc_handlebars = handlebars::Handlebars::new();
arc_handlebars.register_helper("fluent", Box::new(arc_loader));
$(
$assert_macro ! (
static_handlebars
.render_template($lhs, &data)
.unwrap(),
$rhs
);
$assert_macro ! (
arc_handlebars
.render_template($lhs, &data)
.unwrap(),
$rhs
);
)*
}
#[cfg(feature = "tera")]
if stringify!($template_engine) == "tera" {
let (static_loader, arc_loader) = make_loaders!();
let mut static_tera = tera::Tera::default();
static_tera.register_function("fluent", static_loader);
let mut arc_tera = tera::Tera::default();
arc_tera.register_function("fluent", arc_loader);
$(
let lhs = $lhs.replace("{lang}", $locale);
$assert_macro ! (
static_tera
.render_str(&lhs, &tera::Context::from_value(data.clone()).unwrap())
.unwrap(),
$rhs
);
$assert_macro ! (
arc_tera
.render_str(&lhs, &tera::Context::from_value(data.clone()).unwrap())
.unwrap(),
$rhs
);
)*
}
}
)+
};
}
#[cfg(feature = "handlebars")]
mod handlebars {
generate_tests! {
fn english(handlebars, "en-US") {
assert_eq!(r#"{{fluent "simple"}}"#, "simple text");
assert_eq!(r#"{{fluent "reference"}}"#, "simple text with a reference: foo");
assert_eq!(r#"{{fluent "parameter" param="PARAM"}}"#, "text with a PARAM");
assert_eq!(r#"{{fluent "parameter2" param="P1" multi-word-param="P2"}}"#, "text one P1 second P2");
assert_eq!(r#"{{#fluent "parameter"}}{{#fluentparam "param"}}blah blah{{/fluentparam}}{{/fluent}}"#, "text with a blah blah");
assert_eq!(r#"{{#fluent "parameter2"}}{{#fluentparam "param"}}foo{{/fluentparam}}{{#fluentparam "multi-word-param"}}bar{{/fluentparam}}{{/fluent}}"#, "text one foo second bar");
assert_eq!(r#"{{fluent "top-level"}}"#, "top-level value");
assert_eq!(r#"{{fluent "fallback"}}"#, "this should fall back");
}
fn french(handlebars, "fr") {
assert_eq!(r#"{{fluent "simple"}}"#, "texte simple");
assert_eq!(r#"{{fluent "reference"}}"#, "texte simple avec une référence: foo");
assert_eq!(r#"{{fluent "parameter" param="PARAM"}}"#, "texte avec une PARAM");
assert_eq!(r#"{{fluent "parameter2" param="P1" multi-word-param="P2"}}"#, "texte une P1 seconde P2");
assert_eq!(r#"{{#fluent "parameter"}}{{#fluentparam "param"}}blah blah{{/fluentparam}}{{/fluent}}"#, "texte avec une blah blah");
assert_eq!(r#"{{#fluent "parameter2"}}{{#fluentparam "param"}}foo{{/fluentparam}}{{#fluentparam "multi-word-param"}}bar{{/fluentparam}}{{/fluent}}"#, "texte une foo seconde bar");
assert_eq!(r#"{{fluent "fallback"}}"#, "this should fall back");
}
fn chinese(handlebars, "zh-TW") {
assert_eq!(r#"{{fluent "exists"}}"#, "兒");
assert_eq!(r#"{{fluent "fallback-zh"}}"#, "气");
assert_eq!(r#"{{fluent "fallback"}}"#, "this should fall back");
}
}
}
#[cfg(feature = "tera")]
mod tera {
generate_tests! {
fn english(tera, "en-US") {
assert_eq!(r#"{{ fluent(key="simple", lang="{lang}") }}"#, "simple text");
assert_eq!(r#"{{ fluent(key="reference", lang="{lang}") }}"#, "simple text with a reference: foo");
assert_eq!(r#"{{ fluent(key="parameter", lang="{lang}", param="PARAM") }}"#, "text with a PARAM");
assert_eq!(r#"{{ fluent(key="parameter2", lang="{lang}", param="P1", multi_word_param="P2") }}"#, "text one P1 second P2");
assert_eq!(r#"{{ fluent(key="top-level", lang="{lang}") }}"#, "top-level value");
assert_eq!(r#"{{ fluent(key="fallback", lang="{lang}") }}"#, "this should fall back");
}
fn french(tera, "fr") {
assert_eq!(r#"{{ fluent(key="simple", lang="{lang}") }}"#, "texte simple");
assert_eq!(r#"{{ fluent(key="reference", lang="{lang}") }}"#, "texte simple avec une référence: foo");
assert_eq!(r#"{{ fluent(key="parameter", param="PARAM", lang="{lang}") }}"#, "texte avec une PARAM");
assert_eq!(r#"{{ fluent(key="parameter2", param="P1", multi_word_param="P2", lang="{lang}") }}"#, "texte une P1 seconde P2");
assert_eq!(r#"{{ fluent(key="fallback", lang="{lang}") }}"#, "this should fall back");
}
fn chinese(tera, "zh-TW") {
assert_eq!(r#"{{ fluent(key="exists", lang="{lang}") }}"#, "兒");
assert_eq!(r#"{{ fluent(key="fallback-zh", lang="{lang}") }}"#, "气");
assert_eq!(r#"{{ fluent(key="fallback", lang="{lang}") }}"#, "this should fall back");
}
}
/// Default lang argument works
#[test]
fn use_default_lang() {
let loader = FluentLoader::new(&*super::LOCALES).with_default_lang("de".parse().unwrap());
let mut tera = tera::Tera::default();
tera.register_function("fluent", loader);
let context = tera::Context::new();
assert_eq!(
tera.render_str(r#"{{ fluent(key="hello-world") }}"#, &context)
.unwrap(),
"Hallo Welt!"
);
assert_eq!(
tera.render_str(r#"{{ fluent(key="hello-world", lang="fr") }}"#, &context)
.unwrap(),
"Bonjour le monde!"
);
}
/// Rendering fails when no default and no explicit lang argument is provided
#[test]
fn no_default_and_no_argument_error() {
let loader = FluentLoader::new(&*super::LOCALES);
let mut tera = tera::Tera::default();
tera.register_function("fluent", loader);
let context = tera::Context::new();
assert!(tera
.render_str(r#"{{ fluent(key="hellow-world") }}"#, &context)
.is_err());
}
}