aboutsummaryrefslogtreecommitdiff
path: root/shakehand/src/shakehand.rs
diff options
context:
space:
mode:
Diffstat (limited to 'shakehand/src/shakehand.rs')
-rw-r--r--shakehand/src/shakehand.rs425
1 files changed, 425 insertions, 0 deletions
diff --git a/shakehand/src/shakehand.rs b/shakehand/src/shakehand.rs
new file mode 100644
index 0000000..3e73b09
--- /dev/null
+++ b/shakehand/src/shakehand.rs
@@ -0,0 +1,425 @@
+use proc_macro2::{Ident, TokenStream as TokenStream2};
+use quote::{format_ident, quote};
+use std::collections::{BTreeMap, BTreeSet};
+
+use crate::analyzer::{
+ TomlFile, TranslationEntry, extract_params, key_to_ident, lang_to_variant, path_to_mod_name,
+ replace_params_with_format,
+};
+
+/// Generate the language enum (and functions `lang()` / `set_lang()`)
+fn generate_languages_enum(
+ all_languages: &BTreeSet<String>,
+ fallback: &str,
+ locale_path: &str,
+) -> TokenStream2 {
+ let variants_info: Vec<(Ident, String)> = all_languages
+ .iter()
+ .map(|lang| {
+ let name = lang_to_variant(lang);
+ (format_ident!("{}", name), lang.clone())
+ })
+ .collect();
+
+ let enum_doc = format!("All language files present under \"{}\"", locale_path);
+
+ if variants_info.is_empty() {
+ return quote! {
+ /// This constant stores the discriminant of the current language variant.
+ /// It is initialized at program start by reading the locale or a config file.
+ pub static __SHAKE_HAND_LANG: std::sync::atomic::AtomicU8 =
+ std::sync::atomic::AtomicU8::new(0u8);
+
+ #[derive(Debug, Default, Clone, Copy)]
+ #[repr(u8)]
+ #[doc = #enum_doc]
+ pub enum Languages {}
+
+ #[inline(always)]
+ /// Get the current language from the global static variable
+ pub fn lang() -> Languages {
+ panic!("shakehand: no locale files found")
+ }
+
+ #[inline(always)]
+ /// Set the current language in the global static variable
+ pub fn set_lang(_lang: Languages) {
+ panic!("shakehand: no locale files found")
+ }
+ };
+ }
+
+ let fallback_idx = all_languages
+ .iter()
+ .position(|l| l == fallback)
+ .unwrap_or(0);
+
+ let fallback_ident = &variants_info[fallback_idx].0;
+
+ // List of enum variants with doc comments, fallback variant gets #[default]
+ let enum_variants: Vec<TokenStream2> = variants_info
+ .iter()
+ .enumerate()
+ .map(|(i, (ident, raw))| {
+ if i == fallback_idx {
+ quote! {
+ #[doc = #raw]
+ #[default]
+ #ident,
+ }
+ } else {
+ quote! {
+ #[doc = #raw]
+ #ident,
+ }
+ }
+ })
+ .collect();
+
+ // lang() match arms (match u8 values as returned by AtomicU8::load)
+
+ let lang_match_arms: Vec<TokenStream2> = variants_info
+ .iter()
+ .enumerate()
+ .map(|(i, (ident, _))| {
+ let idx = i as u8;
+ quote! { #idx => Languages::#ident, }
+ })
+ .collect();
+
+ quote! {
+ /// This constant stores the discriminant of the current language variant.
+ /// It is initialized at program start by reading the locale or a config file.
+ pub static __SHAKE_HAND_LANG: std::sync::atomic::AtomicU8 =
+ std::sync::atomic::AtomicU8::new(#fallback_idx as u8);
+
+ #[derive(Debug, Default, Clone, Copy)]
+ #[repr(u8)]
+ #[allow(non_camel_case_types)]
+ #[doc = #enum_doc]
+ pub enum Languages {
+ #(#enum_variants)*
+ }
+
+ /// Get the current language from the global static variable
+ #[inline(always)]
+ pub fn lang() -> Languages {
+ match __SHAKE_HAND_LANG.load(std::sync::atomic::Ordering::Relaxed) {
+ #(#lang_match_arms)*
+ _ => Languages::#fallback_ident,
+ }
+ }
+
+ /// Set the current language in the global static variable
+ #[inline(always)]
+ pub fn set_lang(lang: Languages) {
+ __SHAKE_HAND_LANG.store(lang as u8, std::sync::atomic::Ordering::Relaxed);
+ }
+ }
+}
+
+/// Generate a `format!(fmt_str, args…)` expression for a value that has parameters
+fn make_format_expr(value: &str) -> TokenStream2 {
+ let fmt_str = replace_params_with_format(value);
+ let lang_params = extract_params(value);
+ let format_args: Vec<TokenStream2> = lang_params
+ .iter()
+ .map(|p| {
+ let var = format_ident!("{}", just_fmt::snake_case!(p));
+ // 取出 .as_ref() 后的变量值
+ quote! { #var }
+ })
+ .collect();
+ quote! { format!(#fmt_str, #(#format_args),*) }
+}
+
+/// Generate match arms for a single entry (arms for languages with values) and a `_ =>` catch-all (fallback)
+fn make_match_arms(
+ entry: &TranslationEntry,
+ all_available: &BTreeSet<String>,
+ fallback: &str,
+) -> (Vec<TokenStream2>, TokenStream2) {
+ let mut arms: Vec<TokenStream2> = Vec::new();
+ let mut found_fallback = false;
+
+ let mut fallback_arm = if entry.has_params {
+ quote! { _ => ::std::string::String::new(), }
+ } else {
+ quote! { _ => "", }
+ };
+
+ for lang in all_available {
+ let value = entry.values.get(lang.as_str());
+ let variant_name = format_ident!("{}", lang_to_variant(lang));
+ let is_fallback = lang == fallback;
+
+ match value {
+ Some(v) if entry.has_params => {
+ let body = make_format_expr(v);
+ let arm = quote! { Languages::#variant_name => #body, };
+ if is_fallback {
+ found_fallback = true;
+ fallback_arm = quote! { _ => #body, };
+ }
+ arms.push(arm);
+ }
+ Some(v) => {
+ let arm = quote! { Languages::#variant_name => #v, };
+ if is_fallback {
+ found_fallback = true;
+ fallback_arm = quote! { _ => #v, };
+ }
+ arms.push(arm);
+ }
+ None => {}
+ }
+ }
+
+ // When the fallback language doesn't have a value for this key, use the first available language as a catch-all
+ if !found_fallback && let Some(first_val) = entry.values.values().next() {
+ if entry.has_params {
+ let body = make_format_expr(first_val);
+ fallback_arm = quote! { _ => #body, };
+ } else {
+ fallback_arm = quote! { _ => #first_val, };
+ }
+ }
+
+ (arms, fallback_arm)
+}
+
+/// Generate a method for a single translation entry
+fn generate_entry_method(
+ entry: &TranslationEntry,
+ all_languages: &BTreeSet<String>,
+ fallback: &str,
+) -> TokenStream2 {
+ let method_name = format_ident!("{}", key_to_ident(&entry.key));
+ let key_str = format!("Key \"{}\"", entry.key);
+
+ // Doc table showing each language's value for this key
+ let mut lang_rows: Vec<TokenStream2> = Vec::new();
+ // Table header
+ lang_rows.push(quote! { #[doc = "|Language|Value|"] });
+ lang_rows.push(quote! { #[doc = "|-|-|"] });
+ for lang in all_languages.iter() {
+ let val = entry
+ .values
+ .get(lang.as_str())
+ .map(|s| s.as_str())
+ .unwrap_or("(NO TRANSLATION)");
+ let row = format!("|**{}**|*\"{}\"*|", lang, val);
+ lang_rows.push(quote! { #[doc = #row] });
+ }
+ let lang_docs = lang_rows;
+
+ // Parameter name conflict: compile error + deprecated function
+ if entry.params_conflict {
+ let err_msg = format!(
+ "shakehand: key `{}` has inconsistent parameter names across languages",
+ entry.key,
+ );
+ let panic_msg = format!(
+ "shakehand: key `{}` has inconsistent parameter names across languages, fix the .toml file",
+ entry.key,
+ );
+ return quote! {
+ ::core::compile_error!(#err_msg);
+
+ #[deprecated(note = "parameter mismatch across languages, fix the .toml file")]
+ #[doc = #key_str]
+ ///
+ #(#lang_docs)*
+ #[must_use]
+ pub fn #method_name () -> ! {
+ panic!(#panic_msg)
+ }
+ };
+ }
+
+ // Only generate match arms for languages that have a translation; missing ones fall through to `_ =>`
+ let (match_arms, catch_all) =
+ make_match_arms(entry, &entry.values.keys().cloned().collect(), fallback);
+
+ if entry.has_params {
+ let params_with_type: Vec<TokenStream2> = entry
+ .params
+ .iter()
+ .map(|p| {
+ let name = format_ident!("{}", just_fmt::snake_case!(p));
+ quote! { #name: impl AsRef<str> }
+ })
+ .collect();
+
+ let param_bindings: Vec<TokenStream2> = entry
+ .params
+ .iter()
+ .map(|p| {
+ let name = format_ident!("{}", just_fmt::snake_case!(p));
+ quote! { let #name = #name.as_ref(); }
+ })
+ .collect();
+
+ let param_docs: Vec<TokenStream2> = entry
+ .params
+ .iter()
+ .map(|p| {
+ let doc = format!("- `{}`", p);
+ quote! { #[doc = #doc] }
+ })
+ .collect();
+
+ quote! {
+ #[inline(always)]
+ #[doc = #key_str]
+ ///
+ #(#lang_docs)*
+ ///
+ /// # Parameters
+ #(#param_docs)*
+ #[must_use]
+ pub fn #method_name (#(#params_with_type),*) -> String {
+ #(#param_bindings)*
+ match lang() {
+ #(#match_arms)*
+ #catch_all
+ }
+ }
+ }
+ } else {
+ quote! {
+ #[inline(always)]
+ #[doc = #key_str]
+ ///
+ #(#lang_docs)*
+ #[must_use]
+ pub fn #method_name () -> &'static str {
+ match lang() {
+ #(#match_arms)*
+ #catch_all
+ }
+ }
+ }
+ }
+}
+
+/// Generate a struct and its impl block for a single toml file
+fn generate_struct(
+ toml_file: &TomlFile,
+ all_languages: &BTreeSet<String>,
+ locale_path: &str,
+ fallback: &str,
+) -> TokenStream2 {
+ let struct_name = format_ident!("{}", toml_file.struct_name);
+ let methods: Vec<TokenStream2> = toml_file
+ .entries
+ .iter()
+ .map(|entry| generate_entry_method(entry, all_languages, fallback))
+ .collect();
+
+ let struct_name_str = toml_file.struct_name.as_str();
+
+ // Count how many keys each language has, for the table
+ let mut lang_counts: Vec<(String, usize)> = all_languages
+ .iter()
+ .map(|lang| {
+ let count = toml_file
+ .entries
+ .iter()
+ .filter(|e| e.values.contains_key(lang.as_str()))
+ .count();
+ (lang.clone(), count)
+ })
+ .collect();
+ lang_counts.sort_by(|a, b| a.1.cmp(&b.1).reverse());
+
+ // Table rows
+ let mut count_rows: Vec<TokenStream2> = Vec::new();
+ count_rows.push(quote! { #[doc = "|Language|Count|"] });
+ count_rows.push(quote! { #[doc = "|-|-|"] });
+ for (lang, count) in &lang_counts {
+ let row = format!("|**{}**|{}|", lang, count);
+ count_rows.push(quote! { #[doc = #row] });
+ }
+
+ let path_doc = format!(
+ "Language information from file `{}/{}.toml`",
+ locale_path, struct_name_str
+ );
+
+ quote! {
+ #[doc = concat!("# ", #struct_name_str)]
+ ///
+ #[doc = #path_doc]
+ ///
+ #(#count_rows)*
+ pub struct #struct_name;
+
+ impl #struct_name {
+ #(#methods)*
+ }
+ }
+}
+
+/// Generate the complete module code
+pub fn generate_module(
+ files: Vec<TomlFile>,
+ all_languages: BTreeSet<String>,
+ fallback: String,
+ locale_path: &str,
+) -> TokenStream2 {
+ let lang_enum = generate_languages_enum(&all_languages, &fallback, locale_path);
+
+ // Group by module path
+ let mut root_files: Vec<&TomlFile> = Vec::new();
+ let mut sub_modules: BTreeMap<String, Vec<&TomlFile>> = BTreeMap::new();
+
+ for f in &files {
+ if f.module_path.is_empty() {
+ root_files.push(f);
+ } else {
+ let mod_name = f.module_path[0].clone();
+ sub_modules.entry(mod_name).or_default().push(f);
+ }
+ }
+
+ // Generate root-level structs
+ let root_structs: Vec<TokenStream2> = root_files
+ .iter()
+ .map(|f| generate_struct(f, &all_languages, locale_path, &fallback))
+ .collect();
+
+ // Generate sub-modules
+ let sub_mods: Vec<TokenStream2> = sub_modules
+ .iter()
+ .map(|(mod_name, mod_files): (&String, &Vec<&TomlFile>)| {
+ let mod_ident = format_ident!("{}", path_to_mod_name(mod_name));
+ let sub_structs: Vec<TokenStream2> = mod_files
+ .iter()
+ .map(|f| {
+ let fixed_file = TomlFile {
+ module_path: f.module_path[1..].to_vec(),
+ struct_name: f.struct_name.clone(),
+ entries: f.entries.clone(),
+ all_languages: f.all_languages.clone(),
+ };
+ generate_struct(&fixed_file, &all_languages, locale_path, &fallback)
+ })
+ .collect();
+
+ quote! {
+ pub mod #mod_ident {
+ #(#sub_structs)*
+ }
+ }
+ })
+ .collect();
+
+ quote! {
+ #lang_enum
+
+ #(#root_structs)*
+
+ #(#sub_mods)*
+ }
+}