1#![doc = include_str!("../README.md")]
2
3#[cfg(feature = "derive")]
4pub use es_fluent_derive::{EsFluent, EsFluentChoice, EsFluentKv, EsFluentThis};
5
6#[doc(hidden)]
7pub use es_fluent_manager_core::{FluentManager, I18nModule, LocalizationError, Localizer};
8
9#[doc(hidden)]
10pub use fluent_bundle::FluentValue;
11
12#[doc(hidden)]
13pub use inventory as __inventory;
14
15#[doc(hidden)]
16pub use rust_embed as __rust_embed;
17
18#[doc(hidden)]
19pub use es_fluent_manager_core as __manager_core;
20
21#[doc(hidden)]
22pub use es_fluent_core as __core;
23
24#[doc(hidden)]
25pub use unic_langid;
26
27mod traits;
28pub use traits::{EsFluentChoice, FluentDisplay, ThisFtl, ToFluentString};
29
30use std::sync::{Arc, OnceLock, RwLock};
31
32#[doc(hidden)]
33static CONTEXT: OnceLock<Arc<RwLock<FluentManager>>> = OnceLock::new();
34
35#[doc(hidden)]
36static CUSTOM_LOCALIZER: OnceLock<
37 Box<
38 dyn Fn(&str, Option<&std::collections::HashMap<&str, FluentValue>>) -> Option<String>
39 + Send
40 + Sync,
41 >,
42> = OnceLock::new();
43
44#[doc(hidden)]
53pub fn set_context(manager: FluentManager) {
54 CONTEXT
55 .set(Arc::new(RwLock::new(manager)))
56 .map_err(|_| "Context already set")
57 .expect("Failed to set context");
58}
59
60#[doc(hidden)]
69pub fn set_shared_context(manager: Arc<RwLock<FluentManager>>) {
70 CONTEXT
71 .set(manager)
72 .map_err(|_| "Context already set")
73 .expect("Failed to set shared context");
74}
75
76#[doc(hidden)]
86pub fn set_custom_localizer<F>(localizer: F)
87where
88 F: Fn(&str, Option<&std::collections::HashMap<&str, FluentValue>>) -> Option<String>
89 + Send
90 + Sync
91 + 'static,
92{
93 CUSTOM_LOCALIZER
94 .set(Box::new(localizer))
95 .map_err(|_| "Custom localizer already set")
96 .expect("Failed to set custom localizer");
97}
98
99#[doc(hidden)]
101pub fn update_context<F>(f: F)
102where
103 F: FnOnce(&mut FluentManager),
104{
105 if let Some(context_arc) = CONTEXT.get() {
106 let mut context = context_arc
107 .write()
108 .expect("Failed to acquire write lock on context");
109 f(&mut context);
110 }
111}
112
113#[doc(hidden)]
122pub fn localize<'a>(
123 id: &str,
124 args: Option<&std::collections::HashMap<&str, FluentValue<'a>>>,
125) -> String {
126 if let Some(custom_localizer) = CUSTOM_LOCALIZER.get()
127 && let Some(message) = custom_localizer(id, args)
128 {
129 return message;
130 }
131
132 if let Some(context_arc) = CONTEXT.get() {
133 let context = context_arc
134 .read()
135 .expect("Failed to acquire read lock on context");
136
137 if let Some(message) = context.localize(id, args) {
138 return message;
139 }
140 }
141
142 log::warn!("Translation for '{}' not found or context not set.", id);
143 id.to_string()
144}
145
146#[cfg(feature = "generate")]
148mod generate {
149 use std::path::PathBuf;
150
151 pub use es_fluent_generate::FluentParseMode;
152 pub use es_fluent_generate::error::FluentGenerateError;
153
154 #[derive(Debug)]
156 pub enum GeneratorError {
157 Config(es_fluent_toml::I18nConfigError),
159 CrateName(String),
161 Generate(FluentGenerateError),
163 }
164
165 impl std::fmt::Display for GeneratorError {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 match self {
168 Self::Config(e) => write!(f, "Configuration error: {}", e),
169 Self::CrateName(e) => write!(f, "Failed to detect crate name: {}", e),
170 Self::Generate(e) => write!(f, "Generation error: {}", e),
171 }
172 }
173 }
174
175 impl std::error::Error for GeneratorError {}
176
177 impl From<es_fluent_toml::I18nConfigError> for GeneratorError {
178 fn from(e: es_fluent_toml::I18nConfigError) -> Self {
179 Self::Config(e)
180 }
181 }
182
183 impl From<FluentGenerateError> for GeneratorError {
184 fn from(e: FluentGenerateError) -> Self {
185 Self::Generate(e)
186 }
187 }
188
189 #[derive(bon::Builder)]
216 pub struct EsFluentGenerator {
217 #[builder(default)]
220 mode: FluentParseMode,
221
222 #[builder(into)]
224 crate_name: Option<String>,
225
226 #[builder(into)]
228 output_path: Option<PathBuf>,
229 }
230
231 impl EsFluentGenerator {
232 pub fn generate(&self) -> Result<(), GeneratorError> {
234 let crate_name = match &self.crate_name {
235 Some(name) => name.clone(),
236 None => Self::detect_crate_name()?,
237 };
238
239 let output_path = match &self.output_path {
240 Some(path) => path.clone(),
241 None => {
242 let config = es_fluent_toml::I18nConfig::read_from_manifest_dir()?;
243 config.assets_dir.join(&config.fallback_language)
244 },
245 };
246
247 let crate_ident = crate_name.replace('-', "_");
248 let type_infos = crate::__core::registry::get_all_ftl_type_infos()
249 .into_iter()
250 .filter(|info| {
251 info.module_path == crate_ident
252 || info.module_path.starts_with(&format!("{}::", crate_ident))
253 })
254 .collect::<Vec<_>>();
255
256 log::info!(
257 "Generating FTL files for {} types in crate '{}'",
258 type_infos.len(),
259 crate_name
260 );
261
262 es_fluent_generate::generate(&crate_name, output_path, type_infos, self.mode.clone())?;
263
264 Ok(())
265 }
266
267 fn detect_crate_name() -> Result<String, GeneratorError> {
269 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
270 .map_err(|_| GeneratorError::CrateName("CARGO_MANIFEST_DIR not set".to_string()))?;
271 let manifest_path = std::path::PathBuf::from(&manifest_dir).join("Cargo.toml");
272
273 cargo_metadata::MetadataCommand::new()
274 .exec()
275 .ok()
276 .and_then(|metadata| {
277 metadata
278 .packages
279 .iter()
280 .find(|pkg| pkg.manifest_path == manifest_path)
281 .map(|pkg| pkg.name.to_string())
282 })
283 .or_else(|| std::env::var("CARGO_PKG_NAME").ok())
284 .ok_or_else(|| {
285 GeneratorError::CrateName("Could not determine crate name".to_string())
286 })
287 }
288 }
289}
290
291#[cfg(feature = "generate")]
292pub use generate::{EsFluentGenerator, FluentParseMode, GeneratorError};
293
294#[cfg(feature = "generate")]
295pub use es_fluent_toml;