1use heck::{ToShoutySnakeCase, ToSnakeCase};
2use proc_macro::TokenStream;
3use proc_macro2::{Span, TokenStream as TokenStream2};
4use quote::{quote, quote_spanned};
5use syn::{Data, DeriveInput, Ident, Visibility, parse_macro_input};
6use unic_langid::{LanguageIdentifier, langid};
7
8use prepare::make_init;
9
10mod locales;
11mod prepare;
12mod read;
13
14use locales::{LangInfo, extract_messages};
15
16use crate::prepare::make_messages_methods;
17
18const DEFAULT_PATH: &str = "locales";
19const DEFAULT_FALLBACK_LOCALE: LanguageIdentifier = langid!("en");
20
21#[proc_macro_attribute]
37pub fn localize(_args: TokenStream, input: TokenStream) -> TokenStream {
38 let input = parse_macro_input!(input as DeriveInput);
39 match input.data {
40 Data::Struct(_) => (),
41 Data::Enum(d) => {
42 return quote_spanned! { d.enum_token.span => compile_error!("use struct"); }.into();
43 }
44 Data::Union(d) => {
45 return quote_spanned! { d.union_token.span => compile_error!("use struct"); }.into();
46 }
47 }
48
49 let dir = DEFAULT_PATH;
50 let files = match read::read_files(dir) {
51 Ok(c) => c,
52 Err(e) => {
53 let msg = format!("failed to read .ftl files from \"{dir}\": {e}");
54 return quote! { compile_error!(#msg); }.into();
55 }
56 };
57
58 let messages = match extract_messages(files) {
59 Ok(m) => m,
60 Err(e) => {
61 let msg = format!("invalid .ftl files:\n{e}");
62 return quote! { compile_error!(#msg); }.into();
63 }
64 };
65
66 let languages_names: Vec<_> = messages
67 .iter()
68 .map(|(lang, _)| lang.to_string())
69 .map(|l| l.to_snake_case())
70 .collect();
71
72 let ident = input.ident;
73 let vis = input.vis;
74 let res = localize_base(
75 vis.clone(),
76 ident,
77 messages,
78 languages_names,
79 DEFAULT_FALLBACK_LOCALE,
80 );
81
82 res.into()
83}
84
85fn localize_base(
86 vis: Visibility,
87 ident: Ident,
88 messages: Vec<(LanguageIdentifier, LangInfo)>,
89 languages_names: Vec<String>,
90 fallback_locale: LanguageIdentifier,
91) -> TokenStream2 {
92 let init_fun = make_init(&messages);
93 let message_methods = make_messages_methods(vis.clone(), &messages);
94
95 let mut languages_enum_variants = vec![];
96 let mut languages_enum_from = vec![];
97 for (lang_enum, lang_str) in languages_names
98 .iter()
99 .map(|l| (l.to_shouty_snake_case(), l))
100 {
101 let lang_enum = syn::Ident::new(&lang_enum, Span::call_site());
102 languages_enum_variants.push(quote! { #lang_enum });
103 languages_enum_from.push(quote! { #lang_str => Self::#lang_enum });
104 }
105
106 let fallback_lang_enum = syn::Ident::new(
107 fallback_locale.to_string().to_shouty_snake_case().as_str(),
108 Span::call_site(),
109 );
110 languages_enum_from.push(quote! {
111 _ => Self::#fallback_lang_enum,
112 });
113
114 quote! {
115 #[derive(Default)]
116 #vis struct #ident {
117 bundles: __interly::Bundles,
118 }
119
120 #vis mod __interly {
121 use ::std::collections::HashMap;
122 use ::std::sync::Arc;
123 use ::interly::{
124 FluentArgs,
125 FluentBundle,
126 FluentResource,
127 IntlLangMemoizer,
128 LanguageIdentifier,
129 Lazy,
130 };
131
132 use super::#ident;
133
134 pub(super) type Bundles = HashMap<
135 LANG,
136 FluentBundle<Arc<FluentResource>, IntlLangMemoizer>,
137 >;
138
139 impl #ident {
140 const FALLBACK_LANG: LANG = LANG::#fallback_lang_enum;
141
142 #vis fn init() -> Self {
143 #init_fun
144 }
145
146 #vis fn languages() -> ::std::vec::Vec<&'static str> {
147 ::std::vec![#(#languages_names),*]
148 }
149
150 #vis fn __format_msg(
151 &self,
152 msg_id: &str,
153 lang: LANG,
154 args: Option<&FluentArgs<'_>>,
155 ) -> String {
156 let mut bundle = self.bundles.get(&lang).expect("no bundle");
157 if !bundle.has_message(msg_id) {
158 bundle = self
159 .bundles
160 .get(&Self::FALLBACK_LANG)
161 .expect("no fallback bundle");
162 }
163 let msg = bundle
164 .get_message(msg_id)
165 .expect("no message")
166 .value()
167 .expect("no value in message");
168 let mut errs = ::std::vec![];
169 bundle.format_pattern(msg, args, &mut errs).to_string()
170 }
171
172 #message_methods
173 }
174
175 #vis static LOCALIZE: Lazy<#ident> = Lazy::new(|| { #ident::init() });
176
177 #[derive(PartialEq, Eq, Hash)]
178 #vis enum LANG {
179 #(#languages_enum_variants),*
180 }
181
182 impl From<&str> for LANG {
183 fn from(lang: &str) -> Self {
184 match lang.to_lowercase().as_str() {
185 #(#languages_enum_from),*
186 }
187 }
188 }
189 impl From<&::std::string::String> for LANG {
190 fn from(lang: &::std::string::String) -> Self {
191 lang.as_str().into()
192 }
193 }
194 }
195
196 #[allow(unused)]
197 #[macro_export] macro_rules! tr {
199 ($e:ident, $lang:expr) => {
200 tr!($e, $lang,)
201 };
202 ($e:ident, $lang:expr, $($v:expr),*) => {
203 $crate::__interly::LOCALIZE.$e($lang, $($v),*)
204 };
205 }
206
207 #[allow(unused)]
208 #[macro_export] macro_rules! tr_literal {
210 ($e:expr, $lang:expr) => {
211 $crate::__interly::LOCALIZE.__format_msg($e, $lang.into(), None)
212 };
213 }
217
218 }
220}