From 99454c8f5fa7425f5fe15412bd034415a2fd0c02 Mon Sep 17 00:00:00 2001 From: kgv Date: Sun, 12 Nov 2023 12:43:51 +0300 Subject: [PATCH] 0.9.0 --- Cargo.toml | 14 +-- README.md | 34 ++++---- doc/en-US.md | 57 +++---------- doc/ru-RU.md | 59 ++++--------- .../fallback_chain/assets/locales/.ftl.ron | 5 ++ .../en-US/{hello_world.ftl => main.ftl} | 2 +- .../assets/locales/en-US/main.ftl.ron | 6 -- .../ru/ru-BY/{hello_world.ftl => main.ftl} | 0 .../assets/locales/ru/ru-BY/main.ftl.ron | 6 -- .../ru/ru-RU/{hello_world.ftl => main.ftl} | 0 .../assets/locales/ru/ru-RU/main.ftl.ron | 6 -- examples/fallback_chain/main.rs | 49 ++++++----- .../locales/{en-US/main.ftl.yml => .ftl.yml} | 4 +- .../en-US/{hello_world.ftl => main.ftl} | 0 examples/minimal/main.rs | 4 +- examples/multilingual/assets/locales/.ftl.ron | 5 ++ .../multilingual/assets/locales/de-DE.ftl | 3 + .../multilingual/assets/locales/en-US.ftl | 3 + .../multilingual/assets/locales/ru-RU.ftl | 3 + examples/multilingual/main.rs | 43 ++++++++++ examples/ui/assets/locales/.ftl.ron | 7 ++ examples/ui/assets/locales/de-DE/menu.ftl.ron | 6 -- examples/ui/assets/locales/en-US/menu.ftl.ron | 6 -- .../ui/assets/locales/ru/ru-BY/menu.ftl.ron | 6 -- .../ui/assets/locales/ru/ru-RU/menu.ftl.ron | 6 -- .../ui/assets/locales/ru/ru-RU/play/ui.ftl | 1 - examples/ui/assets/locales/und/menu.ftl.ron | 6 -- examples/ui/locales.rs | 22 ----- examples/ui/main.rs | 18 ++-- examples/ui/resources/mod.rs | 25 +----- examples/ui/systems/load.rs | 42 +++++---- examples/ui/systems/menu.rs | 56 +++++------- examples/ui/systems/mod.rs | 1 - examples/ui/systems/parameters/mod.rs | 24 ------ src/assets/bundle.rs | 85 ++++++++----------- src/assets/resource.rs | 26 +++--- src/exts/fluent/bundle.rs | 14 --- src/exts/fluent/mod.rs | 3 - src/exts/mod.rs | 1 - src/lib.rs | 7 +- src/resources/bundles.rs | 60 +++++++++++++ src/resources/locale.rs | 47 ---------- src/resources/locales.rs | 46 ++++++++++ src/resources/localization.rs | 77 ----------------- src/resources/mod.rs | 6 +- src/systems/mod.rs | 6 -- src/systems/parameters/mod.rs | 62 -------------- 47 files changed, 374 insertions(+), 595 deletions(-) create mode 100644 examples/fallback_chain/assets/locales/.ftl.ron rename examples/fallback_chain/assets/locales/en-US/{hello_world.ftl => main.ftl} (51%) delete mode 100644 examples/fallback_chain/assets/locales/en-US/main.ftl.ron rename examples/fallback_chain/assets/locales/ru/ru-BY/{hello_world.ftl => main.ftl} (100%) delete mode 100644 examples/fallback_chain/assets/locales/ru/ru-BY/main.ftl.ron rename examples/fallback_chain/assets/locales/ru/ru-RU/{hello_world.ftl => main.ftl} (100%) delete mode 100644 examples/fallback_chain/assets/locales/ru/ru-RU/main.ftl.ron rename examples/minimal/assets/locales/{en-US/main.ftl.yml => .ftl.yml} (57%) rename examples/minimal/assets/locales/en-US/{hello_world.ftl => main.ftl} (100%) create mode 100644 examples/multilingual/assets/locales/.ftl.ron create mode 100644 examples/multilingual/assets/locales/de-DE.ftl create mode 100644 examples/multilingual/assets/locales/en-US.ftl create mode 100644 examples/multilingual/assets/locales/ru-RU.ftl create mode 100644 examples/multilingual/main.rs create mode 100644 examples/ui/assets/locales/.ftl.ron delete mode 100644 examples/ui/assets/locales/de-DE/menu.ftl.ron delete mode 100644 examples/ui/assets/locales/en-US/menu.ftl.ron delete mode 100644 examples/ui/assets/locales/ru/ru-BY/menu.ftl.ron delete mode 100644 examples/ui/assets/locales/ru/ru-RU/menu.ftl.ron delete mode 100644 examples/ui/assets/locales/ru/ru-RU/play/ui.ftl delete mode 100644 examples/ui/assets/locales/und/menu.ftl.ron delete mode 100644 examples/ui/locales.rs delete mode 100644 examples/ui/systems/parameters/mod.rs delete mode 100644 src/exts/fluent/bundle.rs delete mode 100644 src/exts/fluent/mod.rs delete mode 100644 src/exts/mod.rs create mode 100644 src/resources/bundles.rs delete mode 100644 src/resources/locale.rs create mode 100644 src/resources/locales.rs delete mode 100644 src/resources/localization.rs delete mode 100644 src/systems/mod.rs delete mode 100644 src/systems/parameters/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 218e51e..d00d3db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_fluent" -version = "0.8.0" +version = "0.9.0" authors = ["g "] edition = "2021" description = "Bevy plugin for localization using Fluent" @@ -18,23 +18,23 @@ categories = [ exclude = [".github/**/*"] [dependencies] -bevy = { version = "0.12", default-features = false, features = ["bevy_asset"] } +bevy = { version = "0.12.0", default-features = false, features = [ + "bevy_asset", +] } fluent = "0.16.0" fluent_content = "0.0.5" fluent-langneg = "0.13.0" -futures-lite = "2.0.0" +futures-lite = "2.0.1" indexmap = { version = "2.1.0", features = ["serde"] } intl-memoizer = "0.5.1" ron = "0.8.1" -serde = { version = "1.0.188", features = ["derive"] } +serde = { version = "1.0.192", features = ["derive"] } serde_yaml = "0.9.27" thiserror = "1.0.50" tracing = "0.1.40" unic-langid = { version = "0.9.1", features = ["serde"] } uuid = { version = "1.5.0", features = ["serde", "v4", "v5"] } -# fluent-syntax = { git = "https://github.com/projectfluent/fluent-rs" } -# globset = "0.4.13" [dev-dependencies] -bevy = "0.12" +bevy = "0.12.0" unic-langid = { version = "0.9.1", features = ["macros"] } diff --git a/README.md b/README.md index 7959c21..0075bfc 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ | bevy | bevy_fluent | |------|-------------| -| 0.12 | 0.8 | +| 0.12 | 0.8, 0.9 | | 0.11 | 0.7 | | 0.10 | 0.6 | | 0.9 | 0.5 | @@ -25,21 +25,23 @@ ## See Also - [Bevy][bevy] ❤️ -- [Bevy localisation plugin issue][bevy-localisation-plugin-issue] +- [Bevy localisation plugin issue](https://github.com/bevyengine/bevy/issues/461) *** - [Fluent][fluent] -- [Fluent fallback][fluent-fallback] -- [Fluent language negotiation][fluent-langneg] -- [Fluent message format 2.0][fluent-message-format-2.0] -- [Fluent resource manager][fluent-resmgr] -- [L10nRegistry][l10nregistry] +- [Fluent fallback](https://github.com/projectfluent/fluent-rs/tree/master/fluent-fallback) +- [Fluent language negotiation](https://github.com/projectfluent/fluent-langneg-rs) +- [Fluent message format 2.0](https://github.com/zbraniecki/message-format-2.0-rs) +- [Fluent resource manager](https://github.com/projectfluent/fluent-rs/tree/master/fluent-resmgr) +- [L10nRegistry](https://github.com/zbraniecki/l10nregistry-rs) *** - [Locales](https://github.com/unicode-org/cldr-json/blob/master/cldr-json/cldr-core/availableLocales.json) -- [Unicode Language Identifier][unicode-language-identifier] +- [Message format working group](https://github.com/unicode-org/message-format-wg) +- [Unicode Language Identifier](http://unicode.org/reports/tr35/#Unicode_language_identifier) += [ICU4X](https://github.com/unicode-org/icu4x) ## Dedication @@ -47,16 +49,10 @@ Thanks to my little sister Anny ❤️ the world's best linguist and the most wonderful sister. [bevy]: https://github.com/bevyengine/bevy -[bevy-localisation-plugin-issue]: https://github.com/bevyengine/bevy/issues/461 [fluent]: https://github.com/projectfluent/fluent-rs -[fluent-fallback]: https://github.com/projectfluent/fluent-rs/tree/master/fluent-fallback -[fluent-langneg]: https://github.com/projectfluent/fluent-langneg-rs -[fluent-message-format-2.0]: https://github.com/zbraniecki/message-format-2.0-rs -[fluent-resmgr]: https://github.com/projectfluent/fluent-rs/tree/master/fluent-resmgr -[l10nregistry]: https://github.com/zbraniecki/l10nregistry-rs -[unicode-language-identifier]: http://unicode.org/reports/tr35/#Unicode_language_identifier - -[unic-locale]: https://docs.rs/unic-locale/0.9.0/unic_locale/index.html -[language-tags]: https://www.w3.org/International/questions/qa-choosing-language-tags.ru -[language-subtag-lookup]: https://r12a.github.io/app-subtags + [iana-language-subtag-registry]: http://www.iana.org/assignments/language-subtag-registry +[icu4x message-format 2]: https://github.com/unicode-org/icu4x/pull/2272 +[language-subtag-lookup]: https://r12a.github.io/app-subtags +[language-tags]: https://www.w3.org/International/questions/qa-choosing-language-tags.ru +[unic-locale]: https://docs.rs/unic-locale/0.9.0/unic_locale/index.html \ No newline at end of file diff --git a/doc/en-US.md b/doc/en-US.md index f841948..61532b3 100644 --- a/doc/en-US.md +++ b/doc/en-US.md @@ -7,15 +7,7 @@ Load asset using `AssetServer`: ```rust -let handle = asset_server.load("locales/en-US/main.ftl.ron"); -``` - -Load all assets matching the glob using `AssetServerExt`: - -```rust -use bevy_fluent::exts::bevy::AssetServerExt; - -let handles = asset_server.load_glob("locales/**/main.ftl.ron")?; +let handle = asset_server.load("locales/.ftl.ron#en-US"); ``` Check assets load state: @@ -26,52 +18,32 @@ if let LoadState::Loaded = asset_server.get_load_state(handle) { } ``` -Check assets load state: - -```rust -if let LoadState::Loaded = asset_server.get_group_load_state(handles) { - ... -} -``` - -Create a bundle fallback chain based on the locale fallback chain using -`LocalizationBuilder`: - -```rust -let localization = localization_builder.build(handles); -``` - Request content: ```rust let hello_world = bundle_asset.content("hello-world")?; -let hello_world = localization.content("hello-world")?; ``` ## Definitions +[`BundleAsset`][bundle-asset] - is an abstraction for presentation +[`FluentBundle`][fluent-bundle]. A *bundles* file has the extension `.ftl.ron` +or `.ftl.yml` and proper format. It contains information about all +`FluentBundle`s. -[***Localization***][localization] is a Fluent [***bundle***][fluent-bundle] -fallback chain. - -[***Bundle asset***][bundle-asset] - is an abstraction for presentation Fluent -*bundles*. Each *bundle asset* file has the extension `.ftl.ron`. - -[***Resource asset***][resource-asset] - is an abstraction for presentation -Fluent [***resources***][fluent-resource]. Each *resource asset* file has the -extension `.ftl`. *Resource asset* is the atomic unit of disk storage for -Fluent. +[`ResourceAsset`][resource-asset] - is an abstraction for presentation +[`FluentResource`][fluent-resource]. A *resource* file has the extension `.ftl`. -Each *resource asset* is a set of [***messages***][message]. *Message* is the -basic atomic translation unit for Fluent. +Each `ResourceAsset` is a set of [`Message`][message]s. `Message` is the basic +atomic translation unit for Fluent. -Each *message* has an [***identifier***][identifier]. +Each `Message` has an [`Identifier`][identifier]. -*Messages* (and [***terms***][term], [***variants***][variant], -[***attributes***][attribute]) store their values as [***patterns***][pattern]. +`Message`s (and [`Term`][Term]s, [`Variant`][variant]s, +[`Attribute`][attribute]s) store their values as [`Pattern`][pattern]s. -Formated *pattern* are called [***content***][content]. +Formated `Pattern` are called [`Content`][content]. -[***Request***][request] is a request to receive *content* specified by the +[`Request`][request] is a request to receive `Content` specified by the parameters. [attribute]: https://docs.rs/fluent-syntax/*/fluent_syntax/ast/struct.Attribute.html @@ -80,7 +52,6 @@ parameters. [fluent-bundle]: https://docs.rs/fluent/*/fluent/bundle/struct.FluentBundle.html [fluent-resource]: https://docs.rs/fluent/*/fluent/struct.FluentResource.html [identifier]: https://docs.rs/fluent-syntax/*/fluent_syntax/ast/struct.Identifier.html -[localization]: https://docs.rs/bevy_fluent/*/bevy_fluent/assets/struct.Localization.html [message]: https://docs.rs/fluent-syntax/*/fluent_syntax/ast/struct.Message.html [pattern]: https://docs.rs/fluent-syntax/*/fluent_syntax/ast/struct.Pattern.html [request]: https://docs.rs/bevy_fluent/*/bevy_fluent/exts/bundle/struct.Request.html diff --git a/doc/ru-RU.md b/doc/ru-RU.md index 2feee91..b624331 100644 --- a/doc/ru-RU.md +++ b/doc/ru-RU.md @@ -7,15 +7,7 @@ Загрузить ассет с помощью `AssetServer`: ```rust -let handle = asset_server.load("locales/ru-RU/main.ftl.ron"); -``` - -Загрузить все ассеты, удовлетворяющие шаблону, с помощью `AssetServerExt`: - -```rust -use bevy_fluent::exts::bevy::AssetServerExt; - -let handles = asset_server.load_glob("locales/**/main.ftl.ron")?; +let handle = asset_server.load("locales/.ftl.ron#ru-RU"); ``` Проверить статус загрузки ассета: @@ -26,54 +18,34 @@ if let LoadState::Loaded = asset_server.get_load_state(handle) { } ``` -Проверить статус загрузки нескольких ассетов: - -```rust -if let LoadState::Loaded = asset_server.get_group_load_state(handles) { - ... -} -``` - -Создать резервную цепочку бандлов на основе резервной цепочки локалей с помощью -`LocalizationBuilder`: - -```rust -let localization = localization_builder.build(handles); -``` - Запросить контент: ```rust let hello_world = bundle_asset.content("hello-world")?; -let hello_world = localization.content("hello-world")?; ``` ## Определения -[***Локализация***][localization] представляет собой резервную цепочку -[***бандлов***][fluent-bundle] Fluent. - -[***Ассет бандла***][bundle-asset] - является абстракцией для представления -*бандлов* Fluent. Файл *ассета бандла* имеет расширение `.ftl.ron`. +[`BundleAsset`][bundle-asset] - является абстракцией для представления +[`FluentBundle`][fluent-bundle]. Файл *бандлов* имеет расширение `.ftl.ron` или +`.ftl.yml` и соответствующий формат. Он содержит информацию обо всех +`FluentBundle`. -[***Ассет ресурса***][resource-asset] - является абстракцией для представления -[***ресурсов***][fluent-resource] Fluent. Файл *ассета ресурсов* имеет -расширение `.ftl`. *Ассет ресурса* является атомарной единицей хранения -информации на диске для Fluent. +[`ResourceAsset`][resource-asset] - является абстракцией для представления +[`FluentResource`][fluent-resource]. Файл *ресурса* имеет расширение `.ftl`. -Каждый *ассет ресурса* представляет собой набор [***сообщений***][message]. -*Cообщение* является атомарной единицей перевода во Fluent. +Каждый `ResourceAsset` представляет собой набор из [`Message`][message]. +`Message` является атомарной единицей перевода во Fluent. -Каждое *сообщение* имеет [***идентификатор***][identifier]. +Каждое `Message` имеет [`Identifier`][identifier]. -*Сообщения* (как и [***термы***][term], [***варианты***][variant], -[***аттрибуты***][attribute]) хранят свои значения в виде -[***паттернов***][pattern]. +`Message` (как и [`Term`][term], [`Variant`][variant], [`Attribute`][attribute]) +хранят свои значения в виде [`Pattern`][pattern]. -Форматированный *паттерн* называется [***контентом***][content]. +Форматированный `Pattern` называется [`Content`][content]. -[***Запрос***][request] представляет собой запрос на получение соответствующего -заданным параметрам *контента*. +[`Request`][request] представляет собой запрос на получение `Content`, +соответствующего заданным параметрам. [attribute]: https://docs.rs/fluent-syntax/*/fluent_syntax/ast/struct.Attribute.html [bundle-asset]: https://docs.rs/bevy_fluent/*/bevy_fluent/assets/struct.BundleAsset.html @@ -81,7 +53,6 @@ let hello_world = localization.content("hello-world")?; [fluent-bundle]: https://docs.rs/fluent/*/fluent/bundle/struct.FluentBundle.html [fluent-resource]: https://docs.rs/fluent/*/fluent/struct.FluentResource.html [identifier]: https://docs.rs/fluent-syntax/*/fluent_syntax/ast/struct.Identifier.html -[localization]: https://docs.rs/bevy_fluent/*/bevy_fluent/assets/struct.Localization.html [message]: https://docs.rs/fluent-syntax/*/fluent_syntax/ast/struct.Message.html [pattern]: https://docs.rs/fluent-syntax/*/fluent_syntax/ast/struct.Pattern.html [request]: https://docs.rs/bevy_fluent/*/bevy_fluent/exts/bundle/struct.Request.html diff --git a/examples/fallback_chain/assets/locales/.ftl.ron b/examples/fallback_chain/assets/locales/.ftl.ron new file mode 100644 index 0000000..7d820cc --- /dev/null +++ b/examples/fallback_chain/assets/locales/.ftl.ron @@ -0,0 +1,5 @@ +{ + "en-US": ["en-US/main.ftl"], + "ru-BY": ["ru/ru-BY/main.ftl"], + "ru-RU": ["ru/ru-RU/main.ftl"], +} diff --git a/examples/fallback_chain/assets/locales/en-US/hello_world.ftl b/examples/fallback_chain/assets/locales/en-US/main.ftl similarity index 51% rename from examples/fallback_chain/assets/locales/en-US/hello_world.ftl rename to examples/fallback_chain/assets/locales/en-US/main.ftl index 34ec3ea..47185c4 100644 --- a/examples/fallback_chain/assets/locales/en-US/hello_world.ftl +++ b/examples/fallback_chain/assets/locales/en-US/main.ftl @@ -1,3 +1,3 @@ hello = hello world = world -hello-world = hello world +bevy = bevy diff --git a/examples/fallback_chain/assets/locales/en-US/main.ftl.ron b/examples/fallback_chain/assets/locales/en-US/main.ftl.ron deleted file mode 100644 index f583b0f..0000000 --- a/examples/fallback_chain/assets/locales/en-US/main.ftl.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - locale: "en-US", - resources: [ - "hello_world.ftl", - ] -) diff --git a/examples/fallback_chain/assets/locales/ru/ru-BY/hello_world.ftl b/examples/fallback_chain/assets/locales/ru/ru-BY/main.ftl similarity index 100% rename from examples/fallback_chain/assets/locales/ru/ru-BY/hello_world.ftl rename to examples/fallback_chain/assets/locales/ru/ru-BY/main.ftl diff --git a/examples/fallback_chain/assets/locales/ru/ru-BY/main.ftl.ron b/examples/fallback_chain/assets/locales/ru/ru-BY/main.ftl.ron deleted file mode 100644 index 264c650..0000000 --- a/examples/fallback_chain/assets/locales/ru/ru-BY/main.ftl.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - locale: "ru-BY", - resources: [ - "hello_world.ftl", - ] -) diff --git a/examples/fallback_chain/assets/locales/ru/ru-RU/hello_world.ftl b/examples/fallback_chain/assets/locales/ru/ru-RU/main.ftl similarity index 100% rename from examples/fallback_chain/assets/locales/ru/ru-RU/hello_world.ftl rename to examples/fallback_chain/assets/locales/ru/ru-RU/main.ftl diff --git a/examples/fallback_chain/assets/locales/ru/ru-RU/main.ftl.ron b/examples/fallback_chain/assets/locales/ru/ru-RU/main.ftl.ron deleted file mode 100644 index 44daf26..0000000 --- a/examples/fallback_chain/assets/locales/ru/ru-RU/main.ftl.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - locale: "ru-RU", - resources: [ - "hello_world.ftl", - ] -) diff --git a/examples/fallback_chain/main.rs b/examples/fallback_chain/main.rs index 5b6cb28..902f3a3 100644 --- a/examples/fallback_chain/main.rs +++ b/examples/fallback_chain/main.rs @@ -1,14 +1,14 @@ -use bevy::{ - asset::{LoadState, LoadedFolder}, - prelude::*, -}; -use bevy_fluent::prelude::*; +use bevy::{asset::LoadState, prelude::*}; +use bevy_fluent::{prelude::*, resources::Locales}; use fluent_content::Content; use unic_langid::langid; pub fn main() { App::new() - .insert_resource(Locale::new(langid!("ru-RU")).with_default(langid!("en-US"))) + .insert_resource( + Locales::new([langid!("en-US"), langid!("ru-RU"), langid!("ru-BY")]) + .with_default(langid!("en-US")), + ) .add_plugins(( DefaultPlugins.set(AssetPlugin { file_path: "examples/fallback_chain/assets".to_string(), @@ -16,26 +16,37 @@ pub fn main() { }), FluentPlugin, )) - .add_systems(Update, localized_hello_world) + .add_systems(Update, localize) .run(); } -fn localized_hello_world( - localization_builder: LocalizationBuilder, +fn localize( + locales: Res, asset_server: Res, - mut handle: Local>>, - mut localization: Local>, + assets: Res>, + mut handles: Local>>>, + mut bundles: Local, ) { - let handle = &*handle.get_or_insert_with(|| asset_server.load_folder("locales")); - if let Some(LoadState::Loaded) = asset_server.get_load_state(handle) { - let localization = localization.get_or_insert_with(|| localization_builder.build(handle)); + let handles = handles.get_or_insert_with(|| { + locales + .request(Some(langid!("ru-RU"))) + .iter() + .map(|locale| asset_server.load(format!("locales/.ftl.ron#{locale}"))) + .collect() + }); + if handles + .iter() + .all(|handle| asset_server.get_load_state(handle) == Some(LoadState::Loaded)) + { + *bundles = handles + .iter() + .map(|handle| (handle.clone(), assets.get(handle).unwrap().clone())) + .collect(); // From ru-RU bundle, the first in fallback chain. - assert!(matches!(localization.content("hello"), Some(content) if content == "привет")); + assert!(matches!(bundles.content("hello"), Some(content) if content == "привет")); // From ru-BY bundle, the second in fallback chain. - assert!(matches!(localization.content("world"), Some(content) if content == "свету")); + assert!(matches!(bundles.content("world"), Some(content) if content == "свету")); // From en-US bundle, the last in fallback chain, default locale. - assert!( - matches!(localization.content("hello-world"), Some(content) if content == "hello world") - ); + assert!(matches!(bundles.content("bevy"), Some(content) if content == "bevy")); } } diff --git a/examples/minimal/assets/locales/en-US/main.ftl.yml b/examples/minimal/assets/locales/.ftl.yml similarity index 57% rename from examples/minimal/assets/locales/en-US/main.ftl.yml rename to examples/minimal/assets/locales/.ftl.yml index 2ee8ca5..9057261 100644 --- a/examples/minimal/assets/locales/en-US/main.ftl.yml +++ b/examples/minimal/assets/locales/.ftl.yml @@ -1,4 +1,2 @@ # Locale files may be in YAML as in this example, or in RON -locale: en-US -resources: - - hello_world.ftl +en-US: [en-US/main.ftl] diff --git a/examples/minimal/assets/locales/en-US/hello_world.ftl b/examples/minimal/assets/locales/en-US/main.ftl similarity index 100% rename from examples/minimal/assets/locales/en-US/hello_world.ftl rename to examples/minimal/assets/locales/en-US/main.ftl diff --git a/examples/minimal/main.rs b/examples/minimal/main.rs index e04f61a..11a8073 100644 --- a/examples/minimal/main.rs +++ b/examples/minimal/main.rs @@ -1,11 +1,9 @@ use bevy::{asset::LoadState, prelude::*}; use bevy_fluent::prelude::*; use fluent_content::Content; -use unic_langid::langid; pub fn main() { App::new() - .insert_resource(Locale::new(langid!("en-US"))) .add_plugins(( DefaultPlugins.set(AssetPlugin { file_path: "examples/minimal/assets".to_string(), @@ -22,7 +20,7 @@ fn localized_hello_world( assets: Res>, mut handle: Local>>, ) { - let handle = &*handle.get_or_insert_with(|| asset_server.load("locales/en-US/main.ftl.yml")); + let handle = &*handle.get_or_insert_with(|| asset_server.load("locales/.ftl.yml#en-US")); if let Some(LoadState::Loaded) = asset_server.get_load_state(handle) { let bundle = assets.get(handle).unwrap(); assert!(matches!(bundle.content("hello-world"), Some(content) if content == "hello world")); diff --git a/examples/multilingual/assets/locales/.ftl.ron b/examples/multilingual/assets/locales/.ftl.ron new file mode 100644 index 0000000..a1a9197 --- /dev/null +++ b/examples/multilingual/assets/locales/.ftl.ron @@ -0,0 +1,5 @@ +{ + "de-DE": ["de-DE.ftl"], + "en-US": ["en-US.ftl"], + "ru-RU": ["ru-RU.ftl"], +} \ No newline at end of file diff --git a/examples/multilingual/assets/locales/de-DE.ftl b/examples/multilingual/assets/locales/de-DE.ftl new file mode 100644 index 0000000..227c14f --- /dev/null +++ b/examples/multilingual/assets/locales/de-DE.ftl @@ -0,0 +1,3 @@ +hello = hallo +world = welt +hello-world = { hello } { world } diff --git a/examples/multilingual/assets/locales/en-US.ftl b/examples/multilingual/assets/locales/en-US.ftl new file mode 100644 index 0000000..8d29361 --- /dev/null +++ b/examples/multilingual/assets/locales/en-US.ftl @@ -0,0 +1,3 @@ +hello = hello +world = world +hello-world = { hello } { world } diff --git a/examples/multilingual/assets/locales/ru-RU.ftl b/examples/multilingual/assets/locales/ru-RU.ftl new file mode 100644 index 0000000..9ace33a --- /dev/null +++ b/examples/multilingual/assets/locales/ru-RU.ftl @@ -0,0 +1,3 @@ +hello = привет +world = мир +hello-world = { hello } { world } \ No newline at end of file diff --git a/examples/multilingual/main.rs b/examples/multilingual/main.rs new file mode 100644 index 0000000..186066d --- /dev/null +++ b/examples/multilingual/main.rs @@ -0,0 +1,43 @@ +use bevy::{asset::LoadState, prelude::*}; +use bevy_fluent::prelude::*; +use fluent_content::Content; + +pub fn main() { + App::new() + .add_plugins(( + DefaultPlugins.set(AssetPlugin { + file_path: "examples/multilingual/assets".to_string(), + ..default() + }), + FluentPlugin, + )) + .add_systems(Update, localized_hello_world) + .run(); +} + +fn localized_hello_world( + asset_server: Res, + assets: Res>, + mut de: Local>>, + mut en: Local>>, + mut ru: Local>>, +) { + let de = &*de.get_or_insert_with(|| asset_server.load("locales/.ftl.ron#de-DE")); + if asset_server.get_load_state(de) == Some(LoadState::Loaded) { + let de = assets.get(de).unwrap(); + // From de-DE bundle. + assert!(matches!(de.content("hello-world"), Some(content) if content == "hallo welt")); + } + let en = &*en.get_or_insert_with(|| asset_server.load("locales/.ftl.ron#en-US")); + if asset_server.get_load_state(en) == Some(LoadState::Loaded) { + let en = assets.get(en).unwrap(); + // From en-US bundle. + assert!(matches!(en.content("hello-world"), Some(content) if content == "hello world")); + } + let ru = &*ru.get_or_insert_with(|| asset_server.load("locales/.ftl.ron#ru-RU")); + if asset_server.get_load_state(ru) == Some(LoadState::Loaded) { + let ru = assets.get(ru).unwrap(); + // From ru-RU bundle. + assert!(matches!(ru.content("hello-world"), Some(content) if content == "привет мир")); + } +} diff --git a/examples/ui/assets/locales/.ftl.ron b/examples/ui/assets/locales/.ftl.ron new file mode 100644 index 0000000..5eadd58 --- /dev/null +++ b/examples/ui/assets/locales/.ftl.ron @@ -0,0 +1,7 @@ +{ + "de-DE": ["de-DE/menu/ui.ftl"], + "en-US": ["en-US/menu/ui.ftl"], + "ru-BY": ["ru/ru-BY/menu/ui.ftl"], + "ru-RU": ["ru/ru-RU/menu/ui.ftl"], + "und": ["und/menu/locales.ftl"], +} \ No newline at end of file diff --git a/examples/ui/assets/locales/de-DE/menu.ftl.ron b/examples/ui/assets/locales/de-DE/menu.ftl.ron deleted file mode 100644 index f1ac807..0000000 --- a/examples/ui/assets/locales/de-DE/menu.ftl.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - locale: "de-DE", - resources: [ - "menu/ui.ftl", - ] -) diff --git a/examples/ui/assets/locales/en-US/menu.ftl.ron b/examples/ui/assets/locales/en-US/menu.ftl.ron deleted file mode 100644 index e17707c..0000000 --- a/examples/ui/assets/locales/en-US/menu.ftl.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - locale: "en-US", - resources: [ - "menu/ui.ftl", - ] -) diff --git a/examples/ui/assets/locales/ru/ru-BY/menu.ftl.ron b/examples/ui/assets/locales/ru/ru-BY/menu.ftl.ron deleted file mode 100644 index 492a627..0000000 --- a/examples/ui/assets/locales/ru/ru-BY/menu.ftl.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - locale: "ru-BY", - resources: [ - "menu/ui.ftl", - ] -) diff --git a/examples/ui/assets/locales/ru/ru-RU/menu.ftl.ron b/examples/ui/assets/locales/ru/ru-RU/menu.ftl.ron deleted file mode 100644 index 5d53e96..0000000 --- a/examples/ui/assets/locales/ru/ru-RU/menu.ftl.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - locale: "ru-RU", - resources: [ - "menu/ui.ftl", - ] -) diff --git a/examples/ui/assets/locales/ru/ru-RU/play/ui.ftl b/examples/ui/assets/locales/ru/ru-RU/play/ui.ftl deleted file mode 100644 index 4abce2b..0000000 --- a/examples/ui/assets/locales/ru/ru-RU/play/ui.ftl +++ /dev/null @@ -1 +0,0 @@ -play = играть \ No newline at end of file diff --git a/examples/ui/assets/locales/und/menu.ftl.ron b/examples/ui/assets/locales/und/menu.ftl.ron deleted file mode 100644 index 78b9d83..0000000 --- a/examples/ui/assets/locales/und/menu.ftl.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - locale: "und", - resources: [ - "menu/locales.ftl", - ] -) diff --git a/examples/ui/locales.rs b/examples/ui/locales.rs deleted file mode 100644 index c239930..0000000 --- a/examples/ui/locales.rs +++ /dev/null @@ -1,22 +0,0 @@ -use unic_langid::{langid, LanguageIdentifier}; - -// pub const UND: LanguageIdentifier = langid!("und"); - -pub mod de { - use super::*; - - pub const DE: LanguageIdentifier = langid!("de-DE"); -} - -pub mod en { - use super::*; - - pub const US: LanguageIdentifier = langid!("en-US"); -} - -pub mod ru { - use super::*; - - pub const BY: LanguageIdentifier = langid!("ru-BY"); - pub const RU: LanguageIdentifier = langid!("ru-RU"); -} diff --git a/examples/ui/main.rs b/examples/ui/main.rs index 0077ed4..9802a90 100644 --- a/examples/ui/main.rs +++ b/examples/ui/main.rs @@ -1,12 +1,12 @@ #![allow(clippy::type_complexity)] use crate::{ - locales::{de, en, ru}, - resources::{Font, Locales}, + resources::Font, systems::{load, menu}, }; use bevy::prelude::*; use bevy_fluent::prelude::*; +use unic_langid::langid; fn main() { App::new() @@ -17,18 +17,25 @@ fn main() { }), FluentPlugin, )) - .insert_resource(Locale::new(ru::RU).with_default(en::US)) - .insert_resource(Locales(vec![de::DE, en::US, ru::BY, ru::RU])) + .insert_resource( + Locales::new([ + langid!("de-DE"), + langid!("en-US"), + langid!("ru-BY"), + langid!("ru-RU"), + ]) + .with_default(langid!("und")), + ) .init_resource::() .add_state::() .add_systems(OnEnter(GameState::Load), load::setup) .add_systems(Update, load::update.run_if(in_state(GameState::Load))) .add_systems(OnEnter(GameState::Menu), menu::setup) - .add_systems(OnExit(GameState::Menu), menu::cleanup) .add_systems( Update, (menu::interaction, menu::next, menu::previous).run_if(in_state(GameState::Menu)), ) + .add_systems(OnExit(GameState::Menu), menu::cleanup) .run(); } @@ -40,7 +47,6 @@ pub enum GameState { } mod components; -mod locales; mod resources; mod systems; mod to_sentence_case; diff --git a/examples/ui/resources/mod.rs b/examples/ui/resources/mod.rs index a0ffd57..eac3960 100644 --- a/examples/ui/resources/mod.rs +++ b/examples/ui/resources/mod.rs @@ -1,6 +1,5 @@ use bevy::prelude::{Font as BevyFont, *}; -use std::ops::Deref; -use unic_langid::LanguageIdentifier; +use bevy_fluent::BundleAsset; /// Font #[derive(Resource)] @@ -14,22 +13,6 @@ impl FromWorld for Font { } } -/// Locales -#[derive(Resource)] -pub struct Locales(pub Vec); - -impl Locales { - pub fn index(&self, locale: &LanguageIdentifier) -> usize { - self.iter() - .position(|item| item == locale) - .expect("index not found") - } -} - -impl Deref for Locales { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} +/// Handles +#[derive(Clone, Default, Deref, Resource)] +pub struct Handles(pub Vec>); diff --git a/examples/ui/systems/load.rs b/examples/ui/systems/load.rs index 795ad1f..926a313 100644 --- a/examples/ui/systems/load.rs +++ b/examples/ui/systems/load.rs @@ -1,29 +1,37 @@ -use crate::GameState; -use bevy::{ - asset::{LoadState, LoadedFolder}, - prelude::*, -}; +use crate::{resources::Handles, GameState}; +use bevy::{asset::LoadState, prelude::*}; use bevy_fluent::prelude::*; -pub fn setup(mut commands: Commands, asset_server: Res) { - let handle = asset_server.load_folder("locales"); - commands.insert_resource(LocaleFolder(handle)); +pub fn setup(mut commands: Commands, asset_server: Res, locales: Res) { + let handles = Handles( + locales + .request(Some(&locales.available[0])) + .iter() + .map(|locale| asset_server.load(format!("locales/.ftl.ron#{locale}"))) + .collect(), + ); + commands.insert_resource(handles); } pub fn update( mut commands: Commands, - localization_builder: LocalizationBuilder, asset_server: Res, + assets: Res>, + handles: Res, mut next_state: ResMut>, - locale_folder: Res, ) { - if let Some(LoadState::Loaded) = asset_server.get_load_state(&locale_folder.0) { - let localization = localization_builder.build(&locale_folder.0); - commands.remove_resource::(); - commands.insert_resource(localization); + if handles + .iter() + .all(|handle| asset_server.get_load_state(handle) == Some(LoadState::Loaded)) + { + let bundles = Bundles( + handles + .iter() + .map(|handle| (handle.clone(), assets.get(handle).unwrap().clone())) + .collect(), + ); + commands.remove_resource::(); + commands.insert_resource(bundles); next_state.set(GameState::Menu); } } - -#[derive(Resource)] -pub struct LocaleFolder(Handle); diff --git a/examples/ui/systems/menu.rs b/examples/ui/systems/menu.rs index 5d3c9ba..0e0d060 100644 --- a/examples/ui/systems/menu.rs +++ b/examples/ui/systems/menu.rs @@ -1,7 +1,6 @@ use crate::{ components::{Menu, NextButton, PreviousButton}, resources::Font, - systems::parameters::Swiper, to_sentence_case::ToSentenceCase, GameState, }; @@ -12,12 +11,12 @@ use fluent_content::Content; pub fn setup( mut commands: Commands, font: Res, - locale: Res, - localization: Res, + locales: Res, + bundles: Res, ) { - let request = locale.requested.to_string().to_lowercase(); - let locale = localization.content(&request).unwrap().to_sentence_case(); - let choose_language = localization + let request = locales.available[0].to_string().to_lowercase(); + let locale = bundles.content(&request).unwrap().to_sentence_case(); + let choose_language = bundles .content("choose-language") .unwrap() .to_sentence_case(); @@ -190,49 +189,40 @@ pub fn interaction( } pub fn next( - mut swiper: Swiper, + mut locales: ResMut, mut next_state: ResMut>, query: Query<&Interaction, (Changed, With)>, ) { if let Ok(Interaction::Pressed) = query.get_single() { - swiper.next(); + locales.next(); next_state.set(GameState::Load); } } pub fn previous( - mut swiper: Swiper, + mut locales: ResMut, mut next_state: ResMut>, query: Query<&Interaction, (Changed, With)>, ) { if let Ok(Interaction::Pressed) = query.get_single() { - swiper.previous(); + locales.previous(); next_state.set(GameState::Load); } } -// const LOCALES: &[LanguageIdentifier] = &[de::DE, en::US, ru::BY, ru::RU]; +/// Rotate locales +trait Rotate { + fn next(&mut self); -// /// Shift to one of the next or previous locale -// trait Shift { -// fn shift(&mut self, count: isize); -// } + fn previous(&mut self); +} + +impl Rotate for Locales { + fn next(&mut self) { + self.available.rotate_right(1); + } -// impl Shift for Locale { -// fn shift(&mut self, count: isize) { -// error!(%count); -// if let Some(mut position) = LOCALES.iter().position(|locale| locale == self.requested()) { -// error!(%position); -// if count.is_positive() { -// let count = count as _; -// position = position.saturating_add(count).min(LOCALES.len() - 1); -// } else if count.is_negative() { -// let count = count.abs() as _; -// position = position.saturating_sub(count); -// } -// error!(%position); -// *self = -// Self::new(LOCALES[position].clone()).with_default(self.default().unwrap().clone()); -// } -// } -// } + fn previous(&mut self) { + self.available.rotate_left(1); + } +} diff --git a/examples/ui/systems/mod.rs b/examples/ui/systems/mod.rs index cc0091a..93fcabc 100644 --- a/examples/ui/systems/mod.rs +++ b/examples/ui/systems/mod.rs @@ -1,3 +1,2 @@ pub mod load; pub mod menu; -pub mod parameters; diff --git a/examples/ui/systems/parameters/mod.rs b/examples/ui/systems/parameters/mod.rs deleted file mode 100644 index a23610d..0000000 --- a/examples/ui/systems/parameters/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::resources::Locales; -use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_fluent::*; - -/// Swipe to one of the next or previous locale -#[derive(SystemParam)] -pub struct Swiper<'w> { - locale: ResMut<'w, Locale>, - locales: Res<'w, Locales>, -} - -impl Swiper<'_> { - pub fn next(&mut self) { - let mut index = self.locales.index(&self.locale.requested); - index = index.saturating_add(1).min(self.locales.len() - 1); - self.locale.requested = self.locales[index].clone(); - } - - pub fn previous(&mut self) { - let mut index = self.locales.index(&self.locale.requested); - index = index.saturating_sub(1); - self.locale.requested = self.locales[index].clone(); - } -} diff --git a/src/assets/bundle.rs b/src/assets/bundle.rs index 67d8df7..d147052 100644 --- a/src/assets/bundle.rs +++ b/src/assets/bundle.rs @@ -6,29 +6,23 @@ use bevy::{ asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, prelude::*, reflect::{TypePath, TypeUuid}, - utils::{ - tracing::{self, instrument}, - BoxedFuture, - }, + utils::BoxedFuture, }; use fluent::{bundle::FluentBundle, FluentResource}; use intl_memoizer::concurrent::IntlLangMemoizer; -use serde::{Deserialize, Serialize}; -use std::{ops::Deref, path::PathBuf, str, sync::Arc}; +use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::Arc}; use unic_langid::LanguageIdentifier; /// [`FluentBundle`](fluent::bundle::FluentBundle) wrapper /// /// Collection of [`FluentResource`]s for a single locale -#[derive(Asset, Clone, TypePath, TypeUuid)] +#[derive(Asset, Clone, Deref, TypePath, TypeUuid)] #[uuid = "929113bb-9187-44c3-87be-6027fc3b7ac5"] pub struct BundleAsset(pub(crate) Arc, IntlLangMemoizer>>); -impl Deref for BundleAsset { - type Target = FluentBundle, IntlLangMemoizer>; - - fn deref(&self) -> &Self::Target { - &self.0 +impl BundleAsset { + pub fn new(bundle: Arc, IntlLangMemoizer>>) -> Self { + Self(bundle) } } @@ -48,18 +42,38 @@ impl AssetLoader for BundleAssetLoader { load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result> { Box::pin(async move { - let path = load_context.path(); let mut content = String::new(); reader.read_to_string(&mut content).await?; - match path.extension() { - Some(extension) if extension == "ron" => { - load(ron::de::from_str(&content)?, load_context).await - } - Some(extension) if extension == "yaml" || extension == "yml" => { - load(serde_yaml::from_str(&content)?, load_context).await - } + let resources: Resources = match load_context.path().extension().and_then(OsStr::to_str) + { + Some("ron") => ron::de::from_str(&content)?, + Some("yml" | "yaml") => serde_yaml::from_str(&content)?, _ => unreachable!("We already check all the supported extensions."), + }; + for (locale, paths) in resources { + load_context.add_loaded_labeled_asset(locale.to_string(), { + let mut load_context = load_context.begin_labeled_asset(); + let mut bundle = FluentBundle::new_concurrent(vec![locale.clone()]); + for mut path in paths { + if path.is_relative() { + if let Some(parent) = load_context.path().parent() { + path = parent.join(path); + } + } + let loaded = load_context.load_direct(path).await?; + let resource = loaded.get::().unwrap(); + if let Err(errors) = bundle.add_resource(resource.0.clone()) { + warn_span!("add_resource").in_scope(|| { + for error in errors { + warn!(%locale, %error); + } + }); + } + } + load_context.finish(BundleAsset(Arc::new(bundle)), None) + }); } + Ok(BundleAsset(Arc::new(FluentBundle::new_concurrent(vec![])))) }) } @@ -68,32 +82,5 @@ impl AssetLoader for BundleAssetLoader { } } -/// Data -#[derive(Debug, Deserialize, Serialize)] -#[serde(deny_unknown_fields)] -struct Data { - locale: LanguageIdentifier, - resources: Vec, -} - -#[instrument(fields(path = %load_context.path().display()), skip_all)] -async fn load(data: Data, load_context: &mut LoadContext<'_>) -> Result { - let mut bundle = FluentBundle::new_concurrent(vec![data.locale.clone()]); - for mut path in data.resources { - if path.is_relative() { - if let Some(parent) = load_context.path().parent() { - path = parent.join(path); - } - } - let loaded = load_context.load_direct(path).await?; - let resource = loaded.get::().unwrap(); - if let Err(errors) = bundle.add_resource(resource.0.clone()) { - warn_span!("add_resource").in_scope(|| { - for error in errors { - warn!(%error); - } - }); - } - } - Ok(BundleAsset(Arc::new(bundle))) -} +/// Resources +type Resources = HashMap>; diff --git a/src/assets/resource.rs b/src/assets/resource.rs index c1f71ed..da5109b 100644 --- a/src/assets/resource.rs +++ b/src/assets/resource.rs @@ -5,24 +5,19 @@ use bevy::{ asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, prelude::*, reflect::{TypePath, TypeUuid}, - utils::{ - tracing::{self, instrument}, - BoxedFuture, - }, + utils::{tracing::instrument, BoxedFuture}, }; use fluent::FluentResource; -use std::{ops::Deref, str, sync::Arc}; +use std::sync::Arc; /// [`FluentResource`](fluent::FluentResource) wrapper -#[derive(Asset, Clone, Debug, TypePath, TypeUuid)] +#[derive(Asset, Clone, Debug, Deref, TypePath, TypeUuid)] #[uuid = "0b2367cb-fb4a-4746-a305-df98b26dddf6"] pub struct ResourceAsset(pub(crate) Arc); -impl Deref for ResourceAsset { - type Target = FluentResource; - - fn deref(&self) -> &Self::Target { - &self.0 +impl ResourceAsset { + pub fn new(resource: Arc) -> Self { + Self(resource) } } @@ -45,7 +40,7 @@ impl AssetLoader for ResourceAssetLoader { Box::pin(async move { let mut content = String::new(); reader.read_to_string(&mut content).await?; - Ok(ResourceAsset(deserialize(content))) + Ok(ResourceAsset(Arc::new(deserialize(content)))) }) } @@ -55,8 +50,8 @@ impl AssetLoader for ResourceAssetLoader { } #[instrument(skip_all)] -fn deserialize(content: String) -> Arc { - let fluent_resource = match FluentResource::try_new(content) { +fn deserialize(content: String) -> FluentResource { + match FluentResource::try_new(content) { Ok(fluent_resource) => fluent_resource, Err((fluent_resource, errors)) => { error_span!("try_new").in_scope(|| { @@ -66,6 +61,5 @@ fn deserialize(content: String) -> Arc { }); fluent_resource } - }; - Arc::new(fluent_resource) + } } diff --git a/src/exts/fluent/bundle.rs b/src/exts/fluent/bundle.rs deleted file mode 100644 index d5d30dd..0000000 --- a/src/exts/fluent/bundle.rs +++ /dev/null @@ -1,14 +0,0 @@ -use fluent::bundle::FluentBundle; -use unic_langid::LanguageIdentifier; - -/// Extension methods for [`FluentBundle`](fluent::bundle::FluentBundle) -pub trait BundleExt { - /// Bundle locale - fn locale(&self) -> &LanguageIdentifier; -} - -impl BundleExt for FluentBundle { - fn locale(&self) -> &LanguageIdentifier { - &self.locales[0] - } -} diff --git a/src/exts/fluent/mod.rs b/src/exts/fluent/mod.rs deleted file mode 100644 index 53d3678..0000000 --- a/src/exts/fluent/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use self::bundle::BundleExt; - -mod bundle; diff --git a/src/exts/mod.rs b/src/exts/mod.rs deleted file mode 100644 index ccad87c..0000000 --- a/src/exts/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod fluent; diff --git a/src/lib.rs b/src/lib.rs index 001029c..45c1174 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,18 +6,15 @@ pub use self::{ assets::{BundleAsset, ResourceAsset}, plugins::FluentPlugin, - resources::{Locale, Localization}, - systems::parameters::LocalizationBuilder, + resources::{Bundles, Locales}, }; /// `use bevy_fluent::prelude::*;` to import common assets, components and plugins pub mod prelude { #[doc(inline)] - pub use super::{BundleAsset, FluentPlugin, Locale, Localization, LocalizationBuilder}; + pub use super::{BundleAsset, Bundles, FluentPlugin, Locales}; } pub mod assets; -pub mod exts; pub mod plugins; pub mod resources; -pub mod systems; diff --git a/src/resources/bundles.rs b/src/resources/bundles.rs new file mode 100644 index 0000000..150dda7 --- /dev/null +++ b/src/resources/bundles.rs @@ -0,0 +1,60 @@ +//! Bundles resource + +use crate::BundleAsset; +use bevy::{ + prelude::*, + utils::tracing::{self, instrument}, +}; +use fluent::FluentArgs; +use fluent_content::{Content, Request}; +use indexmap::IndexMap; +use std::{ + borrow::Borrow, + fmt::{self, Debug, Formatter}, +}; +use unic_langid::LanguageIdentifier; + +/// Bundles resource +/// +/// Collection of [`BundleAsset`]s. +#[derive(Clone, Default, Resource)] +pub struct Bundles(pub IndexMap, BundleAsset>); + +impl Bundles { + pub fn handles(&self) -> impl Iterator> { + self.0.keys() + } + + fn locales(&self) -> impl Iterator { + self.0.values().map(|bundle| &bundle.locales[0]) + } +} + +impl<'a, T, U> Content<'a, T, U> for Bundles +where + T: Copy + Into>, + U: Borrow>, +{ + #[instrument(fields(request = %request.into()), skip_all)] + fn content(&self, request: T) -> Option { + self.0.values().find_map(|bundle| { + let content = bundle.content(request); + trace!(locale = %bundle.locales[0], ?content); + content + }) + } +} + +impl Debug for Bundles { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_tuple("Bundles") + .field(&self.locales().collect::>()) + .finish() + } +} + +impl FromIterator<(Handle, BundleAsset)> for Bundles { + fn from_iter, BundleAsset)>>(iter: T) -> Self { + Self(FromIterator::from_iter(iter)) + } +} diff --git a/src/resources/locale.rs b/src/resources/locale.rs deleted file mode 100644 index d4829f2..0000000 --- a/src/resources/locale.rs +++ /dev/null @@ -1,47 +0,0 @@ -use bevy::prelude::*; -use fluent_langneg::{negotiate_languages, NegotiationStrategy}; -use std::slice::from_ref; -use unic_langid::LanguageIdentifier; - -/// Locale -#[derive(Clone, Debug, Default, Resource)] -pub struct Locale { - pub requested: LanguageIdentifier, - pub default: Option, -} - -impl Locale { - pub fn new(locale: LanguageIdentifier) -> Self { - Self { - requested: locale, - default: None, - } - } - - pub fn with_default(mut self, locale: LanguageIdentifier) -> Self { - self.default = Some(locale); - self - } - - pub fn fallback_chain<'a, I>(&'a self, locales: I) -> Vec<&'a LanguageIdentifier> - where - I: Iterator, - { - let available = &locales.collect::>(); - let default = self.default.as_ref(); - let requested = from_ref(&self.requested); - let supported = negotiate_languages( - requested, - available, - default.as_ref(), - NegotiationStrategy::Filtering, - ); - debug!( - requested = ?requested.iter().map(|locale| format!("{locale}")).collect::>(), - available = ?available.iter().map(|locale| format!("{locale}")).collect::>(), - default = ?default.map(|locale| format!("{locale}")), - supported = ?supported.iter().map(|locale| format!("{locale}")).collect::>(), - ); - supported.into_iter().copied().collect() - } -} diff --git a/src/resources/locales.rs b/src/resources/locales.rs new file mode 100644 index 0000000..081cd23 --- /dev/null +++ b/src/resources/locales.rs @@ -0,0 +1,46 @@ +use bevy::prelude::*; +use fluent_langneg::{negotiate_languages, NegotiationStrategy}; +use serde::{Deserialize, Serialize}; +use unic_langid::LanguageIdentifier; + +/// Locales +#[derive(Clone, Debug, Default, Deserialize, Resource, Serialize)] +pub struct Locales { + pub available: Vec, + pub default: Option, +} + +impl Locales { + /// Receives available locales. + pub fn new(available: impl IntoIterator) -> Self { + Self { + available: Vec::from_iter(available), + default: None, + } + } + + /// Receives default locale. + pub fn with_default(mut self, default: A) -> Self { + self.default = Some(default); + self + } +} + +impl + PartialEq> Locales { + /// Receives requested locales. Returns supported locales fallback chain. + pub fn request<'a, R: 'a + AsRef>( + &'a self, + requested: impl IntoIterator, + ) -> Vec<&'a A> { + let requested = &Vec::from_iter(requested); + let available = &self.available; + let default = self.default.as_ref(); + let supported = negotiate_languages( + requested, + available, + default, + NegotiationStrategy::Filtering, + ); + supported + } +} diff --git a/src/resources/localization.rs b/src/resources/localization.rs deleted file mode 100644 index 544f4b3..0000000 --- a/src/resources/localization.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Localization asset - -use crate::{exts::fluent::BundleExt, BundleAsset}; -use bevy::{ - prelude::*, - utils::tracing::{self, instrument}, -}; -use fluent::FluentArgs; -use fluent_content::{Content, Request}; -use indexmap::IndexMap; -use std::{ - borrow::Borrow, - fmt::{self, Debug, Formatter}, - ops::{Deref, DerefMut}, -}; -use unic_langid::LanguageIdentifier; - -/// Localization -/// -/// Collection of [`BundleAsset`]s. -#[derive(Default, Resource)] -pub struct Localization(IndexMap, BundleAsset>); - -impl Localization { - pub fn new() -> Self { - Self::default() - } - - pub fn handles(&self) -> impl Iterator> { - self.0.keys() - } - - pub fn insert(&mut self, handle: &Handle, asset: &BundleAsset) { - self.0.insert(handle.clone(), asset.clone()); - } - - pub fn locales(&self) -> impl Iterator { - self.0.values().map(|bundle| bundle.locale()) - } -} - -impl<'a, T, U> Content<'a, T, U> for Localization -where - T: Copy + Into>, - U: Borrow>, -{ - #[instrument(fields(request = %request.into()), skip_all)] - fn content(&self, request: T) -> Option { - self.0.values().find_map(|bundle| { - let content = bundle.content(request); - trace!(locale = %bundle.locale(), ?content); - content - }) - } -} - -impl Debug for Localization { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_tuple("Localization") - .field(&self.locales().collect::>()) - .finish() - } -} - -impl Deref for Localization { - type Target = IndexMap, BundleAsset>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Localization { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} diff --git a/src/resources/mod.rs b/src/resources/mod.rs index a1c18a0..3094848 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -4,7 +4,7 @@ //! [`Resource`](bevy::ecs::system::Resource). #[doc(inline)] -pub use self::{locale::Locale, localization::Localization}; +pub use self::{bundles::Bundles, locales::Locales}; -mod locale; -mod localization; +mod bundles; +mod locales; diff --git a/src/systems/mod.rs b/src/systems/mod.rs deleted file mode 100644 index edccaf7..0000000 --- a/src/systems/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Systems -//! -//! Any entity located directly in this module is -//! [`System`](bevy::ecs::system::System). - -pub mod parameters; diff --git a/src/systems/parameters/mod.rs b/src/systems/parameters/mod.rs deleted file mode 100644 index 1fbaac2..0000000 --- a/src/systems/parameters/mod.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! System parameters -//! -//! Any entity located directly in this module is -//! [`SystemParam`](bevy::ecs::system::SystemParam). - -use crate::{exts::fluent::BundleExt, BundleAsset, Locale, Localization, ResourceAsset}; -use bevy::{asset::LoadedFolder, ecs::system::SystemParam, prelude::*}; -use std::{any::TypeId, collections::HashMap}; - -/// Localization builder -#[derive(SystemParam)] -pub struct LocalizationBuilder<'w> { - loaded_folders: Res<'w, Assets>, - assets: Res<'w, Assets>, - locale: Res<'w, Locale>, -} - -impl LocalizationBuilder<'_> { - pub fn build(&self, handle: &Handle) -> Localization { - let mut localization = Localization::new(); - if let Some(loaded_folder) = self.loaded_folders.get(handle) { - let locale_entries: HashMap<_, _> = loaded_folder - .handles - .iter() - .filter_map(|untyped_handle| { - if untyped_handle.type_id() != TypeId::of::() { - if untyped_handle.type_id() != TypeId::of::() { - warn!( - r#""{:?}" locale folder contains not only `BundleAsset` or `ResourceAsset` "{:?}"."#, - handle.path(), untyped_handle.path() - ); - } - return None; - } - // TODO - let typed_handle = untyped_handle.clone_weak().typed(); - if let Some(asset) = self.assets.get(&typed_handle) { - Some((asset.locale(), Entry { handle: typed_handle, asset })) - } else { - error!( - "{:?} `BundleAsset` didn't receive.", - typed_handle.path(), - ); - None - } - }) - .collect(); - let locales = self.locale.fallback_chain(locale_entries.keys().cloned()); - for locale in locales { - localization.insert(&locale_entries[locale].handle, locale_entries[locale].asset); - } - } else { - error!("{:?} locale folder didn't load.", handle.path()); - } - localization - } -} - -struct Entry<'a> { - handle: Handle, - asset: &'a BundleAsset, -}