lib.rs (19127B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 use fluent::FluentValue; 6 use fluent_fallback::{ 7 types::{ 8 L10nAttribute as FluentL10nAttribute, L10nKey as FluentL10nKey, 9 L10nMessage as FluentL10nMessage, ResourceId, 10 }, 11 Localization, 12 }; 13 use fluent_ffi::{convert_args, FluentArgs, FluentArgument, L10nArg}; 14 use l10nregistry_ffi::{ 15 env::GeckoEnvironment, 16 registry::{get_l10n_registry, GeckoL10nRegistry, GeckoResourceId}, 17 }; 18 use nsstring::{nsACString, nsCString}; 19 use std::os::raw::c_void; 20 use std::{borrow::Cow, cell::RefCell}; 21 use thin_vec::ThinVec; 22 use unic_langid::LanguageIdentifier; 23 use xpcom::{ 24 interfaces::{nsIRunnablePriority}, 25 RefCounted, RefPtr, Refcnt, 26 }; 27 28 #[derive(Debug)] 29 #[repr(C)] 30 pub struct L10nKey<'s> { 31 id: &'s nsACString, 32 args: ThinVec<L10nArg<'s>>, 33 } 34 35 impl<'s> From<&'s L10nKey<'s>> for FluentL10nKey<'static> { 36 fn from(input: &'s L10nKey<'s>) -> Self { 37 FluentL10nKey { 38 id: input.id.to_utf8().to_string().into(), 39 args: convert_args_to_owned(&input.args), 40 } 41 } 42 } 43 44 // This is a variant of `convert_args` from `fluent-ffi` with a 'static constrain 45 // put on the resulting `FluentArgs` to make it acceptable into `spqwn_current_thread`. 46 pub fn convert_args_to_owned(args: &[L10nArg]) -> Option<FluentArgs<'static>> { 47 if args.is_empty() { 48 return None; 49 } 50 51 let mut result = FluentArgs::with_capacity(args.len()); 52 for arg in args { 53 let val = match arg.value { 54 FluentArgument::Double_(d) => FluentValue::from(d), 55 // We need this to be owned because we pass the result into `spawn_local`. 56 FluentArgument::String(s) => FluentValue::from(Cow::Owned(s.to_utf8().to_string())), 57 }; 58 result.set(arg.id.to_string(), val); 59 } 60 Some(result) 61 } 62 63 #[derive(Debug)] 64 #[repr(C)] 65 pub struct L10nAttribute { 66 name: nsCString, 67 value: nsCString, 68 } 69 70 impl From<FluentL10nAttribute<'_>> for L10nAttribute { 71 fn from(attr: FluentL10nAttribute<'_>) -> Self { 72 Self { 73 name: nsCString::from(&*attr.name), 74 value: nsCString::from(&*attr.value), 75 } 76 } 77 } 78 79 #[derive(Debug)] 80 #[repr(C)] 81 pub struct L10nMessage { 82 value: nsCString, 83 attributes: ThinVec<L10nAttribute>, 84 } 85 86 impl std::default::Default for L10nMessage { 87 fn default() -> Self { 88 Self { 89 value: nsCString::new(), 90 attributes: ThinVec::new(), 91 } 92 } 93 } 94 95 #[derive(Debug)] 96 #[repr(C)] 97 pub struct OptionalL10nMessage { 98 is_present: bool, 99 message: L10nMessage, 100 } 101 102 impl From<FluentL10nMessage<'_>> for L10nMessage { 103 fn from(input: FluentL10nMessage) -> Self { 104 let value = if let Some(value) = input.value { 105 value.to_string().into() 106 } else { 107 let mut s = nsCString::new(); 108 s.set_is_void(true); 109 s 110 }; 111 Self { 112 value, 113 attributes: input.attributes.into_iter().map(Into::into).collect(), 114 } 115 } 116 } 117 118 pub struct LocalizationRc { 119 inner: RefCell<Localization<GeckoL10nRegistry, GeckoEnvironment>>, 120 refcnt: Refcnt, 121 } 122 123 // xpcom::RefPtr support 124 unsafe impl RefCounted for LocalizationRc { 125 unsafe fn addref(&self) { 126 localization_addref(self); 127 } 128 unsafe fn release(&self) { 129 localization_release(self); 130 } 131 } 132 133 impl LocalizationRc { 134 pub fn new( 135 res_ids: Vec<ResourceId>, 136 is_sync: bool, 137 registry: Option<&GeckoL10nRegistry>, 138 locales: Option<Vec<LanguageIdentifier>>, 139 ) -> RefPtr<Self> { 140 let env = GeckoEnvironment::new(locales); 141 let inner = if let Some(reg) = registry { 142 Localization::with_env(res_ids, is_sync, env, reg.clone()) 143 } else { 144 let reg = (*get_l10n_registry()).clone(); 145 Localization::with_env(res_ids, is_sync, env, reg) 146 }; 147 148 let loc = Box::new(LocalizationRc { 149 inner: RefCell::new(inner), 150 refcnt: unsafe { Refcnt::new() }, 151 }); 152 153 unsafe { 154 RefPtr::from_raw(Box::into_raw(loc)) 155 .expect("Failed to create RefPtr<LocalizationRc> from Box<LocalizationRc>") 156 } 157 } 158 159 pub fn add_resource_id(&self, res_id: ResourceId) { 160 self.inner.borrow_mut().add_resource_id(res_id); 161 } 162 163 pub fn add_resource_ids(&self, res_ids: Vec<ResourceId>) { 164 self.inner.borrow_mut().add_resource_ids(res_ids); 165 } 166 167 pub fn remove_resource_id(&self, res_id: ResourceId) -> usize { 168 self.inner.borrow_mut().remove_resource_id(res_id) 169 } 170 171 pub fn remove_resource_ids(&self, res_ids: Vec<ResourceId>) -> usize { 172 self.inner.borrow_mut().remove_resource_ids(res_ids) 173 } 174 175 pub fn set_async(&self) { 176 if self.is_sync() { 177 self.inner.borrow_mut().set_async(); 178 } 179 } 180 181 pub fn is_sync(&self) -> bool { 182 self.inner.borrow().is_sync() 183 } 184 185 pub fn on_change(&self) { 186 self.inner.borrow_mut().on_change(); 187 } 188 189 pub fn format_value_sync( 190 &self, 191 id: &nsACString, 192 args: &ThinVec<L10nArg>, 193 ret_val: &mut nsACString, 194 ret_err: &mut ThinVec<nsCString>, 195 ) -> bool { 196 let mut errors = vec![]; 197 let args = convert_args(&args); 198 if let Ok(value) = self.inner.borrow().bundles().format_value_sync( 199 &id.to_utf8(), 200 args.as_ref(), 201 &mut errors, 202 ) { 203 if let Some(value) = value { 204 ret_val.assign(&value); 205 } else { 206 ret_val.set_is_void(true); 207 } 208 #[cfg(debug_assertions)] 209 debug_assert_variables_exist(&errors, &[id], |id| id.to_string()); 210 ret_err.extend(errors.into_iter().map(|err| err.to_string().into())); 211 true 212 } else { 213 false 214 } 215 } 216 217 pub fn format_values_sync( 218 &self, 219 keys: &ThinVec<L10nKey>, 220 ret_val: &mut ThinVec<nsCString>, 221 ret_err: &mut ThinVec<nsCString>, 222 ) -> bool { 223 ret_val.reserve(keys.len()); 224 let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect(); 225 let mut errors = vec![]; 226 if let Ok(values) = self 227 .inner 228 .borrow() 229 .bundles() 230 .format_values_sync(&keys, &mut errors) 231 { 232 for value in values.iter() { 233 if let Some(value) = value { 234 ret_val.push(value.as_ref().into()); 235 } else { 236 let mut void_string = nsCString::new(); 237 void_string.set_is_void(true); 238 ret_val.push(void_string); 239 } 240 } 241 #[cfg(debug_assertions)] 242 debug_assert_variables_exist(&errors, &keys, |key| key.id.to_string()); 243 ret_err.extend(errors.into_iter().map(|err| err.to_string().into())); 244 true 245 } else { 246 false 247 } 248 } 249 250 pub fn format_messages_sync( 251 &self, 252 keys: &ThinVec<L10nKey>, 253 ret_val: &mut ThinVec<OptionalL10nMessage>, 254 ret_err: &mut ThinVec<nsCString>, 255 ) -> bool { 256 ret_val.reserve(keys.len()); 257 let mut errors = vec![]; 258 let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect(); 259 if let Ok(messages) = self 260 .inner 261 .borrow() 262 .bundles() 263 .format_messages_sync(&keys, &mut errors) 264 { 265 for msg in messages { 266 ret_val.push(if let Some(msg) = msg { 267 OptionalL10nMessage { 268 is_present: true, 269 message: msg.into(), 270 } 271 } else { 272 OptionalL10nMessage { 273 is_present: false, 274 message: L10nMessage::default(), 275 } 276 }); 277 } 278 assert_eq!(keys.len(), ret_val.len()); 279 #[cfg(debug_assertions)] 280 debug_assert_variables_exist(&errors, &keys, |key| key.id.to_string()); 281 ret_err.extend(errors.into_iter().map(|err| err.to_string().into())); 282 true 283 } else { 284 false 285 } 286 } 287 288 pub fn format_value( 289 &self, 290 id: &nsACString, 291 args: &ThinVec<L10nArg>, 292 promise: &xpcom::Promise, 293 callback: extern "C" fn(&xpcom::Promise, &nsACString, &ThinVec<nsCString>), 294 ) { 295 let bundles = self.inner.borrow().bundles().clone(); 296 297 let args = convert_args_to_owned(&args); 298 299 let id = nsCString::from(id); 300 let strong_promise = RefPtr::new(promise); 301 302 moz_task::TaskBuilder::new("LocalizationRc::format_value", async move { 303 let mut errors = vec![]; 304 let value = if let Some(value) = bundles 305 .format_value(&id.to_utf8(), args.as_ref(), &mut errors) 306 .await 307 { 308 let v: nsCString = value.to_string().into(); 309 v 310 } else { 311 let mut v = nsCString::new(); 312 v.set_is_void(true); 313 v 314 }; 315 #[cfg(debug_assertions)] 316 debug_assert_variables_exist(&errors, &[id], |id| id.to_string()); 317 let errors = errors 318 .into_iter() 319 .map(|err| err.to_string().into()) 320 .collect(); 321 callback(&strong_promise, &value, &errors); 322 }) 323 .priority(nsIRunnablePriority::PRIORITY_RENDER_BLOCKING as u32) 324 .spawn_local() 325 .detach(); 326 } 327 328 pub fn format_values( 329 &self, 330 keys: &ThinVec<L10nKey>, 331 promise: &xpcom::Promise, 332 callback: extern "C" fn(&xpcom::Promise, &ThinVec<nsCString>, &ThinVec<nsCString>), 333 ) { 334 let bundles = self.inner.borrow().bundles().clone(); 335 336 let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect(); 337 338 let strong_promise = RefPtr::new(promise); 339 340 moz_task::TaskBuilder::new("LocalizationRc::format_values", async move { 341 let mut errors = vec![]; 342 let ret_val = bundles 343 .format_values(&keys, &mut errors) 344 .await 345 .into_iter() 346 .map(|value| { 347 if let Some(value) = value { 348 nsCString::from(value.as_ref()) 349 } else { 350 let mut v = nsCString::new(); 351 v.set_is_void(true); 352 v 353 } 354 }) 355 .collect::<ThinVec<_>>(); 356 357 assert_eq!(keys.len(), ret_val.len()); 358 359 #[cfg(debug_assertions)] 360 debug_assert_variables_exist(&errors, &keys, |key| key.id.to_string()); 361 let errors = errors 362 .into_iter() 363 .map(|err| err.to_string().into()) 364 .collect(); 365 366 callback(&strong_promise, &ret_val, &errors); 367 }) 368 .priority(nsIRunnablePriority::PRIORITY_RENDER_BLOCKING as u32) 369 .spawn_local() 370 .detach(); 371 } 372 373 pub fn format_messages( 374 &self, 375 keys: &ThinVec<L10nKey>, 376 promise: &xpcom::Promise, 377 callback: extern "C" fn( 378 &xpcom::Promise, 379 &ThinVec<OptionalL10nMessage>, 380 &ThinVec<nsCString>, 381 ), 382 ) { 383 let bundles = self.inner.borrow().bundles().clone(); 384 385 let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect(); 386 387 let strong_promise = RefPtr::new(promise); 388 389 moz_task::TaskBuilder::new("LocalizationRc::format_messages", async move { 390 let mut errors = vec![]; 391 let ret_val = bundles 392 .format_messages(&keys, &mut errors) 393 .await 394 .into_iter() 395 .map(|msg| { 396 if let Some(msg) = msg { 397 OptionalL10nMessage { 398 is_present: true, 399 message: msg.into(), 400 } 401 } else { 402 OptionalL10nMessage { 403 is_present: false, 404 message: L10nMessage::default(), 405 } 406 } 407 }) 408 .collect::<ThinVec<_>>(); 409 410 assert_eq!(keys.len(), ret_val.len()); 411 412 #[cfg(debug_assertions)] 413 debug_assert_variables_exist(&errors, &keys, |key| key.id.to_string()); 414 415 let errors = errors 416 .into_iter() 417 .map(|err| err.to_string().into()) 418 .collect(); 419 420 callback(&strong_promise, &ret_val, &errors); 421 }) 422 .priority(nsIRunnablePriority::PRIORITY_RENDER_BLOCKING as u32) 423 .spawn_local() 424 .detach(); 425 } 426 } 427 428 #[no_mangle] 429 pub extern "C" fn localization_parse_locale(input: &nsCString) -> *const c_void { 430 let l: LanguageIdentifier = input.to_utf8().parse().unwrap(); 431 Box::into_raw(Box::new(l)) as *const c_void 432 } 433 434 #[no_mangle] 435 pub extern "C" fn localization_new( 436 res_ids: &ThinVec<GeckoResourceId>, 437 is_sync: bool, 438 reg: Option<&GeckoL10nRegistry>, 439 result: &mut *const LocalizationRc, 440 ) { 441 *result = std::ptr::null(); 442 let res_ids: Vec<ResourceId> = res_ids.iter().map(ResourceId::from).collect(); 443 *result = RefPtr::forget_into_raw(LocalizationRc::new(res_ids, is_sync, reg, None)); 444 } 445 446 #[no_mangle] 447 pub extern "C" fn localization_new_with_locales( 448 res_ids: &ThinVec<GeckoResourceId>, 449 is_sync: bool, 450 reg: Option<&GeckoL10nRegistry>, 451 locales: Option<&ThinVec<nsCString>>, 452 result: &mut *const LocalizationRc, 453 ) -> bool { 454 *result = std::ptr::null(); 455 let res_ids: Vec<ResourceId> = res_ids.iter().map(ResourceId::from).collect(); 456 let locales: Result<Option<Vec<LanguageIdentifier>>, _> = locales 457 .map(|locales| { 458 locales 459 .iter() 460 .map(|s| LanguageIdentifier::from_bytes(&s)) 461 .collect() 462 }) 463 .transpose(); 464 465 if let Ok(locales) = locales { 466 *result = RefPtr::forget_into_raw(LocalizationRc::new(res_ids, is_sync, reg, locales)); 467 true 468 } else { 469 false 470 } 471 } 472 473 #[no_mangle] 474 pub unsafe extern "C" fn localization_addref(loc: &LocalizationRc) { 475 loc.refcnt.inc(); 476 } 477 478 #[no_mangle] 479 pub unsafe extern "C" fn localization_release(loc: *const LocalizationRc) { 480 let rc = (*loc).refcnt.dec(); 481 if rc == 0 { 482 std::mem::drop(Box::from_raw(loc as *const _ as *mut LocalizationRc)); 483 } 484 } 485 486 #[no_mangle] 487 pub extern "C" fn localization_add_res_id(loc: &LocalizationRc, res_id: &GeckoResourceId) { 488 loc.add_resource_id(res_id.into()); 489 } 490 491 #[no_mangle] 492 pub extern "C" fn localization_add_res_ids(loc: &LocalizationRc, res_ids: &ThinVec<GeckoResourceId>) { 493 let res_ids = res_ids.iter().map(ResourceId::from).collect(); 494 loc.add_resource_ids(res_ids); 495 } 496 497 #[no_mangle] 498 pub extern "C" fn localization_remove_res_id(loc: &LocalizationRc, res_id: &GeckoResourceId) -> usize { 499 loc.remove_resource_id(res_id.into()) 500 } 501 502 #[no_mangle] 503 pub extern "C" fn localization_remove_res_ids( 504 loc: &LocalizationRc, 505 res_ids: &ThinVec<GeckoResourceId>, 506 ) -> usize { 507 let res_ids = res_ids.iter().map(ResourceId::from).collect(); 508 loc.remove_resource_ids(res_ids) 509 } 510 511 #[no_mangle] 512 pub extern "C" fn localization_format_value_sync( 513 loc: &LocalizationRc, 514 id: &nsACString, 515 args: &ThinVec<L10nArg>, 516 ret_val: &mut nsACString, 517 ret_err: &mut ThinVec<nsCString>, 518 ) -> bool { 519 loc.format_value_sync(id, args, ret_val, ret_err) 520 } 521 522 #[no_mangle] 523 pub extern "C" fn localization_format_values_sync( 524 loc: &LocalizationRc, 525 keys: &ThinVec<L10nKey>, 526 ret_val: &mut ThinVec<nsCString>, 527 ret_err: &mut ThinVec<nsCString>, 528 ) -> bool { 529 loc.format_values_sync(keys, ret_val, ret_err) 530 } 531 532 #[no_mangle] 533 pub extern "C" fn localization_format_messages_sync( 534 loc: &LocalizationRc, 535 keys: &ThinVec<L10nKey>, 536 ret_val: &mut ThinVec<OptionalL10nMessage>, 537 ret_err: &mut ThinVec<nsCString>, 538 ) -> bool { 539 loc.format_messages_sync(keys, ret_val, ret_err) 540 } 541 542 #[no_mangle] 543 pub extern "C" fn localization_format_value( 544 loc: &LocalizationRc, 545 id: &nsACString, 546 args: &ThinVec<L10nArg>, 547 promise: &xpcom::Promise, 548 callback: extern "C" fn(&xpcom::Promise, &nsACString, &ThinVec<nsCString>), 549 ) { 550 loc.format_value(id, args, promise, callback); 551 } 552 553 #[no_mangle] 554 pub extern "C" fn localization_format_values( 555 loc: &LocalizationRc, 556 keys: &ThinVec<L10nKey>, 557 promise: &xpcom::Promise, 558 callback: extern "C" fn(&xpcom::Promise, &ThinVec<nsCString>, &ThinVec<nsCString>), 559 ) { 560 loc.format_values(keys, promise, callback); 561 } 562 563 #[no_mangle] 564 pub extern "C" fn localization_format_messages( 565 loc: &LocalizationRc, 566 keys: &ThinVec<L10nKey>, 567 promise: &xpcom::Promise, 568 callback: extern "C" fn(&xpcom::Promise, &ThinVec<OptionalL10nMessage>, &ThinVec<nsCString>), 569 ) { 570 loc.format_messages(keys, promise, callback); 571 } 572 573 #[no_mangle] 574 pub extern "C" fn localization_set_async(loc: &LocalizationRc) { 575 loc.set_async(); 576 } 577 578 #[no_mangle] 579 pub extern "C" fn localization_is_sync(loc: &LocalizationRc) -> bool { 580 loc.is_sync() 581 } 582 583 #[no_mangle] 584 pub extern "C" fn localization_on_change(loc: &LocalizationRc) { 585 loc.on_change(); 586 } 587 588 #[cfg(debug_assertions)] 589 fn debug_assert_variables_exist<K, F>( 590 errors: &[fluent_fallback::LocalizationError], 591 keys: &[K], 592 to_string: F, 593 ) where 594 F: Fn(&K) -> String, 595 { 596 for error in errors { 597 if let fluent_fallback::LocalizationError::Resolver { errors, .. } = error { 598 use fluent::{ 599 resolver::{errors::ReferenceKind, ResolverError}, 600 FluentError, 601 }; 602 for error in errors { 603 if let FluentError::ResolverError(ResolverError::Reference( 604 ReferenceKind::Variable { id }, 605 )) = error 606 { 607 // This error needs to be actionable for Firefox engineers to fix 608 // their Fluent issues. It might be nicer to share the specific 609 // message, but at this point we don't have that information. 610 eprintln!( 611 "Fluent error, the argument \"${}\" was not provided a value.", 612 id 613 ); 614 eprintln!("This error happened while formatting the following messages:"); 615 for key in keys { 616 eprintln!(" {:?}", to_string(key)) 617 } 618 619 // Panic with the slightly more cryptic ResolverError. 620 panic!("{}", error.to_string()); 621 } 622 } 623 } 624 } 625 }