use std::path::PathBuf;
use std::str::FromStr;
use bevy::prelude::*;
use bevy_cobweb::prelude::*;
use fluent_langneg::{negotiate_languages, LanguageIdentifier, LangugeIdentifierParserError, NegotiationStrategy};
use crate::prelude::*;
#[derive(Resource, Default, Deref, DerefMut)]
struct CachedRequestedLangs(Vec<LanguageIdentifier>);
fn load_manifest(
In((default_meta, alt_metas)): In<(LocalizationMeta, Vec<LocalizationMeta>)>,
mut cached: ResMut<CachedRequestedLangs>,
mut c: Commands,
mut manifest: ResMut<LocalizationManifest>,
locale: Res<Locale>,
)
{
if **cached != locale.requested {
**cached = locale.requested.clone();
}
manifest.reset(default_meta, alt_metas);
manifest.negotiate(&locale.requested);
tracing::info!("app languages set to {:?} from requested {:?}", manifest.negotiated(), locale.requested);
c.react().broadcast(LocalizationManifestUpdated);
c.react().broadcast(LanguagesNegotiated);
}
fn update_negotiated_languages(
mut cached: ResMut<CachedRequestedLangs>,
mut c: Commands,
mut manifest: ResMut<LocalizationManifest>,
locale: Res<Locale>,
)
{
if **cached == locale.requested {
tracing::trace!("ignoring modified Locale whose contents did not change");
return;
}
**cached = locale.requested.clone();
manifest.negotiate(&locale.requested);
tracing::info!("app languages set to {:?} from requested {:?}", manifest.negotiated(), locale.requested);
c.react().broadcast(LanguagesNegotiated);
}
pub struct LocalizationManifestUpdated;
pub struct LanguagesNegotiated;
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
pub struct LocalizationMetaReflected
{
pub id: String,
#[reflect(default)]
pub name: Option<String>,
pub manifest: PathBuf,
#[reflect(default)]
pub allow_as_fallback: bool,
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct LocalizationMeta
{
pub id: LanguageIdentifier,
pub name: Option<String>,
pub manifest: PathBuf,
pub allow_as_fallback: bool,
}
impl LocalizationMeta
{
pub fn display_name(&self) -> String
{
self.name.clone().unwrap_or_else(|| format!("{}", self.id))
}
}
impl TryFrom<LocalizationMetaReflected> for LocalizationMeta
{
type Error = LangugeIdentifierParserError;
fn try_from(value: LocalizationMetaReflected) -> Result<Self, Self::Error>
{
Ok(Self {
id: LanguageIdentifier::from_str(value.id.as_str())?,
name: value.name,
manifest: value.manifest,
allow_as_fallback: value.allow_as_fallback,
})
}
}
#[derive(Resource, Debug, Default)]
pub struct LocalizationManifest
{
languages: Vec<LocalizationMeta>,
negotiated: Vec<LanguageIdentifier>,
}
impl LocalizationManifest
{
fn reset(&mut self, mut default: LocalizationMeta, alternates: Vec<LocalizationMeta>)
{
if self.languages.len() > 0 {
tracing::warn!("overwriting LocalizationManifest, only one instance of LoadLocalizationManifest may \
be loaded");
}
default.allow_as_fallback = true;
self.languages.clear();
self.languages.push(default);
self.languages.extend(alternates);
}
fn negotiate(&mut self, requested: &[LanguageIdentifier])
{
let allowed_fallbacks: Vec<&LanguageIdentifier> = self
.languages()
.iter()
.filter_map(|meta| match meta.allow_as_fallback {
true => Some(&meta.id),
false => None,
})
.collect();
let system_default = Locale::get_system_locale()
.map(|system_locale| {
negotiate_languages(&[system_locale], &allowed_fallbacks, None, NegotiationStrategy::Lookup)
.get(0)
.cloned()
})
.flatten();
let all_available: Vec<&LanguageIdentifier> = self.languages().iter().map(|meta| &meta.id).collect();
let mut negotiated: Vec<LanguageIdentifier> = negotiate_languages(
requested,
&all_available,
system_default,
NegotiationStrategy::Filtering,
)
.iter()
.map(|l| (**l).clone())
.collect();
let first_fallback = negotiated
.iter()
.position(|n| {
self.languages()
.iter()
.find(|l| l.id == *n)
.unwrap()
.allow_as_fallback
})
.unwrap_or(negotiated.len());
negotiated.truncate(first_fallback + 1);
if let Some(global_default) = self.get_default() {
if !negotiated.contains(&global_default.id) {
negotiated.push(global_default.id.clone());
}
}
self.negotiated = negotiated;
}
pub fn languages(&self) -> &[LocalizationMeta]
{
&self.languages
}
pub fn negotiated(&self) -> &[LanguageIdentifier]
{
&self.negotiated
}
pub fn iter_negotiated_metas(&self) -> impl Iterator<Item = &LocalizationMeta> + '_
{
self.negotiated.iter().map(|id| self.get(id).unwrap())
}
pub fn get(&self, id: &LanguageIdentifier) -> Option<&LocalizationMeta>
{
self.languages.iter().find(|m| m.id == *id)
}
pub fn get_default(&self) -> Option<&LocalizationMeta>
{
self.languages.get(0)
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
pub struct LoadLocalizationManifest
{
pub default: LocalizationMetaReflected,
#[reflect(default)]
pub alts: Vec<LocalizationMetaReflected>,
}
impl Command for LoadLocalizationManifest
{
fn apply(mut self, world: &mut World)
{
let default_meta = match LocalizationMeta::try_from(self.default) {
Ok(default_meta) => default_meta,
Err(err) => {
tracing::error!("failed parsing LoadLocalizationManifest, default language has an invalid id {:?}",
err);
return;
}
};
let alt_metas: Vec<LocalizationMeta> = self
.alts
.drain(..)
.map(LocalizationMeta::try_from)
.filter_map(|maybe_meta| match maybe_meta {
Ok(meta) => Some(meta),
Err(err) => {
tracing::error!("ignoring alternate language in LoadLocalizationManifest with invalid language \
id {:?}", err);
None
}
})
.collect();
world.syscall((default_meta, alt_metas), load_manifest);
}
}
pub(crate) struct LocalizationManifestPlugin;
impl Plugin for LocalizationManifestPlugin
{
fn build(&self, app: &mut App)
{
app.init_resource::<CachedRequestedLangs>()
.init_resource::<LocalizationManifest>()
.register_type::<LocalizationMetaReflected>()
.register_type::<Vec<LocalizationMetaReflected>>()
.register_type::<Option<String>>()
.register_command_type::<LoadLocalizationManifest>()
.add_systems(
PostUpdate,
update_negotiated_languages
.run_if(resource_changed::<Locale>)
.in_set(LocalizationSet::Negotiate)
.run_if(in_state(LoadState::Done)),
);
}
}