Skip to content

Commit

Permalink
Add support for multiple loaders in a single leptos_fluent! macro (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
mscofield0 authored Jun 20, 2024
1 parent 40db226 commit 437aced
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 28 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

## Unreleased - [0.1.0]

### Breaking changes

- The parameter `translations` of the `leptos_fluent!` macro must now be
an array of translations. Replace `translations: TRANSLATIONS` by
`translations: [TRANSLATIONS]`.

### New features

- Add `sync_html_tag_dir` parameter to `leptos_fluent!` macro to sync the `dir`
global attribute of the `<html>` tag with the current language direction.
- Multiple translations can be passed to the `leptos_fluent!` macro.

## 2024-06-16 - [0.0.37]

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ fn App() -> impl IntoView {
// Path to the locales directory, relative to Cargo.toml file.
locales: "./locales",
// Static translations struct provided by fluent-templates.
translations: TRANSLATIONS,
translations: [TRANSLATIONS],
// Check translations correctness in the specified files.
check_translations: "./src/**/*.rs",

Expand Down
2 changes: 1 addition & 1 deletion end2end/tests/cookie.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const COOKIE_NAME: &str = "my-weird-cookie-name";
#[component]
pub fn App() -> impl IntoView {
leptos_fluent! {{
translations: TRANSLATIONS,
translations: [TRANSLATIONS],
locales: "../examples/csr-minimal/locales",
initial_language_from_cookie: true,
cookie_name: COOKIE_NAME,
Expand Down
2 changes: 1 addition & 1 deletion end2end/tests/localstorage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const LOCALSTORAGE_KEY: &str = "foobarbaz";
#[component]
pub fn App() -> impl IntoView {
leptos_fluent! {{
translations: TRANSLATIONS,
translations: [TRANSLATIONS],
locales: "../examples/csr-minimal/locales",
initial_language_from_localstorage: true,
localstorage_key: LOCALSTORAGE_KEY,
Expand Down
2 changes: 1 addition & 1 deletion end2end/tests/url_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const URL_PARAM: &str = "my-weird-url-param";
#[component]
pub fn App() -> impl IntoView {
leptos_fluent! {{
translations: TRANSLATIONS,
translations: [TRANSLATIONS],
locales: "../examples/csr-minimal/locales",
initial_language_from_url_param: true,
url_param: URL_PARAM,
Expand Down
2 changes: 1 addition & 1 deletion end2end/tests/url_param_to_localstorage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const LOCALSTORAGE_KEY: &str = "my-weird-localstorage-key";
#[component]
pub fn App() -> impl IntoView {
leptos_fluent! {{
translations: TRANSLATIONS,
translations: [TRANSLATIONS],
locales: "../examples/csr-minimal/locales",
initial_language_from_url_param: true,
url_param: URL_PARAM,
Expand Down
2 changes: 1 addition & 1 deletion examples/csr-complete/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ static_loader! {
#[component]
pub fn App() -> impl IntoView {
leptos_fluent! {{
translations: TRANSLATIONS,
translations: [TRANSLATIONS],
languages: "./locales/languages.json",
locales: "./locales",
check_translations: "./src/**/*.rs",
Expand Down
2 changes: 1 addition & 1 deletion examples/csr-minimal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ static_loader! {
#[component]
pub fn App() -> impl IntoView {
leptos_fluent! {{
translations: TRANSLATIONS,
translations: [TRANSLATIONS],
locales: "./locales",
check_translations: "./src/**/*.rs",
}};
Expand Down
2 changes: 1 addition & 1 deletion examples/ssr-hydrate-actix/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ static_loader! {
pub fn App() -> impl IntoView {
provide_meta_context();
leptos_fluent! {{
translations: TRANSLATIONS,
translations: [TRANSLATIONS],
locales: "./locales",
check_translations: "./src/**/*.rs",
sync_html_tag_lang: true,
Expand Down
7 changes: 5 additions & 2 deletions examples/ssr-hydrate-axum/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::error_template::{AppError, ErrorTemplate};
use fluent_templates::once_cell::sync::Lazy;
use fluent_templates::static_loader;
use fluent_templates::StaticLoader;
use leptos::*;
use leptos_fluent::{expect_i18n, leptos_fluent, move_tr, tr};
use leptos_meta::*;
Expand All @@ -12,12 +14,13 @@ static_loader! {
};
}

pub static COMPOUND: &[&Lazy<StaticLoader>] = &[&TRANSLATIONS, &TRANSLATIONS];

#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
leptos_fluent! {{
translations: TRANSLATIONS,
languages: "./locales/languages.yaml",
translations: [TRANSLATIONS, TRANSLATIONS] + COMPOUND,
locales: "./locales",
check_translations: "./src/**/*.rs",
sync_html_tag_lang: true,
Expand Down
22 changes: 19 additions & 3 deletions leptos-fluent-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use quote::quote;
/// #[component]
/// pub fn App() -> impl IntoView {
/// leptos_fluent! {{
/// translations: TRANSLATIONS,
/// translations: [TRANSLATIONS],
/// languages: "./locales/languages.json",
/// sync_html_tag_lang: true,
/// sync_html_tag_dir: true,
Expand Down Expand Up @@ -189,7 +189,7 @@ pub fn leptos_fluent(
) -> proc_macro::TokenStream {
#[allow(unused_variables)]
let I18nLoader {
translations_ident,
translations,
languages,
languages_path,
sync_html_tag_lang_bool,
Expand Down Expand Up @@ -855,6 +855,22 @@ pub fn leptos_fluent(
}
};

let translations = {
let loader::Translations { simple, compound } = translations;

let quote = quote! {{
let mut all_loaders = Vec::new();
all_loaders.extend([#(& #simple),*]);
#(
all_loaders.extend(#compound.iter());
);*

all_loaders
}};

quote
};

let quote = quote! {
{
const LANGUAGES: [&::leptos_fluent::Language; #n_languages] = #languages_quote;
Expand All @@ -871,7 +887,7 @@ pub fn leptos_fluent(
let mut i18n = ::leptos_fluent::I18n {
language: ::leptos::create_rw_signal(initial_lang),
languages: &LANGUAGES,
translations: &#translations_ident,
translations: ::leptos::Signal::derive(|| #translations),
};
provide_context::<::leptos_fluent::I18n>(i18n);
#sync_html_tag_lang_quote
Expand Down
96 changes: 91 additions & 5 deletions leptos-fluent-macros/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,97 @@ fn parse_litbool_or_expr_param(
}
}

/// A syntax part consisting of a list of simple loaders.
///
/// e.g. `[loader1, loader2]`
///
/// # Note
/// Must not contain a [`Compound`] loader.
pub(crate) struct Simple(pub(crate) Vec<syn::Path>);

impl Parse for Simple {
fn parse(input: ParseStream) -> Result<Self> {
let bracketed;
syn::bracketed!(bracketed in input);

let list =
bracketed.parse_terminated(syn::Path::parse, syn::Token![,])?;
Ok(Self(list.into_iter().collect()))
}
}

/// A syntax part consisting of a group of loaders passed as one.
///
/// Used to pack loaders and export them from a crate for example.
pub(crate) struct Compound(pub(crate) syn::Path);

impl Parse for Compound {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Self(input.parse()?))
}
}

/// Either a [`List`] or a [`Compound`].
pub(crate) enum SimpleOrCompound {
Simple(Simple),
Compound(Compound),
}

impl Parse for SimpleOrCompound {
fn parse(input: ParseStream) -> Result<Self> {
if let Ok(list) = Simple::parse(input) {
Ok(Self::Simple(list))
} else if let Ok(compound) = Compound::parse(input) {
Ok(Self::Compound(compound))
} else {
Err(syn::Error::new(
input.span(),
"need to pass either a list of loaders or a compound loader",
))
}
}
}

/// A collection of loaders (both simple and compound ones) to use
/// for translating.
pub(crate) struct Translations {
pub(crate) simple: Vec<syn::Path>,
pub(crate) compound: Vec<syn::Path>,
}

impl Parse for Translations {
fn parse(input: ParseStream) -> Result<Self> {
// example of input
// [loader1, loader2] + loaders1 + loaders2 + [loader3]
let mut simple = Vec::new();
let mut compound = Vec::new();

let loaders = syn::punctuated::Punctuated::<
SimpleOrCompound,
syn::Token![+],
>::parse_separated_nonempty(input)?;
for loader in loaders.into_iter() {
match loader {
SimpleOrCompound::Simple(x) => {
for loader in x.0.into_iter() {
simple.push(loader)
}
}
SimpleOrCompound::Compound(compound_loader) => {
compound.push(compound_loader.0)
}
}
}

Ok(Self { simple, compound })
}
}

pub(crate) struct I18nLoader {
pub(crate) languages: Vec<(String, String, String)>,
pub(crate) languages_path: Option<String>,
pub(crate) core_locales_path: Option<String>,
pub(crate) translations_ident: syn::Ident,
pub(crate) translations: Translations,
pub(crate) sync_html_tag_lang_bool: Option<syn::LitBool>,
pub(crate) sync_html_tag_lang_expr: Option<syn::Expr>,
pub(crate) sync_html_tag_dir_bool: Option<syn::LitBool>,
Expand Down Expand Up @@ -125,7 +211,7 @@ impl Parse for I18nLoader {
let mut locales_path: Option<syn::LitStr> = None;
let mut languages_path: Option<syn::LitStr> = None;
let mut core_locales_path: Option<syn::LitStr> = None;
let mut translations_identifier: Option<syn::Ident> = None;
let mut translations: Option<Translations> = None;
#[cfg(not(feature = "ssr"))]
let mut check_translations: Option<syn::LitStr> = None;
let mut sync_html_tag_lang_bool: Option<syn::LitBool> = None;
Expand Down Expand Up @@ -176,7 +262,7 @@ impl Parse for I18nLoader {
fields.parse::<syn::Token![:]>()?;

if k == "translations" {
translations_identifier = Some(fields.parse()?);
translations = Some(fields.parse()?);
} else if k == "locales" {
locales_path = Some(fields.parse()?);
} else if k == "core_locales" {
Expand Down Expand Up @@ -342,7 +428,7 @@ impl Parse for I18nLoader {
}

// translations
let translations_ident = translations_identifier.ok_or_else(|| {
let translations = translations.ok_or_else(|| {
syn::Error::new(input.span(), "Missing `translations` field")
})?;

Expand Down Expand Up @@ -492,7 +578,7 @@ impl Parse for I18nLoader {
}

Ok(Self {
translations_ident,
translations,
languages,
languages_path: languages_file_path,
sync_html_tag_lang_bool,
Expand Down
2 changes: 1 addition & 1 deletion leptos-fluent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ fn App() -> impl IntoView {
// Path to the locales directory, relative to Cargo.toml file.
locales: "./locales",
// Static translations struct provided by fluent-templates.
translations: TRANSLATIONS,
translations: [TRANSLATIONS],
// Check translations correctness in the specified files.
check_translations: "./src/**/*.rs",

Expand Down
41 changes: 32 additions & 9 deletions leptos-fluent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
//! // Path to the locales directory, relative to Cargo.toml file.
//! locales: "./locales",
//! // Static translations struct provided by fluent-templates.
//! translations: TRANSLATIONS,
//! translations: [TRANSLATIONS],
//! // Check translations correctness in the specified files.
//! check_translations: "./src/**/*.rs",
//!
Expand Down Expand Up @@ -236,7 +236,10 @@ use fluent_templates::{
fluent_bundle::FluentValue, loader::Loader, once_cell::sync::Lazy,
LanguageIdentifier, StaticLoader,
};
use leptos::{use_context, Attribute, IntoAttribute, Oco, RwSignal, SignalGet};
use leptos::{
use_context, Attribute, IntoAttribute, Oco, RwSignal, Signal, SignalGet,
SignalWith,
};
pub use leptos_fluent_macros::leptos_fluent;

#[doc(hidden)]
Expand Down Expand Up @@ -347,17 +350,14 @@ impl IntoAttribute for &&'static Language {
/// This context is used to provide the current language, the available languages
/// and all the translations. It is capable of doing what is needed to translate
/// and manage translations in a whole application.
///
/// If you need to separate the translations of different parts of the application,
/// you can wrap this context in another struct and provide it to Leptos as a context.
#[derive(Clone, Copy)]
pub struct I18n {
/// Signal that holds the current language.
pub language: RwSignal<&'static Language>,
/// Available languages for the application.
pub languages: &'static [&'static Language],
/// Static translations loader of fluent-templates.
pub translations: &'static Lazy<StaticLoader>,
pub translations: Signal<Vec<&'static Lazy<StaticLoader>>>,
}

#[cfg(debug_assertions)]
Expand Down Expand Up @@ -395,7 +395,18 @@ pub fn i18n() -> I18n {
#[doc(hidden)]
pub fn tr_impl(text_id: &str) -> String {
let i18n = expect_i18n();
i18n.translations.lookup(&i18n.language.get().id, text_id)
let lang_id = i18n.language.get().id.clone();
let found = i18n.translations.with(|translations| {
for tr in translations {
if let Some(result) = tr.try_lookup(&lang_id, text_id) {
return Some(result);
}
}

None
});

found.unwrap_or("Unknown localization {text_id}".to_string())
}

#[doc(hidden)]
Expand All @@ -404,8 +415,20 @@ pub fn tr_with_args_impl(
args: &std::collections::HashMap<String, FluentValue>,
) -> String {
let i18n = expect_i18n();
i18n.translations
.lookup_with_args(&i18n.language.get().id, text_id, args)
let lang_id = i18n.language.get().id.clone();
let found = i18n.translations.with(|translations| {
for tr in translations {
if let Some(result) =
tr.try_lookup_with_args(&lang_id, text_id, args)
{
return Some(result);
}
}

None
});

found.unwrap_or("Unknown localization {text_id}".to_string())
}

/// Translate a text identifier to the current language.
Expand Down

0 comments on commit 437aced

Please sign in to comment.