1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
//! Localization module.

use std::{
    borrow::Cow,
    marker::PhantomData,
    path::PathBuf,
    str::FromStr,
    sync::{Arc, OnceLock},
};

use crate::prelude::*;

use fluent::{FluentArgs, FluentResource};
use intl_memoizer::concurrent::IntlLangMemoizer;
use unic_langid::LanguageIdentifier;

pub use fluent;
pub use fluent::fluent_args;
pub use fluent_langneg;
pub use intl_memoizer;
pub use sys_locale;
pub use unic_langid;

/// Specialization of of the fluent bundle that is used by bones_framework.
pub type FluentBundle = fluent::bundle::FluentBundle<FluentResourceAsset, IntlLangMemoizer>;

/// An asset containing a [`FluentResource`].
#[derive(HasSchema, Deref, DerefMut, Clone)]
#[schema(opaque, no_default)]
#[type_data(asset_loader(["ftl"], FluentResourceLoader))]
pub struct FluentResourceAsset(pub Arc<FluentResource>);
impl std::borrow::Borrow<FluentResource> for FluentResourceAsset {
    fn borrow(&self) -> &FluentResource {
        &self.0
    }
}

/// An asset containing a [`FluentBundle`].
#[derive(HasSchema, Deref, DerefMut, Clone)]
#[schema(opaque, no_default)]
#[type_data(asset_loader(["locale.yaml", "locale.yml"], FluentBundleLoader))]
pub struct FluentBundleAsset(pub Arc<FluentBundle>);

/// Asset containing all loaded localizations, and functions for formatting localized messages.
#[derive(HasSchema, Deref, DerefMut, Clone)]
#[schema(opaque, no_default)]
#[type_data(asset_loader(["localization.yaml", "localization.yml"], LocalizationLoader))]
pub struct LocalizationAsset {
    /// The bundle selected as the current language.
    #[deref]
    pub current_bundle: FluentBundleAsset,
    /// The bundles for all loaded languages.
    pub bundles: Arc<[FluentBundleAsset]>,
}

impl LocalizationAsset {
    /// Get a localized message.
    pub fn get(&self, id: &str) -> Cow<'_, str> {
        let b = &self.current_bundle.0;
        let Some(message) = b.get_message(id) else {
            return Cow::from("");
        };
        let Some(value) = message.value() else {
            return Cow::from("");
        };

        // TODO: Log localization formatting errors.
        // We need to find a way to log the errors without allocating every time we format:
        // https://github.com/projectfluent/fluent-rs/issues/323.
        b.format_pattern(value, None, &mut vec![])
    }

    /// Get a localized message with the provided arguments.
    pub fn get_with<'a>(&'a self, id: &'a str, args: &'a FluentArgs) -> Cow<'a, str> {
        let b = &self.current_bundle.0;
        let Some(message) = b.get_message(id) else {
            return Cow::from("");
        };
        let Some(value) = message.value() else {
            return Cow::from("");
        };

        b.format_pattern(value, Some(args), &mut vec![])
    }
}

use dashmap::mapref::one::MappedRef;
/// Borrow the localization field from the root asset.
///
/// This parameter uses the schema implementation to find the field of the root asset that is a
/// [`Handle<LocalizationAsset>`].
#[derive(Deref, DerefMut)]
pub struct Localization<'a, T> {
    #[deref]
    asset: MappedRef<'a, Cid, LoadedAsset, LocalizationAsset>,
    _phantom: PhantomData<T>,
}

/// Internal resource used to cache the field of the root asset containing the localization resource
/// for the [`Localization`] parameter.
#[derive(HasSchema, Default, Clone)]
pub struct RootLocalizationFieldIdx(OnceLock<usize>);

impl<T: HasSchema> SystemParam for Localization<'_, T> {
    type State = (AssetServer, AtomicResource<RootLocalizationFieldIdx>);
    type Param<'s> = Localization<'s, T>;

    fn get_state(world: &World) -> Self::State {
        (
            (*world.resources.get::<AssetServer>().unwrap()).clone(),
            world.resources.get_cell::<RootLocalizationFieldIdx>(),
        )
    }
    fn borrow<'s>(
        world: &'s World,
        (asset_server, field_idx): &'s mut Self::State,
    ) -> Self::Param<'s> {
        const ERR: &str = "Could not find a `Handle<LocalizationAsset>` field on root asset, \
                           needed for `Localization` parameter to work";
        let field_idx = field_idx.init_borrow(world);
        let field_idx = field_idx.0.get_or_init(|| {
            let mut idx = None;
            for (i, field) in T::schema()
                .kind
                .as_struct()
                .expect(ERR)
                .fields
                .iter()
                .enumerate()
            {
                if let Some(handle_data) = field.schema.type_data.get::<SchemaAssetHandle>() {
                    if let Some(schema) = handle_data.inner_schema() {
                        if schema == LocalizationAsset::schema() {
                            idx = Some(i);
                            break;
                        }
                    }
                }
            }
            idx.expect(ERR)
        });

        let root = asset_server.root::<T>();
        let root = root.as_schema_ref();
        let handle = root
            .field(*field_idx)
            .expect(ERR)
            .cast::<Handle<LocalizationAsset>>();
        let asset = asset_server.get(*handle);

        Localization {
            asset,
            _phantom: PhantomData,
        }
    }
}

struct FluentResourceLoader;
impl AssetLoader for FluentResourceLoader {
    fn load(&self, _ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
        let bytes = bytes.to_vec();
        Box::pin(async move {
            let string = String::from_utf8(bytes).context("Error loading fluent resource file.")?;
            let res = FluentResource::try_new(string).map_err(|(_, errors)| {
                let errors = errors
                    .into_iter()
                    .map(|e| e.to_string())
                    .collect::<Vec<_>>()
                    .join("\n");

                anyhow::format_err!("Error loading fluent resource file. \n{}", errors)
            })?;

            Ok(SchemaBox::new(FluentResourceAsset(Arc::new(res))))
        })
    }
}

struct FluentBundleLoader;
impl AssetLoader for FluentBundleLoader {
    fn load(&self, mut ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
        let bytes = bytes.to_vec();
        Box::pin(async move {
            let self_path = ctx.loc.path.clone();
            #[derive(serde::Serialize, serde::Deserialize)]
            struct BundleMeta {
                pub locales: Vec<LanguageIdentifier>,
                pub resources: Vec<PathBuf>,
            }
            let meta: BundleMeta =
                serde_yaml::from_slice(&bytes).context("Could not parse locale YAML")?;

            let mut bundle = FluentBundle::new_concurrent(meta.locales);

            for resource_path in meta.resources {
                let normalized = resource_path
                    .absolutize_from(self_path.parent().unwrap())
                    .unwrap();
                let resource_handle = ctx.load_asset(&normalized)?.typed::<FluentResourceAsset>();
                let resource = loop {
                    if let Some(resource) = ctx.asset_server.try_get(resource_handle) {
                        break resource.context("FluentBundle resource `{normalized:?}` handle could not be cast to `FluentResourceAsset`")?;
                    }
                    ctx.asset_server.load_progress.listen().await;
                };
                bundle.add_resource(resource.clone()).map_err(|e| {
                    anyhow::format_err!(
                    "Error(s) adding resource `{normalized:?}` to bundle `{self_path:?}`: {e:?}"
                )
                })?;
            }

            Ok(SchemaBox::new(FluentBundleAsset(Arc::new(bundle))))
        })
    }
}

struct LocalizationLoader;
impl AssetLoader for LocalizationLoader {
    fn load(&self, mut ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
        let bytes = bytes.to_vec();
        Box::pin(async move {
            let self_path = ctx.loc.path.clone();
            #[derive(serde::Serialize, serde::Deserialize)]
            struct LocalizationMeta {
                pub locales: Vec<PathBuf>,
            }
            let meta: LocalizationMeta =
                serde_yaml::from_slice(&bytes).context("Could not parse locale YAML")?;

            let mut bundles: Vec<FluentBundleAsset> = Vec::new();

            for bundle_path in meta.locales {
                let normalized = bundle_path
                    .absolutize_from(self_path.parent().unwrap())
                    .unwrap();
                let bundle_handle = ctx.load_asset(&normalized)?.typed::<FluentBundleAsset>();
                let bundle = loop {
                    if let Some(bundle) = ctx.asset_server.try_get(bundle_handle) {
                        break bundle.context("Localization resource `{normalized:?}` loaded handle could not be cast to `FluentBundleAsset`")?;
                    }
                    ctx.asset_server.load_progress.listen().await;
                };
                bundles.push(bundle.clone());
            }

            let available_locales = bundles
                .iter()
                .flat_map(|x| x.locales.iter())
                .cloned()
                .collect::<Vec<_>>();

            let en_us = LanguageIdentifier::from_str("en-US").unwrap();
            let user_locale = sys_locale::get_locale()
                .and_then(|x| x.parse::<LanguageIdentifier>().ok())
                .unwrap_or(en_us.clone());

            let selected_locale = fluent_langneg::negotiate_languages(
                &[user_locale.clone()],
                &available_locales,
                Some(&en_us),
                fluent_langneg::NegotiationStrategy::Filtering,
            )[0];

            let selected_bundle = bundles
                .iter()
                .find(|bundle| bundle.locales.contains(selected_locale))
                .ok_or_else(|| {
                    anyhow::format_err!("Could not find matching locale for {user_locale}")
                })?;

            Ok(SchemaBox::new(LocalizationAsset {
                current_bundle: selected_bundle.clone(),
                bundles: bundles.into_iter().collect(),
            }))
        })
    }
}