From ee9ab0fff6f412c211ff4349ab0dae09ec4ba38f Mon Sep 17 00:00:00 2001 From: kgv Date: Fri, 1 Sep 2023 12:16:23 +0300 Subject: [PATCH] 0.8.0 --- .github/dependabot.yml | 4 +- .github/workflows/{ci.yml => rust.yml} | 25 +-- Cargo.toml | 31 ++-- README.md | 13 +- examples/fallback_chain/main.rs | 36 ++-- examples/minimal/main.rs | 14 +- examples/ui/main.rs | 12 +- examples/ui/systems/load.rs | 21 +-- src/assets/bundle.rs | 94 +++++----- src/assets/error.rs | 20 +++ src/assets/mod.rs | 2 + src/assets/resource.rs | 60 +++---- src/exts/bevy/asset.rs | 69 -------- src/exts/bevy/mod.rs | 3 - src/exts/fluent/content.rs | 200 --------------------- src/exts/mod.rs | 4 - src/exts/path.rs | 234 ------------------------- src/lib.rs | 5 +- src/plugins/mod.rs | 10 +- src/resources/localization.rs | 15 ++ src/systems/mod.rs | 54 ------ src/systems/parameters/mod.rs | 57 ++++-- 22 files changed, 235 insertions(+), 748 deletions(-) rename .github/workflows/{ci.yml => rust.yml} (70%) create mode 100644 src/assets/error.rs delete mode 100644 src/exts/bevy/asset.rs delete mode 100644 src/exts/bevy/mod.rs delete mode 100644 src/exts/fluent/content.rs delete mode 100644 src/exts/path.rs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 97cb4e8..bd055ee 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,9 +3,9 @@ updates: - package-ecosystem: cargo directory: / schedule: - interval: weekly + interval: monthly - package-ecosystem: github-actions directory: / schedule: - interval: weekly + interval: monthly diff --git a/.github/workflows/ci.yml b/.github/workflows/rust.yml similarity index 70% rename from .github/workflows/ci.yml rename to .github/workflows/rust.yml index d10a1d7..fe5cb99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/rust.yml @@ -1,4 +1,4 @@ -name: ci +name: Rust on: push: @@ -8,6 +8,7 @@ on: env: CARGO_TERM_COLOR: always + RUSTFLAGS: -D warnings jobs: build: @@ -18,22 +19,22 @@ jobs: exclude: - os: macos-latest toolchain: nightly - runs-on: ${{ matrix.os }} - steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 + - uses: dtolnay/rust-toolchain@master with: - toolchain: ${{ matrix.toolchain }} components: rustfmt, clippy - override: true - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features - - name: Install alsa and udev - run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev + toolchain: ${{ matrix.toolchain }} + - name: fmt + if: matrix.toolchain == 'nightly' + run: cargo fmt --all -- --check + - name: check + run: cargo check + - name: update & install if: runner.os == 'linux' + run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev - name: build & test run: cargo test --verbose + - name: clippy + run: cargo clippy --all diff --git a/Cargo.toml b/Cargo.toml index fcfb7c2..218e51e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_fluent" -version = "0.7.0" +version = "0.8.0" authors = ["g "] edition = "2021" description = "Bevy plugin for localization using Fluent" @@ -10,32 +10,31 @@ repository = "https://github.com/kgv/bevy_fluent" license = "MIT OR Apache-2.0" keywords = ["bevy", "gamedev", "internationalization", "localization", "plugin"] categories = [ - "games", "game-development", + "games", "internationalization", "localization", ] exclude = [".github/**/*"] [dependencies] -anyhow = "1.0.70" -bevy = { version = "0.11.0", default-features = false, features = [ - "bevy_asset", -] } +bevy = { version = "0.12", default-features = false, features = ["bevy_asset"] } fluent = "0.16.0" -fluent-langneg = "0.13.0" fluent_content = "0.0.5" -globset = "0.4.10" -indexmap = { version = "2.0.0", features = ["serde"] } +fluent-langneg = "0.13.0" +futures-lite = "2.0.0" +indexmap = { version = "2.1.0", features = ["serde"] } intl-memoizer = "0.5.1" -ron = "0.8.0" -serde = { version = "1.0.160", features = ["derive"] } -serde_yaml = "0.9.21" -thiserror = "1.0.40" -tracing = "0.1.37" +ron = "0.8.1" +serde = { version = "1.0.188", 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.3.1", features = ["serde", "v4", "v5"] } +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.11.0" +bevy = "0.12" unic-langid = { version = "0.9.1", features = ["macros"] } diff --git a/README.md b/README.md index d1b17d8..7959c21 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,13 @@ ## Version -| bevy | bevy_fluent | -|-------|-------------| -| 0.11 | 0.7 | -| 0.10 | 0.6 | -| 0.9 | 0.5 | -| 0.8 | 0.4 | +| bevy | bevy_fluent | +|------|-------------| +| 0.12 | 0.8 | +| 0.11 | 0.7 | +| 0.10 | 0.6 | +| 0.9 | 0.5 | +| 0.8 | 0.4 | ## See Also diff --git a/examples/fallback_chain/main.rs b/examples/fallback_chain/main.rs index be77115..5b6cb28 100644 --- a/examples/fallback_chain/main.rs +++ b/examples/fallback_chain/main.rs @@ -1,4 +1,7 @@ -use bevy::{asset::LoadState, prelude::*}; +use bevy::{ + asset::{LoadState, LoadedFolder}, + prelude::*, +}; use bevy_fluent::prelude::*; use fluent_content::Content; use unic_langid::langid; @@ -6,11 +9,13 @@ use unic_langid::langid; pub fn main() { App::new() .insert_resource(Locale::new(langid!("ru-RU")).with_default(langid!("en-US"))) - .add_plugins(DefaultPlugins.set(AssetPlugin { - asset_folder: "examples/fallback_chain/assets".to_string(), - ..default() - })) - .add_plugins(FluentPlugin) + .add_plugins(( + DefaultPlugins.set(AssetPlugin { + file_path: "examples/fallback_chain/assets".to_string(), + ..default() + }), + FluentPlugin, + )) .add_systems(Update, localized_hello_world) .run(); } @@ -18,20 +23,19 @@ pub fn main() { fn localized_hello_world( localization_builder: LocalizationBuilder, asset_server: Res, + mut handle: Local>>, mut localization: Local>, - mut handles: Local>>>, ) { - let handles = - handles.get_or_insert_with(|| asset_server.load_glob("locales/**/main.ftl.ron").unwrap()); - let load_state = asset_server.get_group_load_state(handles.iter().map(Handle::id)); - if let LoadState::Loaded = load_state { - let localization = - localization.get_or_insert_with(|| localization_builder.build(&*handles)); + 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)); // From ru-RU bundle, the first in fallback chain. - assert!(matches!(localization.content("hello"), Some(v) if v == "привет")); + assert!(matches!(localization.content("hello"), Some(content) if content == "привет")); // From ru-BY bundle, the second in fallback chain. - assert!(matches!(localization.content("world"), Some(v) if v == "свету")); + assert!(matches!(localization.content("world"), Some(content) if content == "свету")); // From en-US bundle, the last in fallback chain, default locale. - assert!(matches!(localization.content("hello-world"), Some(v) if v == "hello world")); + assert!( + matches!(localization.content("hello-world"), Some(content) if content == "hello world") + ); } } diff --git a/examples/minimal/main.rs b/examples/minimal/main.rs index cf065ce..e04f61a 100644 --- a/examples/minimal/main.rs +++ b/examples/minimal/main.rs @@ -6,11 +6,13 @@ use unic_langid::langid; pub fn main() { App::new() .insert_resource(Locale::new(langid!("en-US"))) - .add_plugins(DefaultPlugins.set(AssetPlugin { - asset_folder: "examples/minimal/assets".to_string(), - ..default() - })) - .add_plugins(FluentPlugin) + .add_plugins(( + DefaultPlugins.set(AssetPlugin { + file_path: "examples/minimal/assets".to_string(), + ..default() + }), + FluentPlugin, + )) .add_systems(Update, localized_hello_world) .run(); } @@ -21,7 +23,7 @@ fn localized_hello_world( mut handle: Local>>, ) { let handle = &*handle.get_or_insert_with(|| asset_server.load("locales/en-US/main.ftl.yml")); - if let LoadState::Loaded = asset_server.get_load_state(handle) { + 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/ui/main.rs b/examples/ui/main.rs index 2f95c3f..0077ed4 100644 --- a/examples/ui/main.rs +++ b/examples/ui/main.rs @@ -10,11 +10,13 @@ use bevy_fluent::prelude::*; fn main() { App::new() - .add_plugins(DefaultPlugins.set(AssetPlugin { - asset_folder: "examples/ui/assets".to_string(), - ..default() - })) - .add_plugins(FluentPlugin) + .add_plugins(( + DefaultPlugins.set(AssetPlugin { + file_path: "examples/ui/assets".to_string(), + ..default() + }), + FluentPlugin, + )) .insert_resource(Locale::new(ru::RU).with_default(en::US)) .insert_resource(Locales(vec![de::DE, en::US, ru::BY, ru::RU])) .init_resource::() diff --git a/examples/ui/systems/load.rs b/examples/ui/systems/load.rs index 4bb81a0..795ad1f 100644 --- a/examples/ui/systems/load.rs +++ b/examples/ui/systems/load.rs @@ -1,12 +1,13 @@ use crate::GameState; -use bevy::{asset::LoadState, prelude::*}; +use bevy::{ + asset::{LoadState, LoadedFolder}, + prelude::*, +}; use bevy_fluent::prelude::*; pub fn setup(mut commands: Commands, asset_server: Res) { - let handles = asset_server - .load_glob::("locales/**/menu.ftl.ron") - .unwrap(); - commands.insert_resource(Handles(handles)); + let handle = asset_server.load_folder("locales"); + commands.insert_resource(LocaleFolder(handle)); } pub fn update( @@ -14,15 +15,15 @@ pub fn update( localization_builder: LocalizationBuilder, asset_server: Res, mut next_state: ResMut>, - handles: Res, + locale_folder: Res, ) { - if let LoadState::Loaded = asset_server.get_group_load_state(handles.0.iter().map(Handle::id)) { - let localization = localization_builder.build(&handles.0); - commands.remove_resource::(); + 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); next_state.set(GameState::Menu); } } #[derive(Resource)] -pub struct Handles(Vec>); +pub struct LocaleFolder(Handle); diff --git a/src/assets/bundle.rs b/src/assets/bundle.rs index 9e812d5..67d8df7 100644 --- a/src/assets/bundle.rs +++ b/src/assets/bundle.rs @@ -1,9 +1,9 @@ //! Bundle asset -use crate::{assets::resource, ResourceAsset}; -use anyhow::Result; +use super::{Error, Result}; +use crate::ResourceAsset; use bevy::{ - asset::{AssetLoader, AssetPath, LoadContext, LoadedAsset}, + asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, prelude::*, reflect::{TypePath, TypeUuid}, utils::{ @@ -13,63 +13,22 @@ use bevy::{ }; use fluent::{bundle::FluentBundle, FluentResource}; use intl_memoizer::concurrent::IntlLangMemoizer; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::{ops::Deref, path::PathBuf, str, sync::Arc}; use unic_langid::LanguageIdentifier; -#[instrument(fields(path = %load_context.path().display()), ret, skip_all)] -async fn load(data: Data, load_context: &mut LoadContext<'_>) -> Result<()> { - let mut bundle = FluentBundle::new_concurrent(vec![data.locale.clone()]); - let mut asset_paths = Vec::new(); - let parent = load_context.path().parent(); - for mut path in data.resources { - if path.is_relative() { - if let Some(parent) = parent { - path = parent.join(path); - } - } - let bytes = load_context.read_asset_bytes(&path).await?; - let resource = resource::deserialize(&bytes)?; - if let Err(errors) = bundle.add_resource(resource) { - warn_span!("add_resource").in_scope(|| { - for error in errors { - warn!(%error); - } - }); - } - asset_paths.push(AssetPath::new(path, None)); - } - - let resource_handles = asset_paths - .iter() - .map(|path| load_context.get_handle(path.clone())) - .collect::>(); - load_context.set_default_asset( - LoadedAsset::new(BundleAsset { - bundle: Arc::new(bundle), - resource_handles, - }) - .with_dependencies(asset_paths), - ); - Ok(()) -} - /// [`FluentBundle`](fluent::bundle::FluentBundle) wrapper /// /// Collection of [`FluentResource`]s for a single locale -#[derive(Clone, TypePath, TypeUuid)] +#[derive(Asset, Clone, TypePath, TypeUuid)] #[uuid = "929113bb-9187-44c3-87be-6027fc3b7ac5"] -pub struct BundleAsset { - pub(crate) bundle: Arc, IntlLangMemoizer>>, - /// The resource handles that this bundle depends on - pub(crate) resource_handles: Vec>, -} +pub struct BundleAsset(pub(crate) Arc, IntlLangMemoizer>>); impl Deref for BundleAsset { type Target = FluentBundle, IntlLangMemoizer>; fn deref(&self) -> &Self::Target { - &self.bundle + &self.0 } } @@ -78,19 +37,26 @@ impl Deref for BundleAsset { pub struct BundleAssetLoader; impl AssetLoader for BundleAssetLoader { + type Asset = BundleAsset; + type Settings = (); + type Error = Error; + fn load<'a>( &self, - bytes: &'a [u8], + reader: &'a mut Reader, + _: &'a Self::Settings, load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result<()>> { + ) -> 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_bytes(bytes)?, load_context).await + load(ron::de::from_str(&content)?, load_context).await } Some(extension) if extension == "yaml" || extension == "yml" => { - load(serde_yaml::from_slice(bytes)?, load_context).await + load(serde_yaml::from_str(&content)?, load_context).await } _ => unreachable!("We already check all the supported extensions."), } @@ -103,9 +69,31 @@ impl AssetLoader for BundleAssetLoader { } /// Data -#[derive(Debug, Deserialize)] +#[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))) +} diff --git a/src/assets/error.rs b/src/assets/error.rs new file mode 100644 index 0000000..6d83c22 --- /dev/null +++ b/src/assets/error.rs @@ -0,0 +1,20 @@ +use bevy::asset::LoadDirectError; +use ron::error::SpannedError; +use std::io; +use thiserror::Error; + +/// Result +pub type Result = std::result::Result; + +/// Error +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] io::Error), + #[error(transparent)] + LoadDirect(#[from] LoadDirectError), + #[error(transparent)] + Ron(#[from] SpannedError), + #[error(transparent)] + Yaml(#[from] serde_yaml::Error), +} diff --git a/src/assets/mod.rs b/src/assets/mod.rs index 2f107f7..f04a7cf 100644 --- a/src/assets/mod.rs +++ b/src/assets/mod.rs @@ -2,8 +2,10 @@ //! //! Any entity located directly in this module is [`Asset`](bevy::asset::Asset). +pub use self::error::{Error, Result}; #[doc(inline)] pub use self::{bundle::BundleAsset, resource::ResourceAsset}; pub mod bundle; +pub mod error; pub mod resource; diff --git a/src/assets/resource.rs b/src/assets/resource.rs index 1b30df2..c1f71ed 100644 --- a/src/assets/resource.rs +++ b/src/assets/resource.rs @@ -1,8 +1,8 @@ //! Resource asset -use anyhow::Result; +use super::{Error, Result}; use bevy::{ - asset::{AssetLoader, LoadContext, LoadedAsset}, + asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, prelude::*, reflect::{TypePath, TypeUuid}, utils::{ @@ -13,30 +13,8 @@ use bevy::{ use fluent::FluentResource; use std::{ops::Deref, str, sync::Arc}; -#[instrument(skip_all)] -pub(crate) fn deserialize(bytes: &[u8]) -> Result> { - let string = str::from_utf8(bytes)?.to_string(); - let fluent_resource = match FluentResource::try_new(string) { - Ok(fluent_resource) => fluent_resource, - Err((fluent_resource, errors)) => { - error_span!("try_new").in_scope(|| { - for error in errors { - error!(%error); - } - }); - fluent_resource - } - }; - Ok(Arc::new(fluent_resource)) -} - -#[instrument(fields(path = %load_context.path().display()), skip_all)] -fn load(data: Arc, load_context: &mut LoadContext<'_>) { - load_context.set_default_asset(LoadedAsset::new(ResourceAsset(data))); -} - /// [`FluentResource`](fluent::FluentResource) wrapper -#[derive(Clone, Debug, TypePath, TypeUuid)] +#[derive(Asset, Clone, Debug, TypePath, TypeUuid)] #[uuid = "0b2367cb-fb4a-4746-a305-df98b26dddf6"] pub struct ResourceAsset(pub(crate) Arc); @@ -54,14 +32,20 @@ impl Deref for ResourceAsset { pub struct ResourceAssetLoader; impl AssetLoader for ResourceAssetLoader { + type Asset = ResourceAsset; + type Settings = (); + type Error = Error; + fn load<'a>( &self, - bytes: &'a [u8], - load_context: &'a mut LoadContext, - ) -> BoxedFuture<'a, Result<()>> { + reader: &'a mut Reader, + _: &'a Self::Settings, + _: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result> { Box::pin(async move { - load(deserialize(bytes)?, load_context); - Ok(()) + let mut content = String::new(); + reader.read_to_string(&mut content).await?; + Ok(ResourceAsset(deserialize(content))) }) } @@ -69,3 +53,19 @@ impl AssetLoader for ResourceAssetLoader { &["ftl"] } } + +#[instrument(skip_all)] +fn deserialize(content: String) -> Arc { + let fluent_resource = match FluentResource::try_new(content) { + Ok(fluent_resource) => fluent_resource, + Err((fluent_resource, errors)) => { + error_span!("try_new").in_scope(|| { + for error in errors { + error!(%error); + } + }); + fluent_resource + } + }; + Arc::new(fluent_resource) +} diff --git a/src/exts/bevy/asset.rs b/src/exts/bevy/asset.rs deleted file mode 100644 index 5f63be7..0000000 --- a/src/exts/bevy/asset.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::exts::PathExt; -use anyhow::Result; -use bevy::{ - asset::{Asset, AssetIo, AssetIoError}, - prelude::*, - utils::tracing::{self, instrument}, -}; -use globset::Glob; -use std::path::{Path, PathBuf}; - -/// Extension methods for [`AssetIo`](bevy::asset::AssetIo) -pub trait AssetIoExt: AssetIo { - /// Visit directory - fn visit_directory( - &self, - directory: &Path, - callback: &mut dyn FnMut(PathBuf), - ) -> Result<(), AssetIoError>; - - fn walk_directory(&self, directory: &Path) -> Result, AssetIoError>; -} - -impl AssetIoExt for T { - fn visit_directory( - &self, - directory: &Path, - callback: &mut dyn FnMut(PathBuf), - ) -> Result<(), AssetIoError> { - if self.is_dir(directory) { - for path in self.read_directory(directory)? { - if self.is_dir(&path) { - self.visit_directory(&path, callback)?; - } else { - callback(path); - } - } - } - Ok(()) - } - - fn walk_directory(&self, directory: &Path) -> Result, AssetIoError> { - let mut paths = Vec::new(); - self.visit_directory(directory, &mut |path| paths.push(path))?; - Ok(paths) - } -} - -/// Extension methods for [`AssetServer`](bevy::asset::AssetServer) -pub trait AssetServerExt { - fn load_glob(&self, path: &str) -> Result>>; -} - -impl AssetServerExt for AssetServer { - #[instrument(fields(glob = ?glob), skip_all)] - fn load_glob(&self, glob: &str) -> Result>> { - let path = Path::new(glob); - let matcher = Glob::new(glob)?.compile_matcher(); - let path = path - .find_prefix(|path| self.asset_io().is_dir(path)) - .unwrap_or_else(|_| Path::new("")); - trace!(base = ?path); - Ok(self - .asset_io() - .walk_directory(path)? - .into_iter() - .filter_map(|path| matcher.is_match(&path).then(|| self.load(path))) - .collect()) - } -} diff --git a/src/exts/bevy/mod.rs b/src/exts/bevy/mod.rs deleted file mode 100644 index 10b6ec4..0000000 --- a/src/exts/bevy/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use self::asset::{AssetIoExt, AssetServerExt}; - -mod asset; diff --git a/src/exts/fluent/content.rs b/src/exts/fluent/content.rs deleted file mode 100644 index 8c72da2..0000000 --- a/src/exts/fluent/content.rs +++ /dev/null @@ -1,200 +0,0 @@ -use bevy::prelude::*; -use fluent::{bundle::FluentBundle, FluentArgs, FluentResource, FluentValue}; -use fmt::Write; -use intl_memoizer::concurrent::IntlLangMemoizer; -use std::{ - borrow::Borrow, - fmt::{self, Display, Formatter}, -}; - -fn parse_args(args: &str) -> FluentArgs { - let mut fluent_args = FluentArgs::new(); - for arg in args.split('&') { - match arg.split_once('=') { - Some((key, value)) => match value { - "" => fluent_args.set(key, FluentValue::None), - value => fluent_args.set(key, value), - }, - None => fluent_args.set(arg.trim_end_matches('='), FluentValue::Error), - } - } - fluent_args -} - -/// Content -pub trait Content<'a, T: Into>, U: Borrow>> { - /// Request message content - fn content(&self, request: T) -> Option; -} - -impl<'a, T, U, V> Content<'a, T, U> for FluentBundle -where - T: Into>, - U: Borrow>, - V: Borrow, -{ - fn content(&self, request: T) -> Option { - let request = request.into(); - let request = request.borrow(); - let message = self.get_message(request.id)?; - let pattern = match request.attr { - Some(key) => message.get_attribute(key)?.value(), - None => message.value()?, - }; - let mut errors = Vec::new(); - let content = self - .format_pattern( - pattern, - request.args.as_ref().map(Borrow::borrow), - &mut errors, - ) - .to_string(); - error_span!("format_pattern").in_scope(|| { - for error in errors { - error!(%error); - } - }); - Some(content) - } -} - -/// Message content request -/// -/// Provides access to a message content. Attribute and arguments are optional. -/// -/// # Examples -/// -/// Only identifier: -/// -/// ``` -/// # use bevy_fluent::exts::fluent::content::Request; -/// # -/// let request = Request::from("id"); -/// ``` -/// -/// Identifier and attribute: -/// -/// ``` -/// # use bevy_fluent::exts::fluent::content::Request; -/// # -/// let request = Request::from("id.attr"); -/// ``` -/// -/// Identifier and argument: -/// -/// ``` -/// # use bevy_fluent::exts::fluent::content::Request; -/// # -/// let request = Request::from("id?key=value"); -/// ``` -/// -/// Identifier attribute and arguments: -/// -/// ``` -/// # use bevy_fluent::exts::fluent::content::Request; -/// # -/// let request = Request::from("id.attr?key1=value1&key2=value2"); -/// ``` -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Request<'a, T> { - pub id: &'a str, - pub attr: Option<&'a str>, - pub args: Option, -} - -impl<'a> Request<'a, &'static FluentArgs<'static>> { - pub fn new(id: &'a str) -> Self { - Self { id, ..default() } - } -} - -impl<'a, T> Request<'a, T> { - pub fn attr(self, attr: &'a str) -> Self { - Self { - attr: Some(attr), - ..self - } - } -} - -impl<'a, T> Request<'a, T> { - pub fn args(self, args: U) -> Request<'a, U> { - Request { - id: self.id, - attr: self.attr, - args: Some(args), - } - } -} - -impl Default for Request<'_, T> { - fn default() -> Self { - Self { - id: default(), - attr: default(), - args: default(), - } - } -} - -impl<'a, T: Borrow>> Display for Request<'_, T> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, r#""{}"#, self.id)?; - if let Some(attribute) = &self.attr { - write!(f, ".{attribute}")?; - } - if let Some(args) = &self.args { - let mut args = args.borrow().iter().peekable(); - if args.peek().is_some() { - f.write_char('?')?; - } - for (key, value) in args { - write!(f, "{key}=")?; - match value { - FluentValue::String(key) => write!(f, "{key}")?, - FluentValue::Number(key) => write!(f, "{}", key.as_string())?, - // FluentValue::Custom(Box), - _ => {} - } - } - } - f.write_char('"')?; - Ok(()) - } -} - -impl<'a> From<&'a String> for Request<'a, FluentArgs<'a>> { - fn from(value: &'a String) -> Self { - Self::from(&**value) - } -} - -impl<'a> From<&'a str> for Request<'a, FluentArgs<'a>> { - fn from(value: &'a str) -> Self { - match value.split_once('.') { - Some((id, value)) => match value.split_once('?') { - Some((attr, args)) => Self { - id, - attr: Some(attr), - args: Some(parse_args(args)), - }, - None => Self { - id, - attr: Some(value), - ..default() - }, - }, - None => match value.split_once('?') { - Some((id, args)) => Self { - id, - args: Some(parse_args(args)), - ..default() - }, - None => Self { - id: value, - ..default() - }, - }, - } - } -} diff --git a/src/exts/mod.rs b/src/exts/mod.rs index 73df057..ccad87c 100644 --- a/src/exts/mod.rs +++ b/src/exts/mod.rs @@ -1,5 +1 @@ -pub use self::path::PathExt; - -pub mod bevy; pub mod fluent; -pub mod path; diff --git a/src/exts/path.rs b/src/exts/path.rs deleted file mode 100644 index d6d68bd..0000000 --- a/src/exts/path.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::path::{Component, Path, StripPrefixError}; -use thiserror::Error; - -fn iter_after_stem<'a, 'b, I, J>(mut iter: I, stem: J) -> Option -where - I: Iterator> + Clone, - J: Iterator> + Clone, -{ - loop { - let mut stem_outer = stem.clone(); - let mut iter_outer = iter.clone(); - let mut iter_next = iter_outer.next(); - let mut iter_inner = iter_outer.clone(); - loop { - match (iter_next, stem_outer.next()) { - (Some(x), Some(y)) if x == y => {} - (Some(_), Some(_)) => break, - (None, Some(_)) => return None, - (_, None) => return Some(iter), - } - iter = iter_inner.clone(); - iter_next = iter_inner.next(); - } - iter = iter_outer; - } -} - -fn iter_before_stem<'a, 'b, I, J>(mut iter: I, stem: J) -> Option -where - I: DoubleEndedIterator> + Clone, - J: DoubleEndedIterator> + Clone, -{ - loop { - let mut stem_outer = stem.clone(); - let mut iter_outer = iter.clone(); - let mut iter_next = iter_outer.next_back(); - let mut iter_inner = iter_outer.clone(); - loop { - match (iter_next, stem_outer.next_back()) { - (Some(x), Some(y)) if x == y => {} - (Some(_), Some(_)) => break, - (None, Some(_)) => return None, - (_, None) => return Some(iter), - } - iter = iter_inner.clone(); - iter_next = iter_inner.next_back(); - } - iter = iter_outer; - } -} - -fn iter_before_suffix<'a, 'b, I, J>(mut iter: I, mut suffix: J) -> Option -where - I: DoubleEndedIterator> + Clone, - J: DoubleEndedIterator>, -{ - loop { - let mut iter_inner = iter.clone(); - match (iter_inner.next_back(), suffix.next_back()) { - (Some(ref x), Some(ref y)) if x == y => {} - (_, Some(_)) => return None, - (_, None) => return Some(iter), - } - iter = iter_inner; - } -} - -/// Extension methods for [`Path`](std::path::Path) -pub trait PathExt { - fn find_prefix

(&self, predicate: P) -> Result<&Path, PrefixError> - where - P: FnMut(&Self) -> bool; - - fn prefix>(&self, stem: P) -> Result<&Path, PrefixError>; - - fn strip(&self, prefix: P, suffix: Q) -> Result<&Path, StripError> - where - P: AsRef, - Q: AsRef; - - fn strip_suffix>(&self, suffix: P) -> Result<&Path, StripSuffixError>; - - fn suffix>(&self, stem: P) -> Result<&Path, SuffixError>; -} - -impl PathExt for Path { - fn find_prefix

(&self, mut predicate: P) -> Result<&Path, PrefixError> - where - P: FnMut(&Self) -> bool, - { - let mut components = self.components(); - loop { - if predicate(components.as_path()) { - return Ok(components.as_path()); - } - components.next_back().ok_or(PrefixError(()))?; - } - } - - fn prefix>(&self, stem: P) -> Result<&Path, PrefixError> { - iter_before_stem(self.components(), stem.as_ref().components()) - .map(|components| components.as_path()) - .ok_or(PrefixError(())) - } - - fn strip(&self, prefix: P, suffix: Q) -> Result<&Path, StripError> - where - P: AsRef, - Q: AsRef, - { - Ok(self.strip_prefix(prefix)?.strip_suffix(suffix)?) - } - - fn strip_suffix>(&self, suffix: P) -> Result<&Path, StripSuffixError> { - iter_before_suffix(self.components(), suffix.as_ref().components()) - .map(|components| components.as_path()) - .ok_or(StripSuffixError(())) - } - - fn suffix>(&self, stem: P) -> Result<&Path, SuffixError> { - iter_after_stem(self.components(), stem.as_ref().components()) - .map(|components| components.as_path()) - .ok_or(SuffixError(())) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PrefixError(()); - -#[derive(Clone, Debug, Eq, Error, PartialEq)] -pub enum StripError { - #[error(transparent)] - StripPrefixError(#[from] StripPrefixError), - #[error(transparent)] - StripSuffixError(#[from] StripSuffixError), -} - -#[derive(Clone, Debug, Eq, Error, PartialEq)] -#[error("suffix not found")] -pub struct StripSuffixError(()); - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct SuffixError(()); - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test() { - let path = Path::new("mods/mod_name/locales/*/main/bundle.ron"); - assert_eq!(Ok(Path::new("mods/mod_name/locales")), path.prefix("*")); - assert_eq!(Ok(Path::new("main/bundle.ron")), path.suffix("*")); - let path = Path::new("mods/mod_name/locales/**/main/bundle.ron"); - assert_eq!(Ok(Path::new("mods/mod_name/locales")), path.prefix("**")); - assert_eq!(Ok(Path::new("main/bundle.ron")), path.suffix("**")); - } - - mod prefix { - use super::*; - - #[test] - fn relative() { - let path = Path::new("foo/bar/baz/qux.txt"); - assert_eq!(Ok(Path::new("")), path.prefix("foo")); - assert_eq!(Ok(Path::new("foo")), path.prefix("bar")); - assert_eq!(Ok(Path::new("foo/bar")), path.prefix("baz")); - assert_eq!(Ok(Path::new("foo/bar/baz")), path.prefix("qux.txt")); - assert_eq!(Ok(Path::new("")), path.prefix("foo/bar")); - assert_eq!(Ok(Path::new("foo")), path.prefix("bar/baz")); - assert_eq!(Ok(Path::new("foo/bar")), path.prefix("baz/qux.txt")); - assert_eq!(Ok(Path::new("")), path.prefix("foo/bar/baz")); - assert_eq!(Ok(Path::new("foo")), path.prefix("bar/baz/qux.txt")); - assert_eq!(Ok(Path::new("")), path.prefix("foo/bar/baz/qux.txt")); - } - } - - mod strip_suffix { - use super::*; - - #[test] - fn absolute() { - let path = Path::new("/foo/bar/baz.txt"); - assert_eq!(Ok(Path::new("/foo/bar/baz.txt")), path.strip_suffix("")); - assert_eq!(Ok(Path::new("/foo/bar")), path.strip_suffix("baz.txt")); - assert_eq!(Ok(Path::new("/foo")), path.strip_suffix("bar/baz.txt")); - assert_eq!(Ok(Path::new("/")), path.strip_suffix("foo/bar/baz.txt")); - assert_eq!(Ok(Path::new("")), path.strip_suffix("/foo/bar/baz.txt")); - assert!(matches!(path.strip_suffix("baz"), Err(_))); - assert!(matches!(path.strip_suffix("bar/baz"), Err(_))); - assert!(matches!(path.strip_suffix("/foo/bar/baz"), Err(_))); - assert!(matches!(path.strip_suffix("/foo/bar"), Err(_))); - assert!(matches!(path.strip_suffix("/foo"), Err(_))); - assert!(matches!(path.strip_suffix("/baz.txt"), Err(_))); - assert!(matches!(path.strip_suffix("/bar/baz.txt"), Err(_))); - } - - #[test] - fn relative() { - let path = Path::new("foo/bar/baz.txt"); - assert_eq!(Ok(Path::new("foo/bar/baz.txt")), path.strip_suffix("")); - assert_eq!(Ok(Path::new("foo/bar")), path.strip_suffix("baz.txt")); - assert_eq!(Ok(Path::new("foo")), path.strip_suffix("bar/baz.txt")); - assert_eq!(Ok(Path::new("")), path.strip_suffix("foo/bar/baz.txt")); - assert!(matches!(path.strip_suffix("baz"), Err(_))); - assert!(matches!(path.strip_suffix("bar/baz"), Err(_))); - assert!(matches!(path.strip_suffix("foo/bar/baz"), Err(_))); - assert!(matches!(path.strip_suffix("foo/bar"), Err(_))); - assert!(matches!(path.strip_suffix("foo"), Err(_))); - assert!(matches!(path.strip_suffix("/baz.txt"), Err(_))); - assert!(matches!(path.strip_suffix("/bar/baz.txt"), Err(_))); - assert!(matches!(path.strip_suffix("/foo/bar/baz.txt"), Err(_))); - } - } - - mod suffix { - use super::*; - - #[test] - fn relative() { - let path = Path::new("foo/bar/baz/qux.txt"); - assert_eq!(Ok(Path::new("bar/baz/qux.txt")), path.suffix("foo")); - assert_eq!(Ok(Path::new("baz/qux.txt")), path.suffix("bar")); - assert_eq!(Ok(Path::new("qux.txt")), path.suffix("baz")); - assert_eq!(Ok(Path::new("")), path.suffix("qux.txt")); - assert_eq!(Ok(Path::new("baz/qux.txt")), path.suffix("foo/bar")); - assert_eq!(Ok(Path::new("qux.txt")), path.suffix("bar/baz")); - assert_eq!(Ok(Path::new("")), path.suffix("baz/qux.txt")); - assert_eq!(Ok(Path::new("qux.txt")), path.suffix("foo/bar/baz")); - assert_eq!(Ok(Path::new("")), path.suffix("bar/baz/qux.txt")); - assert_eq!(Ok(Path::new("")), path.suffix("foo/bar/baz/qux.txt")); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index dc14749..001029c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ #[doc(inline)] pub use self::{ assets::{BundleAsset, ResourceAsset}, - exts::bevy::AssetServerExt, plugins::FluentPlugin, resources::{Locale, Localization}, systems::parameters::LocalizationBuilder, @@ -14,9 +13,7 @@ pub use self::{ /// `use bevy_fluent::prelude::*;` to import common assets, components and plugins pub mod prelude { #[doc(inline)] - pub use super::{ - AssetServerExt, BundleAsset, FluentPlugin, Locale, Localization, LocalizationBuilder, - }; + pub use super::{BundleAsset, FluentPlugin, Locale, Localization, LocalizationBuilder}; } pub mod assets; diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index afdc78c..fb80201 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -4,7 +4,6 @@ use crate::{ assets::{bundle::BundleAssetLoader, resource::ResourceAssetLoader}, - systems::update_bundle_asset, BundleAsset, ResourceAsset, }; use bevy::prelude::*; @@ -15,10 +14,9 @@ pub struct FluentPlugin; impl Plugin for FluentPlugin { fn build(&self, app: &mut App) { - app.add_asset::() - .init_asset_loader::() - .add_asset::() - .init_asset_loader::() - .add_systems(PreUpdate, update_bundle_asset); + app.register_asset_loader(ResourceAssetLoader) + .init_asset::() + .register_asset_loader(BundleAssetLoader) + .init_asset::(); } } diff --git a/src/resources/localization.rs b/src/resources/localization.rs index 099a2f6..544f4b3 100644 --- a/src/resources/localization.rs +++ b/src/resources/localization.rs @@ -11,6 +11,7 @@ use indexmap::IndexMap; use std::{ borrow::Borrow, fmt::{self, Debug, Formatter}, + ops::{Deref, DerefMut}, }; use unic_langid::LanguageIdentifier; @@ -60,3 +61,17 @@ impl Debug for Localization { .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/systems/mod.rs b/src/systems/mod.rs index 66fec23..edccaf7 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -3,58 +3,4 @@ //! Any entity located directly in this module is //! [`System`](bevy::ecs::system::System). -use crate::{BundleAsset, ResourceAsset}; -use bevy::prelude::{warn, AssetEvent, Assets, EventReader, Handle, Res, ResMut}; -use fluent::bundle::FluentBundle; -use std::sync::Arc; - -/// Re-loads bundle assets when the resources they depend on changes. -pub(crate) fn update_bundle_asset( - mut resource_updates: EventReader>, - mut bundle_assets: ResMut>, - resource_assets: Res>, -) { - for event in resource_updates.iter() { - let mut bundles_to_update = Vec::new(); - - // If a resource asset is modified - if let AssetEvent::Modified { handle } = event { - // Look for all the bundles that that resource was used in - for (bundle_id, bundle_asset) in bundle_assets.iter() { - for resource_handle in &bundle_asset.resource_handles { - if handle.id() == resource_handle.id() { - bundles_to_update.push(Handle::weak(bundle_id)); - } - } - } - - // Update all bundles that included the resource - for handle in bundles_to_update { - // Get a mutable reference to the old bundle - let bundle = bundle_assets.get_mut(&handle).unwrap(); - - // Create a new bundle to replace it - let mut new_bundle = FluentBundle::new_concurrent(bundle.locales.clone()); - - // Add all resources from the old bundle to the new bundle - for resource_handle in &bundle.resource_handles { - let resource = resource_assets.get(resource_handle).unwrap(); - if let Err(errors) = new_bundle.add_resource(resource.0.clone()) { - for e in errors { - // Skip overriding errors, because we specifically want to override any - // updated messages. - if !matches!(e, fluent::FluentError::Overriding { .. }) { - warn!("Error loading fluent resource: {}", e); - } - } - } - } - - // Update the old bundle - bundle.bundle = Arc::new(new_bundle); - } - } - } -} - pub mod parameters; diff --git a/src/systems/parameters/mod.rs b/src/systems/parameters/mod.rs index 2050002..1fbaac2 100644 --- a/src/systems/parameters/mod.rs +++ b/src/systems/parameters/mod.rs @@ -3,39 +3,60 @@ //! Any entity located directly in this module is //! [`SystemParam`](bevy::ecs::system::SystemParam). -use crate::{exts::fluent::BundleExt, BundleAsset, Locale, Localization}; -use bevy::{ecs::system::SystemParam, prelude::*}; -use std::collections::HashMap; +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<'a>( - &self, - handles: impl IntoIterator>, - ) -> Localization { - let locale_entries: HashMap<_, _> = handles - .into_iter() - .map(|handle| { - let asset = self.assets.get(handle).unwrap(); - (asset.locale(), Entry { handle, asset }) - }) - .collect(); - let locales = self.locale.fallback_chain(locale_entries.keys().cloned()); + pub fn build(&self, handle: &Handle) -> Localization { let mut localization = Localization::new(); - for locale in locales { - localization.insert(locale_entries[locale].handle, locale_entries[locale].asset); + 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: &'a Handle, + handle: Handle, asset: &'a BundleAsset, }