use std::{collections::BTreeMap, ops::Not};
pub mod interpolate;
pub mod locale;
pub mod parsed_value;
pub mod plurals;
pub mod ranges;
use interpolate::Interpolation;
use leptos_i18n_parser::{
parse_locales::{
ParsedLocales,
error::{Error, Result},
locale::{BuildersKeys, BuildersKeysInner, InterpolOrLit, Locale, LocaleValue, Namespace},
options::ParseOptions,
parsed_value::ParsedValue,
},
utils::{
UnwrapAt,
key::{Key, KeyPath},
},
};
use locale::LiteralType;
use parsed_value::TRANSLATIONS_KEY;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{ToTokens, format_ident, quote};
pub fn load_locales(
parsed_locales: &ParsedLocales,
crate_path: Option<&syn::Path>,
emit_diagnostics: bool,
top_level_attributes: Option<&TokenStream>,
gen_docs: bool,
) -> Result<TokenStream> {
let default_crate_path = syn::Path::from(syn::Ident::new("leptos_i18n", Span::call_site()));
let crate_path = crate_path.unwrap_or(&default_crate_path);
let ParsedLocales {
cfg,
builder_keys,
diag,
..
} = parsed_locales;
if cfg!(all(feature = "csr", feature = "dynamic_load")) && cfg.translations_uri.is_none() {
return Err(Error::MissingTranslationsURI.into());
}
let deprecated_ranges = if diag.has_ranges() {
Some(quote! {
mod __warn_deprecated_ranges {
fn __warn_deprecated_ranges_() {
use super::l_i18n_crate::__private::warn_deprecated_ranges;
warn_deprecated_ranges();
}
}
})
} else {
None
};
let diag = emit_diagnostics.then_some(diag);
let enum_ident = syn::Ident::new("Locale", Span::call_site());
let keys_ident = syn::Ident::new("I18nKeys", Span::call_site());
let translation_unit_enum_ident = syn::Ident::new("I18nTranslationUnitsId", Span::call_site());
let locale_type = create_locale_type(
builder_keys,
&keys_ident,
&enum_ident,
&translation_unit_enum_ident,
cfg.translations_uri.as_deref(),
&cfg.options,
gen_docs,
);
let locale_enum = create_locales_enum(
builder_keys,
&keys_ident,
&enum_ident,
&translation_unit_enum_ident,
&cfg.locales,
gen_docs,
)?;
let scopes_mod = create_scopes_module(builder_keys);
let mut macros_reexport = vec![
quote!(t),
quote!(td),
quote!(tu),
quote!(use_i18n_scoped),
quote!(scope_i18n),
quote!(scope_locale),
quote!(define_scope),
quote!(t_string),
quote!(tu_string),
quote!(t_display),
quote!(tu_display),
quote!(td_string),
quote!(td_display),
];
let providers = if cfg!(feature = "islands") {
macros_reexport.push(quote!(ti));
quote! {
use leptos::children::Children;
use leptos::prelude::RenderHtml;
#[l_i18n_crate::reexports::leptos::island]
#[allow(non_snake_case)]
pub fn I18nContextProvider(
#[prop(optional)]
set_lang_attr_on_html: Option<bool>,
#[prop(optional)]
set_dir_attr_on_html: Option<bool>,
#[prop(optional)]
enable_cookie: Option<bool>,
#[prop(optional, into)]
cookie_name: Option<Cow<'static, str>>,
children: Children
) -> impl IntoView {
l_i18n_crate::context::provide_i18n_context_component_island::<#enum_ident>(
set_lang_attr_on_html,
set_dir_attr_on_html,
enable_cookie,
cookie_name,
children
)
}
#[l_i18n_crate::reexports::leptos::island]
#[allow(non_snake_case)]
pub fn I18nSubContextProvider(
children: Children,
#[prop(optional)]
initial_locale: Option<#enum_ident>,
#[prop(optional, into)]
cookie_name: Option<Cow<'static, str>>,
) -> impl IntoView {
l_i18n_crate::context::i18n_sub_context_provider_island::<#enum_ident>(
children,
initial_locale,
cookie_name,
)
}
}
} else {
quote! {
use leptos::prelude::TypedChildren;
#[l_i18n_crate::reexports::leptos::component]
#[allow(non_snake_case)]
pub fn I18nContextProvider<Chil: IntoView + 'static>(
#[prop(optional)]
set_lang_attr_on_html: Option<bool>,
#[prop(optional)]
set_dir_attr_on_html: Option<bool>,
#[prop(optional)]
enable_cookie: Option<bool>,
#[prop(optional, into)]
cookie_name: Option<Cow<'static, str>>,
#[prop(optional)]
cookie_options: Option<CookieOptions<#enum_ident>>,
#[prop(optional)]
ssr_lang_header_getter: Option<UseLocalesOptions>,
children: TypedChildren<Chil>
) -> impl IntoView {
l_i18n_crate::context::provide_i18n_context_component::<#enum_ident, Chil>(
set_lang_attr_on_html,
set_dir_attr_on_html,
enable_cookie,
cookie_name,
cookie_options,
ssr_lang_header_getter,
children
)
}
#[l_i18n_crate::reexports::leptos::component]
#[allow(non_snake_case)]
pub fn I18nSubContextProvider<Chil: IntoView + 'static>(
children: TypedChildren<Chil>,
#[prop(optional, into)]
initial_locale: Option<Signal<#enum_ident>>,
#[prop(optional, into)]
cookie_name: Option<Cow<'static, str>>,
#[prop(optional)]
cookie_options: Option<CookieOptions<#enum_ident>>,
#[prop(optional)]
ssr_lang_header_getter: Option<UseLocalesOptions>,
) -> impl IntoView {
l_i18n_crate::context::i18n_sub_context_provider_inner::<#enum_ident, Chil>(
children,
initial_locale,
cookie_name,
cookie_options,
ssr_lang_header_getter
)
}
}
};
let macros_reexport = quote!(pub use #crate_path::{#(#macros_reexport,)*};);
Ok(quote! {
pub mod i18n {
#![allow(unused_braces)]
#![allow(clippy::type_complexity)]
#![allow(clippy::let_and_return)]
#![allow(clippy::unit_arg)]
#top_level_attributes
use #crate_path as l_i18n_crate;
#locale_enum
#locale_type
#scopes_mod
#[inline]
#[track_caller]
pub fn use_i18n() -> l_i18n_crate::I18nContext<#enum_ident> {
l_i18n_crate::use_i18n_context()
}
#[inline]
#[track_caller]
pub fn use_i18n_scoped<S: l_i18n_crate::Scope<#enum_ident>>() -> l_i18n_crate::I18nContext<#enum_ident, S> {
l_i18n_crate::use_i18n_with_scope()
}
#[deprecated(
note = "It is now preferred to use the <I18nContextProvider> component"
)]
#[track_caller]
pub fn provide_i18n_context() -> l_i18n_crate::I18nContext<#enum_ident> {
l_i18n_crate::context::provide_i18n_context_with_options_inner(Default::default())
}
mod providers {
use super::{l_i18n_crate, #enum_ident};
use l_i18n_crate::reexports::leptos;
#[allow(unused_imports)]
use leptos::prelude::{IntoView, Signal};
use std::borrow::Cow;
#[allow(unused_imports)]
use l_i18n_crate::context::{CookieOptions, UseLocalesOptions};
#providers
}
pub use providers::{I18nContextProvider, I18nSubContextProvider};
pub use l_i18n_crate::Locale as I18nLocaleTrait;
#macros_reexport
#diag
#deprecated_ranges
}
})
}
fn create_locales_enum(
keys: &BuildersKeys,
keys_ident: &syn::Ident,
enum_ident: &syn::Ident,
translation_unit_enum_ident: &syn::Ident,
locales: &[Key],
gen_docs: bool,
) -> Result<TokenStream> {
let as_str_match_arms = locales
.iter()
.map(|key| (&key.ident, &key.name))
.map(|(variant, locale)| quote!(#enum_ident::#variant => #locale))
.collect::<Vec<_>>();
let from_str_match_arms = locales
.iter()
.map(|key| (&key.ident, &key.name))
.map(|(variant, locale)| quote!(#locale => Ok(#enum_ident::#variant)))
.collect::<Vec<_>>();
let constant_names_ident = locales
.iter()
.map(|key| {
(
key,
format_ident!("{}_LANGID", key.name.to_uppercase().replace('-', "_")),
)
})
.collect::<Vec<_>>();
let static_icu_locales = constant_names_ident
.iter()
.map(|(key, ident)| {
let locale = &key.name;
quote!(static #ident: std::sync::LazyLock<l_i18n_crate::reexports::icu::locid::Locale> = std::sync::LazyLock::new(|| #locale.parse().expect("Valid locale"));)
})
.collect::<Vec<_>>();
let as_icu_locale_match_arms = constant_names_ident
.iter()
.map(|(variant, constant)| quote!(#enum_ident::#variant => &#constant))
.collect::<Vec<_>>();
let server_fn_mod = if cfg!(all(feature = "dynamic_load", not(feature = "csr"))) {
quote! {
mod server_fn {
#[allow(unused_imports)]
use super::{l_i18n_crate, #enum_ident, #keys_ident, #translation_unit_enum_ident};
use l_i18n_crate::reexports::leptos::server_fn;
#[l_i18n_crate::reexports::leptos::server(I18nRequestTranslationsServerFn)]
pub async fn i18n_request_translations(locale: #enum_ident, translations_id: #translation_unit_enum_ident) -> Result<l_i18n_crate::__private::fetch_translations::LocaleServerFnOutput, server_fn::ServerFnError> {
let strings = #keys_ident::__i18n_request_translations__(locale, translations_id);
let wrapped = l_i18n_crate::__private::fetch_translations::LocaleServerFnOutput::new(strings);
Ok(wrapped)
}
}
}
} else if cfg!(all(feature = "dynamic_load", feature = "csr")) {
quote! {
mod server_fn {
#[allow(unused_imports)]
use super::{l_i18n_crate, #enum_ident, #keys_ident, #translation_unit_enum_ident};
use l_i18n_crate::reexports::leptos::server_fn::ServerFnError;
pub async fn i18n_request_translations(locale: #enum_ident, translations_id: #translation_unit_enum_ident) -> Result<l_i18n_crate::__private::fetch_translations::LocaleServerFnOutput, ServerFnError> {
#keys_ident::__i18n_request_translations__(locale, translations_id).await
}
}
}
} else {
quote!()
};
let server_fn_type = if cfg!(all(feature = "dynamic_load", not(feature = "csr"))) {
quote!(
type ServerFn = server_fn::I18nRequestTranslationsServerFn;
)
} else {
quote!()
};
let request_translations = if cfg!(feature = "dynamic_load") {
quote! {
fn request_translations(
self,
translations_id: #translation_unit_enum_ident,
) -> impl std::future::Future<Output = Result<l_i18n_crate::__private::fetch_translations::LocaleServerFnOutput, l_i18n_crate::reexports::leptos::server_fn::ServerFnError>> + Send + Sync + 'static {
server_fn::i18n_request_translations(self, translations_id)
}
}
} else {
quote!()
};
let init_translations = if cfg!(all(feature = "dynamic_load", feature = "hydrate")) {
quote! {
fn init_translations(self, translations_id: Self::TranslationUnitId, values: Vec<Box<str>>) {
#keys_ident::__init_translations__(self, translations_id, values);
}
}
} else {
quote!()
};
let ld = icu_locale::LocaleDirectionality::new_common();
let locids = locales
.iter()
.map(|locale| match locale.name.parse::<icu_locale::Locale>() {
Ok(loc) => Ok((locale, loc.id)),
Err(err) => Err(Error::InvalidLocale {
locale: locale.name.clone(),
err,
}
.into()),
})
.collect::<Result<Vec<_>>>()?;
let direction_match_arms = locids.iter().map(|(locale, locid)| {
let dir = match ld.get(locid) {
Some(icu_locale::Direction::LeftToRight) => quote!(LeftToRight),
Some(icu_locale::Direction::RightToLeft) => quote!(RightToLeft),
_ => quote!(Auto),
};
quote! {
#enum_ident::#locale => l_i18n_crate::Direction::#dir
}
});
let docs = if gen_docs {
let mut docs = String::from("## Supported locales:\n");
for (i, key) in locales.iter().enumerate() {
use core::fmt::Write;
if i == 0 {
writeln!(&mut docs, "- `{}` (default)", key).unwrap();
} else {
writeln!(&mut docs, "- `{}`", key).unwrap();
}
}
match keys {
BuildersKeys::NameSpaces { namespaces, .. } => {
use core::fmt::Write;
writeln!(&mut docs, "\n## Namespaces :").unwrap();
for ns in namespaces {
writeln!(&mut docs, "- `{}`", ns.key).unwrap();
}
}
BuildersKeys::Locales { keys, .. } => {
gen_keys_doc(&mut docs, &keys.0).unwrap();
}
};
quote! {
#[doc = #docs]
}
} else {
quote! {}
};
let ts = quote! {
#docs
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Default)]
#[allow(non_camel_case_types)]
pub enum #enum_ident {
#[default]
#(#locales,)*
}
impl l_i18n_crate::reexports::serde::Serialize for #enum_ident {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: l_i18n_crate::reexports::serde::Serializer,
{
l_i18n_crate::reexports::serde::Serialize::serialize(l_i18n_crate::Locale::as_str(*self), serializer)
}
}
impl<'de> l_i18n_crate::reexports::serde::Deserialize<'de> for #enum_ident {
fn deserialize<D>(deserializer: D) -> Result<#enum_ident, D::Error>
where
D: l_i18n_crate::reexports::serde::de::Deserializer<'de>,
{
l_i18n_crate::reexports::serde::de::Deserializer::deserialize_str(deserializer, l_i18n_crate::__private::LocaleVisitor::<#enum_ident>::new())
}
}
impl #enum_ident {
pub const fn get_keys_const(self) -> #keys_ident {
#keys_ident::__new_internal(self)
}
}
impl l_i18n_crate::Locale for #enum_ident {
type Keys = #keys_ident;
type TranslationUnitId = #translation_unit_enum_ident;
#server_fn_type
fn as_str(self) -> &'static str {
let s = match self {
#(
#as_str_match_arms,
)*
};
l_i18n_crate::__private::intern(s)
}
fn as_icu_locale(self) -> &'static l_i18n_crate::reexports::icu::locid::Locale {
#(
#static_icu_locales
)*
match self {
#(
#as_icu_locale_match_arms,
)*
}
}
fn direction(self) -> l_i18n_crate::Direction {
match self {
#(
#direction_match_arms,
)*
}
}
fn get_all() -> &'static [Self] {
&[#(#enum_ident::#locales,)*]
}
fn to_base_locale(self) -> Self {
self
}
fn from_base_locale(locale: Self) -> Self {
locale
}
#request_translations
#init_translations
}
impl core::str::FromStr for #enum_ident {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
#(#from_str_match_arms,)*
_ => Err(())
}
}
}
impl core::convert::AsRef<l_i18n_crate::reexports::icu::locid::LanguageIdentifier> for #enum_ident {
fn as_ref(&self) -> &l_i18n_crate::reexports::icu::locid::LanguageIdentifier {
l_i18n_crate::Locale::as_langid(*self)
}
}
impl core::convert::AsRef<l_i18n_crate::reexports::icu::locid::Locale> for #enum_ident {
fn as_ref(&self) -> &l_i18n_crate::reexports::icu::locid::Locale {
l_i18n_crate::Locale::as_icu_locale(*self)
}
}
impl core::convert::AsRef<str> for #enum_ident {
fn as_ref(&self) -> &str {
l_i18n_crate::Locale::as_str(*self)
}
}
impl core::convert::AsRef<Self> for #enum_ident {
fn as_ref(&self) -> &Self {
self
}
}
impl core::fmt::Display for #enum_ident {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::Display::fmt(l_i18n_crate::Locale::as_str(*self), f)
}
}
#server_fn_mod
};
Ok(ts)
}
struct Subkeys<'a> {
original_key: Key,
key: syn::Ident,
mod_key: syn::Ident,
locales: &'a [Locale],
keys: &'a BuildersKeysInner,
docs: TokenStream,
}
impl<'a> Subkeys<'a> {
pub fn mod_ident(key: &Key) -> Ident {
format_ident!("sk_{}", key)
}
pub fn item_ident(key: &Key) -> Ident {
format_ident!("{}_subkeys", key)
}
pub fn new(
key: Key,
key_path: &KeyPath,
locales: &'a [Locale],
keys: &'a BuildersKeysInner,
gen_docs: bool,
) -> Self {
let mod_key = Self::mod_ident(&key);
let new_key = Self::item_ident(&key);
let docs = if gen_docs {
let path = key_path.to_string_with_key(&key);
let mut docs = format!("Full path: `{}`\n", path);
gen_keys_doc(&mut docs, &keys.0).unwrap();
quote! {
#[doc = #docs]
}
} else {
quote! {}
};
Subkeys {
original_key: key,
key: new_key,
mod_key,
locales,
keys,
docs,
}
}
}
fn strings_accessor_method_name(locale: &Locale) -> Ident {
format_ident!("__get_{}_translations__", locale.top_locale_name)
}
fn create_scopes_module(keys: &BuildersKeys) -> TokenStream {
match keys {
BuildersKeys::NameSpaces { keys, .. } => {
let inner_scopes = keys.iter().map(|(key, keys)| {
let ns_mod_ident = create_namespace_mod_ident(&key.ident);
let inner_scopes = create_scopes_module_inner(&keys.0);
quote! {
pub mod #key {
#[doc(hidden)]
pub mod __sk_self {
#[allow(unused)]
pub use super::super::super::namespaces::#ns_mod_ident::subkeys::*;
}
#[doc(hidden)]
#[allow(non_camel_case_types, non_snake_case, unused)]
pub use super::super::namespaces::#ns_mod_ident::#key as __this;
#(#inner_scopes)*
}
}
});
quote! {
pub mod scopes {
#(#inner_scopes)*
}
}
}
BuildersKeys::Locales { keys, .. } => {
let inner_scopes = create_scopes_module_inner(&keys.0);
quote! {
pub mod scopes {
#[doc(hidden)]
pub mod __sk_self {
#[allow(unused)]
pub use super::super::subkeys::*;
}
#(#inner_scopes)*
}
}
}
}
}
fn create_scopes_module_inner(
keys: &BTreeMap<Key, LocaleValue>,
) -> impl Iterator<Item = TokenStream> {
keys.iter()
.filter_map(|(key, value)| match value {
LocaleValue::Subkeys { keys, .. } => Some((key, &keys.0)),
_ => None,
})
.map(|(key, keys)| {
let inner_scopes = create_scopes_module_inner(keys);
let item_ident = Subkeys::item_ident(key);
let mod_ident = Subkeys::mod_ident(key);
quote! {
pub mod #key {
#[doc(hidden)]
pub mod __sk_self {
#[allow(unused)]
pub use super::super::__sk_self::#mod_ident::subkeys::*;
}
#[doc(hidden)]
#[allow(non_camel_case_types, non_snake_case, unused)]
pub use super::__sk_self::#mod_ident::#item_ident as __this;
#(#inner_scopes)*
}
}
})
}
fn create_locale_type_inner<const IS_TOP: bool>(
type_ident: &syn::Ident,
parent_ident: Option<&syn::Ident>,
enum_ident: &syn::Ident,
translation_unit_enum_ident: &syn::Ident,
locales: &[Locale],
keys: &BTreeMap<Key, LocaleValue>,
key_path: &mut KeyPath,
namespace_name: Option<&str>,
translations_uri: Option<&str>,
options: &ParseOptions,
docs: &TokenStream,
gen_docs: bool,
) -> TokenStream {
let translations_key = Key::new(TRANSLATIONS_KEY).unwrap_at("TRANSLATIONS_KEY");
let literal_keys = keys
.iter()
.filter_map(|(key, value)| match value {
LocaleValue::Value {
value: InterpolOrLit::Lit(t),
defaults,
} => Some((key, LiteralType::from(*t), defaults)),
_ => None,
})
.collect::<Vec<_>>();
let literal_accessors = literal_keys
.iter()
.map(|(key, literal_type, defaults)| {
let computed_defaults= defaults.compute();
if options.show_keys_only {
let key_str = key_path.to_string_with_key(key);
if cfg!(all(feature = "dynamic_load", not(feature = "ssr"))) {
quote! {
pub fn #key(self) -> l_i18n_crate::__private::LitWrapperFut<impl std::future::Future<Output = l_i18n_crate::__private::LitWrapper<&'static str>>> {
let fut = async move {
l_i18n_crate::__private::LitWrapper::new(#key_str)
};
l_i18n_crate::__private::LitWrapperFut::new(fut)
}
}
} else if cfg!(feature = "dynamic_load") {
quote! {
pub fn #key(self) -> l_i18n_crate::__private::LitWrapperFut<l_i18n_crate::__private::LitWrapper<&'static str>> {
l_i18n_crate::__private::LitWrapperFut::new_not_fut(#key_str)
}
}
} else {
quote! {
pub const fn #key(self) -> l_i18n_crate::__private::LitWrapper<&'static str> {
l_i18n_crate::__private::LitWrapper::new(#key_str)
}
}
}
} else {
let match_arms = locales.iter().filter_map(|locale| {
let lit = locale
.keys
.get(key)
.unwrap_at("create_locale_type_inner_1");
if matches!(lit, ParsedValue::Default) {
return None;
}
let ident = &locale.top_locale_name;
let accessor = strings_accessor_method_name(locale);
let defaulted = computed_defaults.get(&locale.top_locale_name).map(|defaulted_locales| {
defaulted_locales.iter().map(|key| {
quote!(| #enum_ident::#key)
}).collect::<TokenStream>()
});
let lit = parsed_value::to_token_stream(lit, locale.top_locale_string_count);
let ts = if *literal_type == LiteralType::String {
let strings_count = locale.top_locale_string_count;
if cfg!(all(feature = "dynamic_load", not(feature = "ssr"))) {
quote! {
#enum_ident::#ident #defaulted => {
#[allow(unused)]
let #translations_key: &'static [Box<str>; #strings_count] = #type_ident::#accessor().await;
l_i18n_crate::__private::LitWrapper::new(#lit)
}
}
} else if cfg!(all(feature = "dynamic_load", feature = "ssr")) {
quote! {
#enum_ident::#ident #defaulted => {
#[allow(unused)]
let #translations_key: &'static [&'static str; #strings_count] = #type_ident::#accessor();
l_i18n_crate::__private::LitWrapperFut::new_not_fut(#lit)
}
}
} else {
quote! {
#enum_ident::#ident #defaulted => {
#[allow(unused)]
const #translations_key: &[&str; #strings_count] = #type_ident::#accessor();
l_i18n_crate::__private::LitWrapper::new(#lit)
}
}
}
} else if cfg!(feature = "dynamic_load") {
quote! {
#enum_ident::#ident #defaulted => {
l_i18n_crate::__private::LitWrapperFut::new_not_fut(#lit)
}
}
} else {
quote! {
#enum_ident::#ident #defaulted => {
l_i18n_crate::__private::LitWrapper::new(#lit)
}
}
};
Some(ts)
});
if cfg!(all(feature = "dynamic_load", not(feature = "ssr"))) {
quote! {
pub fn #key(self) -> l_i18n_crate::__private::LitWrapperFut<impl std::future::Future<Output = l_i18n_crate::__private::LitWrapper<#literal_type>>> {
let fut = async move {
match self.0 {
#(
#match_arms
)*
}
};
l_i18n_crate::__private::LitWrapperFut::new(fut)
}
}
} else if cfg!(all(feature = "dynamic_load", feature = "ssr")) {
quote! {
pub fn #key(self) -> l_i18n_crate::__private::LitWrapperFut<l_i18n_crate::__private::LitWrapper<#literal_type>> {
match self.0 {
#(
#match_arms
)*
}
}
}
} else {
quote! {
pub const fn #key(self) -> l_i18n_crate::__private::LitWrapper<#literal_type> {
match self.0 {
#(
#match_arms
)*
}
}
}
}
}
})
.collect::<Vec<_>>();
let subkeys = keys
.iter()
.filter_map(|(key, value)| match value {
LocaleValue::Subkeys { locales, keys } => {
Some(Subkeys::new(key.clone(), key_path, locales, keys, gen_docs))
}
_ => None,
})
.collect::<Vec<_>>();
let subkeys_ts = subkeys.iter().map(|sk| {
let subkey_mod_ident = &sk.mod_key;
let mut pushed_key = key_path.push_key(sk.original_key.clone());
let subkey_impl = create_locale_type_inner::<false>(
&sk.key,
Some(type_ident),
enum_ident,
translation_unit_enum_ident,
sk.locales,
&sk.keys.0,
&mut pushed_key,
namespace_name,
translations_uri,
options,
&sk.docs,
gen_docs,
);
quote! {
pub mod #subkey_mod_ident {
use super::{#enum_ident, l_i18n_crate};
#subkey_impl
}
}
});
let subkeys_accessors = subkeys.iter().map(|sk| {
let original_key = &sk.original_key;
let key = &sk.key;
let mod_ident = &sk.mod_key;
let docs = &sk.docs;
quote! {
#docs
pub const fn #original_key(self) -> subkeys::#mod_ident::#key {
subkeys::#mod_ident::#key::__new_internal(self.0)
}
}
});
let subkeys_module = quote! {
#[doc(hidden)]
pub mod subkeys {
#[allow(unused)]
use super::{#enum_ident, l_i18n_crate};
#(
#subkeys_ts
)*
}
};
let builders = keys
.iter()
.filter_map(|(key, value)| match value {
LocaleValue::Value {
value: InterpolOrLit::Interpol(keys),
defaults,
} => Some((
key,
Interpolation::new(
key, enum_ident, keys, locales, key_path, type_ident, defaults, options,
gen_docs,
),
)),
_ => None,
})
.collect::<Vec<_>>();
let builder_accessors = builders.iter().map(|(key, inter)| {
let inter_ident = &inter.ident;
let docs = &inter.docs;
quote! {
#docs
pub const fn #key(self) -> builders::#inter_ident {
builders::#inter_ident::new(self.0)
}
}
});
let builder_impls = builders.iter().map(|(_, inter)| &inter.imp);
let builder_module = builders.is_empty().not().then(move || {
quote! {
#[doc(hidden)]
pub mod builders {
use super::{#enum_ident, l_i18n_crate};
#(
#builder_impls
)*
}
}
});
let string_holders = if IS_TOP {
locales
.iter()
.map(|locale| {
let locale_name = &*locale.top_locale_name.ident;
let struct_name = format_ident!("{}_{}", type_ident, locale_name);
let strings_count = locale.top_locale_string_count;
let strings = &*locale.strings;
let get_fn = if cfg!(all(feature = "dynamic_load", not(feature = "ssr"))) {
quote! {
pub async fn get_translations() -> &'static [Box<str>; #strings_count] {
<Self as l_i18n_crate::__private::fetch_translations::TranslationUnit>::request_strings().await
}
}
} else if cfg!(all(feature = "dynamic_load", feature = "ssr")) {
quote! {
pub fn get_translations() -> &'static [&'static str; #strings_count] {
<Self as l_i18n_crate::__private::fetch_translations::TranslationUnit>::register();
<Self as l_i18n_crate::__private::fetch_translations::TranslationUnit>::STRINGS
}
}
} else {
quote! {
pub const fn get_translations() -> &'static [&'static str; #strings_count] {
<Self as l_i18n_crate::__private::fetch_translations::TranslationUnit>::STRINGS
}
}
};
let request_translations = if cfg!(all(feature = "dynamic_load", feature = "csr")) {
let uri = translations_uri.expect("Missing URI"); let endpoint = uri.replace("{locale}", &locale.name.name).replace("{namespace}", namespace_name.unwrap_or(""));
quote! {
pub async fn __i18n_request_translations__() -> Result<l_i18n_crate::__private::fetch_translations::LocaleServerFnOutput, l_i18n_crate::reexports::leptos::server_fn::ServerFnError> {
use l_i18n_crate::reexports::leptos::server_fn;
#[l_i18n_crate::reexports::leptos::server(endpoint = #endpoint, prefix = "", input = l_i18n_crate::reexports::leptos::server_fn::codec::GetUrl, output = l_i18n_crate::reexports::leptos::server_fn::codec::Json)]
pub async fn i18n_request_translations_inner() -> Result<l_i18n_crate::__private::fetch_translations::LocaleServerFnOutput, server_fn::ServerFnError>;
i18n_request_translations_inner().await
}
}
} else {
quote!()
};
let id = if parent_ident.is_some() {
quote!(const ID: super::super::#translation_unit_enum_ident = super::super::#translation_unit_enum_ident::#type_ident)
} else {
quote!(const ID: () = ())
};
let get_string = if cfg!(not(all(feature = "dynamic_load", not(feature = "ssr")))) {
quote!{
const STRINGS: &[&str; #strings_count] = &[#(#strings,)*];
}
} else {
quote! {
fn get_strings_lock() -> &'static l_i18n_crate::__private::fetch_translations::OnceCell<Box<Self::Strings>> {
Self::__get_strings_lock()
}
}
};
let string_type = if cfg!(all(feature = "dynamic_load", not(feature = "ssr"))) {
quote!([Box<str>; #strings_count])
} else {
quote!([&'static str; #strings_count])
};
let translation_unit_impl = quote! {
impl l_i18n_crate::__private::fetch_translations::TranslationUnit for #struct_name {
type Locale = #enum_ident;
const LOCALE: #enum_ident = #enum_ident::#locale_name;
#id;
type Strings = #string_type;
#get_string
}
};
let get_strings_lock_fn = if cfg!(all(feature = "dynamic_load", not(feature = "ssr"))) {
quote! {
fn __get_strings_lock() -> &'static l_i18n_crate::__private::fetch_translations::OnceCell<Box<[Box<str>; #strings_count]>> {
static STRINGS_LOCK: l_i18n_crate::__private::fetch_translations::OnceCell<Box<[Box<str>; #strings_count]>> = l_i18n_crate::__private::fetch_translations::OnceCell::new();
&STRINGS_LOCK
}
}
} else {
quote! {}
};
quote! {
#[allow(non_camel_case_types)]
struct #struct_name;
impl #struct_name {
#get_fn
#request_translations
#get_strings_lock_fn
}
#translation_unit_impl
}
})
.collect()
} else {
quote! {}
};
let string_accessors = locales.iter().map(|locale| {
let accessor_ident = strings_accessor_method_name(locale);
let strings_count = locale.top_locale_string_count;
match parent_ident {
Some(parent) if !IS_TOP => {
if cfg!(all(feature = "dynamic_load", not(feature = "ssr"))) {
quote! {
pub async fn #accessor_ident() -> &'static [Box<str>; #strings_count] {
super::super::#parent::#accessor_ident().await
}
}
} else if cfg!(all(feature = "dynamic_load", feature = "ssr")) {
quote! {
pub fn #accessor_ident() -> &'static [&'static str; #strings_count] {
super::super::#parent::#accessor_ident()
}
}
} else {
quote! {
pub const fn #accessor_ident() -> &'static [&'static str; #strings_count] {
super::super::#parent::#accessor_ident()
}
}
}
}
_ => {
let string_holder = format_ident!("{}_{}", type_ident, locale.top_locale_name);
if cfg!(all(feature = "dynamic_load", not(feature = "ssr"))) {
quote! {
pub async fn #accessor_ident() -> &'static [Box<str>; #strings_count] {
#string_holder::get_translations().await
}
}
} else if cfg!(all(feature = "dynamic_load", feature = "ssr")) {
quote! {
pub fn #accessor_ident() -> &'static [&'static str; #strings_count] {
#string_holder::get_translations()
}
}
} else {
quote! {
pub const fn #accessor_ident() -> &'static [&'static str; #strings_count] {
#string_holder::get_translations()
}
}
}
}
}
});
let i18n_request_translations_fn = if IS_TOP {
let match_arms = locales.iter().map(|locale| {
let string_holder = format_ident!("{}_{}", type_ident, locale.top_locale_name);
let locale_name = &locale.top_locale_name;
if cfg!(all(feature = "dynamic_load", feature = "csr")) {
quote! {
#enum_ident::#locale_name => #string_holder::__i18n_request_translations__().await
}
} else {
quote! {
#enum_ident::#locale_name => #string_holder::get_translations()
}
}
});
let match_stmt = if cfg!(all(
feature = "dynamic_load",
not(any(feature = "ssr", feature = "csr"))
)) {
quote! {
unreachable!(
"This function should not have been called on the client!"
)
}
} else {
quote! {
match _locale {
#(
#match_arms,
)*
}
}
};
if cfg!(all(feature = "dynamic_load", feature = "csr")) {
quote! {
#[doc(hidden)]
pub async fn __i18n_request_translations__(_locale: #enum_ident, _: ()) -> Result<l_i18n_crate::__private::fetch_translations::LocaleServerFnOutput, l_i18n_crate::reexports::leptos::server_fn::ServerFnError> {
#match_stmt
}
}
} else {
quote! {
#[doc(hidden)]
pub fn __i18n_request_translations__(_locale: #enum_ident, _: ()) -> &'static [&'static str] {
#match_stmt
}
}
}
} else {
quote!()
};
let init_translations = if IS_TOP && cfg!(all(feature = "dynamic_load", feature = "hydrate")) {
if cfg!(feature = "ssr") {
quote! {
#[doc(hidden)]
pub fn __init_translations__(_locale: #enum_ident, _: (), _values: Vec<Box<str>>) {
panic!("Tried to compile with both \"ssr\" and \"hydrate\" features enabled.")
}
}
} else {
let match_arms = locales.iter().map(|locale| {
let string_holder = format_ident!("{}_{}", type_ident, locale.top_locale_name);
let locale_name = &locale.top_locale_name;
quote! {
#enum_ident::#locale_name => <#string_holder as l_i18n_crate::__private::fetch_translations::TranslationUnit>::init_translations(values)
}
});
quote! {
#[doc(hidden)]
pub fn __init_translations__(locale: #enum_ident, _: (), values: Vec<Box<str>>) {
match locale {
#(
#match_arms,
)*
}
}
}
}
} else {
quote!()
};
quote! {
#docs
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[allow(non_camel_case_types, non_snake_case)]
pub struct #type_ident(#enum_ident);
#[allow(dead_code)]
type #translation_unit_enum_ident = ();
impl #type_ident {
pub const fn __new_internal(locale: #enum_ident) -> Self {
#type_ident(locale)
}
#(
#[allow(non_snake_case)]
#literal_accessors
)*
#(
#[allow(non_snake_case)]
#subkeys_accessors
)*
#(
#[allow(non_snake_case)]
#builder_accessors
)*
#(
#[allow(non_snake_case)]
#string_accessors
)*
#i18n_request_translations_fn
#init_translations
}
impl l_i18n_crate::LocaleKeys for #type_ident {
type Locale = #enum_ident;
fn from_locale(locale: #enum_ident) -> Self {
Self::__new_internal(locale)
}
}
#string_holders
#builder_module
#subkeys_module
}
}
fn create_namespace_mod_ident(namespace_ident: &syn::Ident) -> syn::Ident {
format_ident!("ns_{}", namespace_ident)
}
fn create_namespaces_types(
keys_ident: &syn::Ident,
enum_ident: &syn::Ident,
translation_unit_enum_ident: &syn::Ident,
namespaces: &[Namespace],
keys: &BTreeMap<Key, BuildersKeysInner>,
translations_uri: Option<&str>,
options: &ParseOptions,
gen_docs: bool,
) -> TokenStream {
let docs = if gen_docs {
use core::fmt::Write;
let mut docs = String::from("# Namespaces :\n");
for ns in namespaces {
writeln!(&mut docs, "- `{}`", ns.key.name).unwrap();
}
quote! {
#[doc = #docs]
}
} else {
quote! {}
};
let namespaces = namespaces
.iter()
.map(|ns| {
let namespace_module_ident = create_namespace_mod_ident(&ns.key.ident);
let docs = if gen_docs {
let keys = keys.get(&ns.key).unwrap_at("create_namespaces_types_2");
let mut docs = format!("Full path: `{}`\n", ns.key);
gen_keys_doc(&mut docs, &keys.0).unwrap();
quote! {
#[doc = #docs]
}
} else {
quote! {}
};
(ns, namespace_module_ident, docs)
})
.collect::<Vec<_>>();
let namespaces_ts = namespaces
.iter()
.map(|(namespace, namespace_module_ident, docs)| {
let keys = keys
.get(&namespace.key)
.unwrap_at("create_namespaces_types_1");
let mut key_path = KeyPath::new(Some(namespace.key.clone()));
let type_impl = create_locale_type_inner::<true>(
&namespace.key.ident,
Some(keys_ident),
enum_ident,
translation_unit_enum_ident,
&namespace.locales,
&keys.0,
&mut key_path,
Some(&namespace.key.name),
translations_uri,
options,
docs,
gen_docs,
);
quote! {
pub mod #namespace_module_ident {
use super::{#enum_ident, l_i18n_crate};
#type_impl
}
}
});
let namespaces_accessors =
namespaces
.iter()
.map(|(namespace, namespace_module_ident, docs)| {
let key = &namespace.key;
quote! {
#docs
pub fn #key(self) -> namespaces::#namespace_module_ident::#key {
namespaces::#namespace_module_ident::#key::__new_internal(self.0)
}
}
});
let translations_unit_variants = namespaces.iter().map(|(ns, _, _)| ns.key.to_token_stream());
let as_str_match_arms = namespaces.iter().map(|(ns, _, _)| {
let ns_ident = &ns.key.ident;
let ns_name = &ns.key.name;
quote! {
#translation_unit_enum_ident::#ns_ident => #ns_name
}
});
let deserialize_match_arms = namespaces.iter().map(|(ns, _, _)| {
let ns_ident = &ns.key.ident;
let ns_name = &ns.key.name;
quote! {
#ns_name => Ok(#translation_unit_enum_ident::#ns_ident)
}
});
let get_strings_match_arms = namespaces.iter().map(|(ns, namespace_module_ident, _)| {
let ns_ident = &ns.key.ident;
let maybe_await = cfg!(all(feature = "dynamic_load", feature = "csr")).then(|| quote!(.await));
quote! {
#translation_unit_enum_ident::#ns_ident => namespaces::#namespace_module_ident::#ns_ident::__i18n_request_translations__(locale, ()) #maybe_await
}
});
let get_strings_match_stmt = if cfg!(all(
feature = "dynamic_load",
not(any(feature = "ssr", feature = "csr"))
)) {
quote! {
unreachable!(
"This function should not have been called on the client!"
)
}
} else {
quote! {
match translations_id {
#(
#get_strings_match_arms,
)*
}
}
};
let init_translations = if cfg!(all(feature = "dynamic_load", feature = "hydrate")) {
let match_arms = namespaces.iter().map(|(ns, namespace_module_ident, _)| {
let ns_ident = &ns.key.ident;
quote! {
#translation_unit_enum_ident::#ns_ident => namespaces::#namespace_module_ident::#ns_ident::__init_translations__(locale, (), values)
}
});
quote! {
#[doc(hidden)]
pub fn __init_translations__(locale: #enum_ident, translations_id: #translation_unit_enum_ident, values: Vec<Box<str>>) {
match translations_id {
#(
#match_arms,
)*
}
}
}
} else {
quote!()
};
let translation_request_fn = if cfg!(all(feature = "dynamic_load", feature = "csr")) {
quote! {
#[doc(hidden)]
#[allow(unused_variables)]
pub async fn __i18n_request_translations__(locale: #enum_ident, translations_id: #translation_unit_enum_ident) -> Result<l_i18n_crate::__private::fetch_translations::LocaleServerFnOutput, l_i18n_crate::reexports::leptos::server_fn::ServerFnError> {
#get_strings_match_stmt
}
}
} else {
quote! {
#[doc(hidden)]
#[allow(unused_variables)]
pub fn __i18n_request_translations__(locale: #enum_ident, translations_id: #translation_unit_enum_ident) -> &'static [&'static str] {
#get_strings_match_stmt
}
}
};
quote! {
#[doc(hidden)]
pub mod namespaces {
use super::{#enum_ident, l_i18n_crate};
#(
#namespaces_ts
)*
}
#docs
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[allow(non_snake_case)]
pub struct #keys_ident(#enum_ident);
impl #keys_ident {
pub const fn __new_internal(locale: #enum_ident) -> Self {
Self(locale)
}
#(
#[allow(non_snake_case)]
#namespaces_accessors
)*
#translation_request_fn
#init_translations
}
impl l_i18n_crate::LocaleKeys for #keys_ident {
type Locale = #enum_ident;
fn from_locale(locale: #enum_ident) -> Self {
Self::__new_internal(locale)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(non_camel_case_types)]
pub enum #translation_unit_enum_ident {
#(
#translations_unit_variants,
)*
}
impl #translation_unit_enum_ident {
pub fn as_str(self) -> &'static str {
match self {
#(
#as_str_match_arms,
)*
}
}
}
impl l_i18n_crate::__private::TranslationUnitId for #translation_unit_enum_ident {
fn to_str(self) -> Option<&'static str> {
Some(self.as_str())
}
}
impl l_i18n_crate::reexports::serde::Serialize for #translation_unit_enum_ident {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: l_i18n_crate::reexports::serde::Serializer,
{
l_i18n_crate::reexports::serde::Serialize::serialize((*self).as_str(), serializer)
}
}
impl<'de> l_i18n_crate::reexports::serde::Deserialize<'de> for #translation_unit_enum_ident {
fn deserialize<D>(deserializer: D) -> Result<#translation_unit_enum_ident, D::Error>
where
D: l_i18n_crate::reexports::serde::de::Deserializer<'de>,
{
let s = l_i18n_crate::reexports::serde::de::Deserializer::deserialize_string(deserializer, l_i18n_crate::__private::StrVisitor)?;
match s.as_str() {
#(
#deserialize_match_arms,
)*
_ => Err(<D::Error as leptos_i18n::reexports::serde::de::Error>::custom(format!("invalid translation unit id: {s}")))
}
}
}
}
}
fn create_locale_type(
keys: &BuildersKeys,
keys_ident: &syn::Ident,
enum_ident: &syn::Ident,
translation_unit_enum_ident: &syn::Ident,
translations_uri: Option<&str>,
options: &ParseOptions,
gen_docs: bool,
) -> TokenStream {
match keys {
BuildersKeys::NameSpaces { namespaces, keys } => create_namespaces_types(
keys_ident,
enum_ident,
translation_unit_enum_ident,
namespaces,
keys,
translations_uri,
options,
gen_docs,
),
BuildersKeys::Locales { locales, keys } => {
let docs = if gen_docs {
let mut docs = String::new();
gen_keys_doc(&mut docs, &keys.0).unwrap();
quote! {
#[doc = #docs]
}
} else {
quote! {}
};
create_locale_type_inner::<true>(
keys_ident,
None,
enum_ident,
translation_unit_enum_ident,
locales,
&keys.0,
&mut KeyPath::new(None),
None,
translations_uri,
options,
&docs,
gen_docs,
)
}
}
}
fn gen_keys_doc(docs: &mut String, keys: &BTreeMap<Key, LocaleValue>) -> core::fmt::Result {
use core::fmt::Write;
let mut keys_iter = keys
.iter()
.filter_map(|(key, value)| match value {
LocaleValue::Value { .. } => Some(key),
LocaleValue::Subkeys { .. } => None,
})
.peekable();
if keys_iter.peek().is_some() {
writeln!(docs, "\n## Keys :")?;
for key in keys_iter {
writeln!(docs, "- `{}`", key)?;
}
}
let mut keys_iter = keys
.iter()
.filter_map(|(key, value)| match value {
LocaleValue::Value { .. } => None,
LocaleValue::Subkeys { .. } => Some(key),
})
.peekable();
if keys_iter.peek().is_some() {
writeln!(docs, "## Subkeys :")?;
for key in keys_iter {
writeln!(docs, "- `{}`", key)?;
}
}
Ok(())
}