use crate::FluentValue;
use crate::registry::{
StaticFluentArgumentName, StaticFluentDomain, StaticFluentEntryId, StaticFluentVariantKey,
};
use es_fluent_manager_core::FluentManager;
use std::sync::Arc;
const WITH_LOOKUP_CALLBACK_COUNT_ERROR: &str =
"FluentLocalizer::with_lookup must invoke its callback exactly once";
#[derive(Clone, Debug, Default)]
pub struct FluentArgs<'a> {
values: es_fluent_manager_core::FluentArgumentMap<'a>,
}
impl<'a> FluentArgs<'a> {
pub fn new() -> Self {
Self {
values: es_fluent_manager_core::FluentArgumentMap::default(),
}
}
pub fn insert(&mut self, name: StaticFluentArgumentName, value: FluentValue<'a>) {
self.values.insert(name, value);
}
pub fn as_raw(&self) -> &es_fluent_manager_core::FluentArgumentMap<'a> {
&self.values
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
pub type FluentMessageLookup<'lookup> = dyn for<'a> FnMut(StaticFluentDomain, StaticFluentEntryId, Option<&'a FluentArgs<'a>>) -> String
+ 'lookup;
pub type FluentLocalizerLookup<'lookup> = dyn for<'a> FnMut(
StaticFluentDomain,
StaticFluentEntryId,
Option<&'a FluentArgs<'a>>,
) -> Option<String>
+ 'lookup;
pub trait FluentMessage {
fn to_fluent_string_with(&self, localize: &mut FluentMessageLookup<'_>) -> String;
}
impl<T: FluentMessage + ?Sized> FluentMessage for &T {
fn to_fluent_string_with(&self, localize: &mut FluentMessageLookup<'_>) -> String {
(**self).to_fluent_string_with(localize)
}
}
pub trait FluentLocalizer {
fn localize<'a>(
&self,
id: StaticFluentEntryId,
args: Option<&'a FluentArgs<'a>>,
) -> Option<String>;
fn localize_in_domain<'a>(
&self,
domain: StaticFluentDomain,
id: StaticFluentEntryId,
args: Option<&'a FluentArgs<'a>>,
) -> Option<String>;
fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
let mut lookup =
|domain: StaticFluentDomain, id: StaticFluentEntryId, args: Option<&FluentArgs<'_>>| {
self.localize_in_domain(domain, id, args)
};
f(&mut lookup);
}
}
impl FluentLocalizer for FluentManager {
fn localize<'a>(
&self,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
FluentManager::localize(self, id, args.map(FluentArgs::as_raw))
}
fn localize_in_domain<'a>(
&self,
domain: StaticFluentDomain,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
FluentManager::localize_in_domain(self, domain, id, args.map(FluentArgs::as_raw))
}
fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
FluentManager::with_lookup(self, &mut |lookup| {
let mut typed_lookup =
|domain: StaticFluentDomain,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'_>>| {
lookup(domain, id, args.map(FluentArgs::as_raw))
};
f(&mut typed_lookup);
});
}
}
impl<T: FluentLocalizer + ?Sized> FluentLocalizer for &T {
fn localize<'a>(
&self,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
(**self).localize(id, args)
}
fn localize_in_domain<'a>(
&self,
domain: StaticFluentDomain,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
(**self).localize_in_domain(domain, id, args)
}
fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
(**self).with_lookup(f);
}
}
impl<T: FluentLocalizer + ?Sized> FluentLocalizer for Arc<T> {
fn localize<'a>(
&self,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
(**self).localize(id, args)
}
fn localize_in_domain<'a>(
&self,
domain: StaticFluentDomain,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
(**self).localize_in_domain(domain, id, args)
}
fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
(**self).with_lookup(f);
}
}
pub trait FluentLocalizerExt: FluentLocalizer {
fn try_localize_message<T>(&self, message: &T) -> Option<String>
where
T: FluentMessage + ?Sized,
{
let mut missing = false;
let mut value = None;
let mut callback_invocations = 0;
self.with_lookup(&mut |lookup| {
assert!(
callback_invocations == 0,
"{}",
WITH_LOOKUP_CALLBACK_COUNT_ERROR
);
callback_invocations = 1;
value = Some(message.to_fluent_string_with(&mut |domain, id, args| {
lookup(domain, id, args).unwrap_or_else(|| {
missing = true;
String::new()
})
}));
});
assert!(
callback_invocations == 1,
"{}",
WITH_LOOKUP_CALLBACK_COUNT_ERROR
);
let value = value.expect(WITH_LOOKUP_CALLBACK_COUNT_ERROR);
if missing { None } else { Some(value) }
}
fn localize_message<T>(&self, message: &T) -> String
where
T: FluentMessage + ?Sized,
{
let mut value = None;
let mut callback_invocations = 0;
self.with_lookup(&mut |lookup| {
assert!(
callback_invocations == 0,
"{}",
WITH_LOOKUP_CALLBACK_COUNT_ERROR
);
callback_invocations = 1;
value = Some(message.to_fluent_string_with(&mut |domain, id, args| {
lookup(domain, id, args).unwrap_or_else(|| {
tracing::warn!(
domain = domain.as_str(),
message_id = id.as_str(),
"missing Fluent message"
);
id.as_str().to_string()
})
}));
});
assert!(
callback_invocations == 1,
"{}",
WITH_LOOKUP_CALLBACK_COUNT_ERROR
);
value.expect(WITH_LOOKUP_CALLBACK_COUNT_ERROR)
}
}
impl<T: FluentLocalizer + ?Sized> FluentLocalizerExt for T {}
#[doc(hidden)]
pub trait IntoFluentValue<'a> {
fn into_fluent_value(self) -> FluentValue<'a>;
}
impl<'a, T> IntoFluentValue<'a> for T
where
T: Into<FluentValue<'a>>,
{
fn into_fluent_value(self) -> FluentValue<'a> {
self.into()
}
}
#[doc(hidden)]
pub struct FluentArgumentValue<T> {
value: T,
}
impl<T> FluentArgumentValue<T> {
pub fn new(value: T) -> Self {
Self { value }
}
}
#[doc(hidden)]
pub struct FluentBorrowedArgumentValue<'a, T: ?Sized> {
value: &'a T,
}
impl<'a, T: ?Sized> FluentBorrowedArgumentValue<'a, T> {
pub fn new(value: &'a T) -> Self {
Self { value }
}
}
#[doc(hidden)]
pub struct FluentOptionalArgumentValue<T> {
value: Option<T>,
}
impl<T> FluentOptionalArgumentValue<T> {
pub fn new(value: Option<T>) -> Self {
Self { value }
}
}
#[doc(hidden)]
pub trait IntoFluentArgumentValue<'a> {
fn into_fluent_argument_value(self, localize: &mut FluentMessageLookup<'_>) -> FluentValue<'a>;
}
impl<'a, T> IntoFluentArgumentValue<'a> for FluentArgumentValue<T>
where
T: FluentMessage,
{
fn into_fluent_argument_value(self, localize: &mut FluentMessageLookup<'_>) -> FluentValue<'a> {
self.value.to_fluent_string_with(localize).into()
}
}
impl<'a, 'value, T> IntoFluentArgumentValue<'a> for FluentBorrowedArgumentValue<'value, T>
where
T: FluentMessage + ?Sized,
{
fn into_fluent_argument_value(self, localize: &mut FluentMessageLookup<'_>) -> FluentValue<'a> {
self.value.to_fluent_string_with(localize).into()
}
}
impl<'a, T> IntoFluentArgumentValue<'a> for &FluentArgumentValue<T>
where
T: Clone + IntoFluentValue<'a>,
{
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
self.value.clone().into_fluent_value()
}
}
impl<'a, 'value, T> IntoFluentArgumentValue<'a> for &FluentBorrowedArgumentValue<'value, T>
where
T: Clone + IntoFluentValue<'a>,
{
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
(*self.value).clone().into_fluent_value()
}
}
impl<'a> IntoFluentArgumentValue<'a> for FluentArgumentValue<bool> {
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
bool_fluent_value(self.value)
}
}
impl<'a, 'value> IntoFluentArgumentValue<'a> for FluentBorrowedArgumentValue<'value, bool> {
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
bool_fluent_value(*self.value)
}
}
impl<'a, 'value, 'inner> IntoFluentArgumentValue<'a>
for FluentBorrowedArgumentValue<'value, &'inner bool>
{
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
bool_fluent_value(**self.value)
}
}
impl<'a> IntoFluentArgumentValue<'a> for FluentArgumentValue<StaticFluentVariantKey> {
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
self.value.as_str().into()
}
}
impl<'a> IntoFluentArgumentValue<'a> for FluentOptionalArgumentValue<StaticFluentVariantKey> {
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
match self.value {
Some(value) => value.as_str().into(),
None => FluentValue::None,
}
}
}
fn bool_fluent_value<'a>(value: bool) -> FluentValue<'a> {
if value { "true" } else { "false" }.into()
}
impl<'a> IntoFluentArgumentValue<'a> for FluentOptionalArgumentValue<&bool> {
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
match self.value {
Some(value) => bool_fluent_value(*value),
None => FluentValue::None,
}
}
}
impl<'a> IntoFluentArgumentValue<'a> for FluentOptionalArgumentValue<&&bool> {
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
match self.value {
Some(value) => bool_fluent_value(**value),
None => FluentValue::None,
}
}
}
impl<'a, T> IntoFluentArgumentValue<'a> for FluentOptionalArgumentValue<T>
where
T: FluentMessage,
{
fn into_fluent_argument_value(self, localize: &mut FluentMessageLookup<'_>) -> FluentValue<'a> {
match self.value {
Some(value) => value.to_fluent_string_with(localize).into(),
None => FluentValue::None,
}
}
}
impl<'a, T> IntoFluentArgumentValue<'a> for &FluentOptionalArgumentValue<&T>
where
T: Clone + IntoFluentValue<'a>,
{
fn into_fluent_argument_value(
self,
_localize: &mut FluentMessageLookup<'_>,
) -> FluentValue<'a> {
match self.value {
Some(value) => (*value).clone().into_fluent_value(),
None => FluentValue::None,
}
}
}
impl<'a, T> IntoFluentArgumentValue<'a> for FluentArgumentValue<Option<T>>
where
T: FluentMessage,
{
fn into_fluent_argument_value(self, localize: &mut FluentMessageLookup<'_>) -> FluentValue<'a> {
match self.value {
Some(value) => value.to_fluent_string_with(localize).into(),
None => FluentValue::None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, RwLock, mpsc};
use std::time::Duration;
fn static_domain(value: &'static str) -> StaticFluentDomain {
StaticFluentDomain::try_new(value).expect("valid test domain")
}
fn static_entry(value: &'static str) -> StaticFluentEntryId {
StaticFluentEntryId::try_new(value).expect("valid test message id")
}
fn panic_lookup<'a>(
_domain: StaticFluentDomain,
_id: StaticFluentEntryId,
_args: Option<&FluentArgs<'a>>,
) -> String {
panic!("ordinary arguments should not invoke nested localization")
}
fn assert_string(value: FluentValue<'_>, expected: &str) {
match value {
FluentValue::String(value) => assert_eq!(value.as_ref(), expected),
other => panic!("expected string FluentValue, got {other:?}"),
}
}
fn assert_number(value: FluentValue<'_>, expected: f64) {
match value {
FluentValue::Number(value) => assert_eq!(value.value, expected),
other => panic!("expected number FluentValue, got {other:?}"),
}
}
#[test]
fn argument_conversion_handles_primitive_values() {
let mut localize = panic_lookup;
let string_value =
FluentArgumentValue::new("borrowed").into_fluent_argument_value(&mut localize);
assert_string(string_value, "borrowed");
let number_value =
FluentArgumentValue::new(42i32).into_fluent_argument_value(&mut localize);
assert_number(number_value, 42.0);
let bool_value = FluentArgumentValue::new(true).into_fluent_argument_value(&mut localize);
assert_string(bool_value, "true");
let false_value = FluentArgumentValue::new(false).into_fluent_argument_value(&mut localize);
assert_string(false_value, "false");
let choice_value = FluentArgumentValue::new(
StaticFluentVariantKey::try_new("selected").expect("valid choice"),
)
.into_fluent_argument_value(&mut localize);
assert_string(choice_value, "selected");
let borrowed_bool = true;
let borrowed_bool_value = FluentBorrowedArgumentValue::new(&borrowed_bool)
.into_fluent_argument_value(&mut localize);
assert_string(borrowed_bool_value, "true");
}
#[test]
#[should_panic(expected = "ordinary arguments should not invoke nested localization")]
fn panic_lookup_reports_unexpected_nested_localization() {
let _ = panic_lookup(static_domain("domain"), static_entry("id"), None);
}
#[test]
fn argument_conversion_handles_optional_and_missing_values() {
let mut localize = panic_lookup;
let optional = Some("optional");
let missing: Option<String> = None;
let optional_number = Some(7i32);
let optional_bool = Some(false);
let missing_bool: Option<bool> = None;
let optional_value = FluentOptionalArgumentValue::new(optional.as_ref())
.into_fluent_argument_value(&mut localize);
assert_string(optional_value, "optional");
let missing_value = FluentOptionalArgumentValue::new(missing.as_ref())
.into_fluent_argument_value(&mut localize);
assert!(matches!(missing_value, FluentValue::None));
let optional_number = FluentOptionalArgumentValue::new(optional_number.as_ref())
.into_fluent_argument_value(&mut localize);
assert_number(optional_number, 7.0);
let optional_bool = FluentOptionalArgumentValue::new(optional_bool.as_ref())
.into_fluent_argument_value(&mut localize);
assert_string(optional_bool, "false");
let missing_bool = FluentOptionalArgumentValue::new(missing_bool.as_ref())
.into_fluent_argument_value(&mut localize);
assert!(matches!(missing_bool, FluentValue::None));
}
#[test]
fn argument_conversion_handles_borrowed_and_owned_values() {
let mut localize = panic_lookup;
let borrowed = String::from("borrowed string");
let borrowed_value =
FluentArgumentValue::new(&borrowed).into_fluent_argument_value(&mut localize);
assert_string(borrowed_value, "borrowed string");
let owned_value = FluentArgumentValue::new(String::from("owned string"))
.into_fluent_argument_value(&mut localize);
assert_string(owned_value, "owned string");
}
struct NestedMessage;
impl FluentMessage for NestedMessage {
fn to_fluent_string_with(&self, localize: &mut FluentMessageLookup<'_>) -> String {
localize(
static_domain("nested-domain"),
static_entry("nested-id"),
None,
)
}
}
#[test]
fn argument_conversion_localizes_nested_messages_with_current_callback() {
let mut localize =
|domain: StaticFluentDomain, id: StaticFluentEntryId, args: Option<&FluentArgs<'_>>| {
assert_eq!(domain.as_str(), "nested-domain");
assert_eq!(id.as_str(), "nested-id");
assert!(args.is_none());
"nested value".to_string()
};
let value =
FluentArgumentValue::new(NestedMessage).into_fluent_argument_value(&mut localize);
assert_string(value, "nested value");
}
#[test]
fn argument_conversion_localizes_optional_nested_messages_with_current_callback() {
let mut localize =
|domain: StaticFluentDomain, id: StaticFluentEntryId, args: Option<&FluentArgs<'_>>| {
assert_eq!(domain.as_str(), "nested-domain");
assert_eq!(id.as_str(), "nested-id");
assert!(args.is_none());
"optional nested value".to_string()
};
let value =
FluentArgumentValue::new(Some(NestedMessage)).into_fluent_argument_value(&mut localize);
assert_string(value, "optional nested value");
let missing = FluentArgumentValue::new(Option::<NestedMessage>::None)
.into_fluent_argument_value(&mut localize);
assert!(matches!(missing, FluentValue::None));
}
struct StaticLocalizer {
value: &'static str,
}
impl FluentLocalizer for StaticLocalizer {
fn localize<'a>(
&self,
id: StaticFluentEntryId,
_args: Option<&FluentArgs<'a>>,
) -> Option<String> {
if id == "nested-id" {
Some(self.value.to_string())
} else {
None
}
}
fn localize_in_domain<'a>(
&self,
domain: StaticFluentDomain,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
if domain == "nested-domain" {
self.localize(id, args)
} else {
None
}
}
}
#[test]
fn localize_message_uses_the_explicit_localizer() {
let en = StaticLocalizer { value: "Hello" };
let fr = StaticLocalizer { value: "Bonjour" };
assert_eq!(en.localize_message(&NestedMessage), "Hello");
assert_eq!(fr.localize_message(&NestedMessage), "Bonjour");
assert_eq!(en.localize_message(&NestedMessage), "Hello");
}
struct MissingMessage;
impl FluentMessage for MissingMessage {
fn to_fluent_string_with(&self, localize: &mut FluentMessageLookup<'_>) -> String {
localize(
static_domain("missing-domain"),
static_entry("missing-id"),
None,
)
}
}
struct CallbackOnlyMessage;
impl FluentMessage for CallbackOnlyMessage {
fn to_fluent_string_with(&self, localize: &mut FluentMessageLookup<'_>) -> String {
localize(
static_domain("callback-domain"),
static_entry("callback-id"),
None,
)
}
}
#[test]
fn fluent_message_reference_impl_delegates_to_inner_message() {
let message = NestedMessage;
let message_ref = &message;
let mut localize = |domain: StaticFluentDomain,
id: StaticFluentEntryId,
_args: Option<&FluentArgs<'_>>| {
format!("{}:{}", domain.as_str(), id.as_str())
};
assert_eq!(
FluentMessage::to_fluent_string_with(&message_ref, &mut localize),
"nested-domain:nested-id"
);
}
#[test]
fn manual_fluent_message_uses_supplied_callback_for_lookup() {
let mut called = false;
let mut localize =
|domain: StaticFluentDomain, id: StaticFluentEntryId, args: Option<&FluentArgs<'_>>| {
called = true;
assert_eq!(domain.as_str(), "callback-domain");
assert_eq!(id.as_str(), "callback-id");
assert!(args.is_none());
"callback result".to_string()
};
assert_eq!(
CallbackOnlyMessage.to_fluent_string_with(&mut localize),
"callback result"
);
assert!(called);
}
#[test]
fn fluent_localizer_reference_and_arc_impls_delegate_to_inner_localizer() {
let localizer = StaticLocalizer { value: "Hello" };
let localizer_ref = &localizer;
let localizer_arc = Arc::new(StaticLocalizer { value: "Bonjour" });
assert_eq!(localizer_ref.localize_message(&NestedMessage), "Hello");
assert_eq!(localizer_arc.localize_message(&NestedMessage), "Bonjour");
assert_eq!(
FluentLocalizer::localize(&localizer_ref, static_entry("nested-id"), None),
Some("Hello".to_string())
);
assert_eq!(
FluentLocalizer::localize_in_domain(
&localizer_ref,
static_domain("nested-domain"),
static_entry("nested-id"),
None,
),
Some("Hello".to_string())
);
assert_eq!(
FluentLocalizer::localize_in_domain(
&localizer_arc,
static_domain("nested-domain"),
static_entry("nested-id"),
None,
),
Some("Bonjour".to_string())
);
}
#[test]
fn localizer_extension_localizes_typed_messages_with_id_fallback() {
let localizer = StaticLocalizer { value: "Hello" };
assert_eq!(
FluentLocalizer::localize(&localizer, static_entry("nested-id"), None),
Some("Hello".to_string())
);
assert_eq!(
FluentLocalizer::localize_in_domain(
&localizer,
static_domain("nested-domain"),
static_entry("nested-id"),
None,
),
Some("Hello".to_string())
);
assert_eq!(localizer.localize_message(&MissingMessage), "missing-id");
}
#[test]
fn localizer_extension_can_return_missing_typed_messages_without_id_fallback() {
let localizer = StaticLocalizer { value: "Hello" };
assert_eq!(
localizer.try_localize_message(&NestedMessage),
Some("Hello".to_string())
);
assert_eq!(localizer.try_localize_message(&MissingMessage), None);
}
struct MinimalScopedLocalizer;
impl MinimalScopedLocalizer {
fn lookup(
&self,
domain: StaticFluentDomain,
id: StaticFluentEntryId,
_args: Option<&FluentArgs<'_>>,
) -> Option<String> {
Some(format!("{domain}:{id}"))
}
}
impl FluentLocalizer for MinimalScopedLocalizer {
fn localize<'a>(
&self,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
self.localize_in_domain(
StaticFluentDomain::from_package_name(env!("CARGO_PKG_NAME")),
id,
args,
)
}
fn localize_in_domain<'a>(
&self,
domain: StaticFluentDomain,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
self.lookup(domain, id, args)
}
fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
let mut lookup = |domain: StaticFluentDomain,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'_>>| {
self.localize_in_domain(domain, id, args)
};
f(&mut lookup);
}
}
struct ScopedMessage;
impl FluentMessage for ScopedMessage {
fn to_fluent_string_with(&self, localize: &mut FluentMessageLookup<'_>) -> String {
localize(
static_domain("custom-domain"),
static_entry("scoped-message"),
None,
)
}
}
#[test]
fn custom_localizer_with_lookup_invokes_callback_and_renders_typed_message() {
assert_eq!(
MinimalScopedLocalizer.localize_message(&ScopedMessage),
"custom-domain:scoped-message"
);
}
struct SkippingCallbackLocalizer;
impl FluentLocalizer for SkippingCallbackLocalizer {
fn localize<'a>(
&self,
_id: StaticFluentEntryId,
_args: Option<&FluentArgs<'a>>,
) -> Option<String> {
None
}
fn localize_in_domain<'a>(
&self,
_domain: StaticFluentDomain,
_id: StaticFluentEntryId,
_args: Option<&FluentArgs<'a>>,
) -> Option<String> {
None
}
fn with_lookup(&self, _f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {}
}
struct DoubleCallbackLocalizer;
impl FluentLocalizer for DoubleCallbackLocalizer {
fn localize<'a>(
&self,
id: StaticFluentEntryId,
_args: Option<&FluentArgs<'a>>,
) -> Option<String> {
Some(id.as_str().to_string())
}
fn localize_in_domain<'a>(
&self,
_domain: StaticFluentDomain,
id: StaticFluentEntryId,
args: Option<&FluentArgs<'a>>,
) -> Option<String> {
self.localize(id, args)
}
fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
let mut lookup =
|_domain: StaticFluentDomain,
id: StaticFluentEntryId,
_args: Option<&FluentArgs<'_>>| { Some(id.as_str().to_string()) };
f(&mut lookup);
f(&mut lookup);
}
}
#[test]
#[should_panic(expected = "FluentLocalizer::with_lookup must invoke its callback exactly once")]
fn localize_message_panics_when_with_lookup_skips_callback() {
SkippingCallbackLocalizer.localize_message(&NestedMessage);
}
#[test]
#[should_panic(expected = "FluentLocalizer::with_lookup must invoke its callback exactly once")]
fn try_localize_message_panics_when_with_lookup_invokes_callback_twice() {
let _ = DoubleCallbackLocalizer.try_localize_message(&NestedMessage);
}
struct BlockingSwitchLocalizer {
selected: RwLock<&'static str>,
child_seen: Mutex<mpsc::Sender<()>>,
continue_child: Mutex<mpsc::Receiver<()>>,
}
impl BlockingSwitchLocalizer {
fn new(child_seen: mpsc::Sender<()>, continue_child: mpsc::Receiver<()>) -> Self {
Self {
selected: RwLock::new("en"),
child_seen: Mutex::new(child_seen),
continue_child: Mutex::new(continue_child),
}
}
fn select(&self, language: &'static str) {
*self
.selected
.write()
.expect("test language lock should not be poisoned") = language;
}
fn selected(&self) -> &'static str {
*self
.selected
.read()
.expect("test language lock should not be poisoned")
}
fn render_lookup(&self, language: &'static str, domain: &str, id: &str) -> Option<String> {
if domain != "switch-domain" {
return None;
}
if id == "child" {
self.child_seen
.lock()
.expect("test child sender lock should not be poisoned")
.send(())
.expect("test should receive child lookup notification");
self.continue_child
.lock()
.expect("test child receiver lock should not be poisoned")
.recv()
.expect("test should release child lookup");
}
matches!(id, "child" | "parent").then(|| format!("{language}-{id}"))
}
}
impl FluentLocalizer for BlockingSwitchLocalizer {
fn localize<'a>(
&self,
id: StaticFluentEntryId,
_args: Option<&FluentArgs<'a>>,
) -> Option<String> {
let language = self.selected();
self.render_lookup(language, "switch-domain", id.as_str())
}
fn localize_in_domain<'a>(
&self,
domain: StaticFluentDomain,
id: StaticFluentEntryId,
_args: Option<&FluentArgs<'a>>,
) -> Option<String> {
let language = self.selected();
self.render_lookup(language, domain.as_str(), id.as_str())
}
fn with_lookup(&self, f: &mut dyn FnMut(&mut FluentLocalizerLookup<'_>)) {
let selected = self
.selected
.read()
.expect("test language lock should not be poisoned");
let language = *selected;
let mut lookup = |domain: StaticFluentDomain,
id: StaticFluentEntryId,
_args: Option<&FluentArgs<'_>>| {
self.render_lookup(language, domain.as_str(), id.as_str())
};
f(&mut lookup);
}
}
struct BlockingParent;
impl FluentMessage for BlockingParent {
fn to_fluent_string_with(&self, localize: &mut FluentMessageLookup<'_>) -> String {
let child = localize(static_domain("switch-domain"), static_entry("child"), None);
let parent = localize(static_domain("switch-domain"), static_entry("parent"), None);
format!("{parent}:{child}")
}
}
#[test]
fn localize_message_keeps_one_lookup_scope_during_concurrent_language_switch() {
let (child_seen_tx, child_seen_rx) = mpsc::channel();
let (continue_child_tx, continue_child_rx) = mpsc::channel();
let localizer = Arc::new(BlockingSwitchLocalizer::new(
child_seen_tx,
continue_child_rx,
));
let render_localizer = Arc::clone(&localizer);
let render = std::thread::spawn(move || render_localizer.localize_message(&BlockingParent));
child_seen_rx
.recv_timeout(Duration::from_secs(1))
.expect("render should reach the child lookup");
let (switch_started_tx, switch_started_rx) = mpsc::channel();
let (switch_done_tx, switch_done_rx) = mpsc::channel();
let switch_localizer = Arc::clone(&localizer);
let switch = std::thread::spawn(move || {
switch_started_tx
.send(())
.expect("test should observe language switch start");
switch_localizer.select("fr");
switch_done_tx
.send(())
.expect("test should observe language switch completion");
});
switch_started_rx
.recv_timeout(Duration::from_secs(1))
.expect("language switch thread should start");
assert!(
switch_done_rx
.recv_timeout(Duration::from_millis(50))
.is_err(),
"language switch completed while typed message render was still in progress"
);
continue_child_tx
.send(())
.expect("test should release the child lookup");
let rendered = render
.join()
.expect("render thread should complete without panicking");
switch_done_rx
.recv_timeout(Duration::from_secs(1))
.expect("language switch should complete after render");
switch
.join()
.expect("language switch thread should complete without panicking");
assert_eq!(rendered, "en-parent:en-child");
assert_eq!(localizer.selected(), "fr");
}
}