es_fluent_manager_macros/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use heck::ToPascalCase as _;
4use proc_macro::TokenStream;
5use quote::quote;
6use std::fs;
7
8/// Defines an embedded i18n module.
9///
10/// This macro will:
11///
12/// 1.  Read the `i18n.toml` configuration file.
13/// 2.  Discover the available languages in the `i18n` directory.
14/// 3.  Generate a `RustEmbed` struct for the i18n assets.
15/// 4.  Generate an `EmbeddedI18nModule` for the crate.
16#[proc_macro]
17pub fn define_embedded_i18n_module(_input: TokenStream) -> TokenStream {
18    let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME must be set");
19    let assets_struct_name = syn::Ident::new(
20        &format!(
21            "{}I18nAssets",
22            &crate_name.replace('-', "_").to_pascal_case()
23        ),
24        proc_macro2::Span::call_site(),
25    );
26
27    let module_data_name = syn::Ident::new(
28        &format!(
29            "{}_I18N_MODULE_DATA",
30            &crate_name.to_uppercase().replace('-', "_")
31        ),
32        proc_macro2::Span::call_site(),
33    );
34
35    let config = match es_fluent_toml::I18nConfig::read_from_manifest_dir() {
36        Ok(config) => config,
37        Err(es_fluent_toml::I18nConfigError::NotFound) => {
38            panic!(
39                "No i18n.toml configuration file found in project root. Please create one with the required settings."
40            );
41        },
42        Err(e) => {
43            panic!("Failed to read i18n.toml configuration: {}", e);
44        },
45    };
46
47    let i18n_root_path = match config.assets_dir_from_manifest() {
48        Ok(path) => path,
49        Err(e) => {
50            panic!(
51                "Failed to resolve assets directory from configuration: {}",
52                e
53            );
54        },
55    };
56
57    if let Err(e) = config.validate_assets_dir() {
58        panic!("Assets directory validation failed: {}", e);
59    }
60
61    let mut languages = Vec::new();
62    let entries = fs::read_dir(&i18n_root_path).unwrap_or_else(|e| {
63        panic!(
64            "Failed to read i18n directory at {:?}: {}",
65            i18n_root_path, e
66        )
67    });
68
69    for entry in entries {
70        let entry = entry.expect("Failed to read directory entry");
71        let path = entry.path();
72        if path.is_dir()
73            && let Some(lang_code) = path.file_name().and_then(|s| s.to_str())
74        {
75            let ftl_file_name = format!("{}.ftl", crate_name);
76            let ftl_path = path.join(ftl_file_name);
77
78            if ftl_path.exists() {
79                languages.push(lang_code.to_string());
80            }
81        }
82    }
83
84    let language_identifiers = languages.iter().map(|lang| {
85        quote! { es_fluent::unic_langid::langid!(#lang) }
86    });
87
88    let i18n_root_str = i18n_root_path.to_string_lossy();
89
90    let expanded = quote! {
91        #[derive(es_fluent::__rust_embed::RustEmbed)]
92        #[folder = #i18n_root_str]
93        struct #assets_struct_name;
94
95        impl es_fluent::__manager_core::EmbeddedAssets for #assets_struct_name {
96            fn domain() -> &'static str {
97                #crate_name
98            }
99        }
100
101        static #module_data_name: es_fluent::__manager_core::EmbeddedModuleData =
102            es_fluent::__manager_core::EmbeddedModuleData {
103                name: #crate_name,
104                domain: #crate_name,
105                supported_languages: &[
106                    #(#language_identifiers),*
107                ],
108            };
109
110        es_fluent::__inventory::submit!(
111            &es_fluent::__manager_core::EmbeddedI18nModule::<#assets_struct_name>::new(&#module_data_name)
112            as &dyn es_fluent::__manager_core::I18nModule
113        );
114    };
115
116    TokenStream::from(expanded)
117}
118
119/// Defines a Bevy i18n module.
120///
121/// This macro will:
122///
123/// 1.  Read the `i18n.toml` configuration file.
124/// 2.  Discover the available languages in the `i18n` directory.
125/// 3.  Generate an `AssetI18nModule` for the crate.
126#[proc_macro]
127pub fn define_bevy_i18n_module(_input: TokenStream) -> TokenStream {
128    let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME must be set");
129    let static_data_name = syn::Ident::new(
130        &format!(
131            "{}_I18N_ASSET_MODULE_DATA",
132            &crate_name.to_uppercase().replace('-', "_")
133        ),
134        proc_macro2::Span::call_site(),
135    );
136
137    let config = match es_fluent_toml::I18nConfig::read_from_manifest_dir() {
138        Ok(config) => config,
139        Err(es_fluent_toml::I18nConfigError::NotFound) => {
140            panic!(
141                "No i18n.toml configuration file found in project root. Please create one with the required settings."
142            );
143        },
144        Err(e) => {
145            panic!("Failed to read i18n.toml configuration: {}", e);
146        },
147    };
148
149    let i18n_root_path = match config.assets_dir_from_manifest() {
150        Ok(path) => path,
151        Err(e) => {
152            panic!(
153                "Failed to resolve assets directory from configuration: {}",
154                e
155            );
156        },
157    };
158
159    if let Err(e) = config.validate_assets_dir() {
160        panic!("Assets directory validation failed: {}", e);
161    }
162
163    let mut languages = Vec::new();
164    let entries = fs::read_dir(&i18n_root_path).unwrap_or_else(|e| {
165        panic!(
166            "Failed to read i18n directory at {:?}: {}",
167            i18n_root_path, e
168        )
169    });
170
171    for entry in entries {
172        let entry = entry.expect("Failed to read directory entry");
173        let path = entry.path();
174        if path.is_dir()
175            && let Some(lang_code) = path.file_name().and_then(|s| s.to_str())
176        {
177            let ftl_file_name = format!("{}.ftl", crate_name);
178            let ftl_path = path.join(ftl_file_name);
179
180            if ftl_path.exists() {
181                languages.push(lang_code.to_string());
182            }
183        }
184    }
185
186    let language_identifiers = languages.iter().map(|lang| {
187        quote! { es_fluent::unic_langid::langid!(#lang) }
188    });
189
190    let expanded = quote! {
191        static #static_data_name: es_fluent::__manager_core::AssetModuleData = es_fluent::__manager_core::AssetModuleData {
192            name: #crate_name,
193            domain: #crate_name,
194            supported_languages: &[
195                #(#language_identifiers),*
196            ],
197        };
198
199        es_fluent::__inventory::submit!(
200            &es_fluent::__manager_core::AssetI18nModule::new(&#static_data_name)
201            as &dyn es_fluent::__manager_core::I18nAssetModule
202        );
203    };
204
205    TokenStream::from(expanded)
206}