Scoping
If you are using subkeys or namespaces, access keys can get pretty big and repetitive, wouldn't it be nice to scope a context to a namespace or subkeys?
Well, this page explains how to do it!
The scope_i18n! Macro
Using namespaces and subkeys can make things quite cumbersome very fast. Imagine you have this:
let i18n = use_i18n();
t!(i18n, namespace.subkeys.value);
t!(i18n, namespace.subkeys.more_subkeys.subvalue);
t!(i18n, namespace.subkeys.more_subkeys.another_subvalue);
This only uses namespace.subkeys.*, but we have to repeat it everywhere.
Here comes the scope_i18n! macro. You can rewrite it to:
let i18n = use_i18n();
let i18n = scope_i18n!(i18n, namespace.subkeys);
t!(i18n, value);
t!(i18n, more_subkeys.subvalue);
t!(i18n, more_subkeys.another_subvalue);
This macro can be chained:
let i18n = use_i18n();
let i18n = scope_i18n!(i18n, namespace);
let i18n = scope_i18n!(i18n, subkeys);
t!(i18n, value);
let i18n = scope_i18n!(i18n, more_subkeys);
t!(i18n, subvalue);
t!(i18n, another_subvalue);
The use_i18n_scoped! Macro
In the above example, we do let i18n = use_i18n(); but only access the context to scope it afterward. We could do
let i18n = scope_i18n!(use_i18n(), namespace.subkeys);
Well, this is what the use_i18n_scoped! macro is for:
let i18n = use_i18n_scoped!(namespace.subkeys);
t!(i18n, value);
t!(i18n, more_subkeys.subvalue);
t!(i18n, more_subkeys.another_subvalue);
The scope_locale! Macro
The above examples are to scope a context, but maybe you use td! a lot and run into the same problems:
fn foo(locale: Locale) {
td!(locale, namespace.subkeys.value);
td!(locale, namespace.subkeys.more_subkeys.subvalue);
td!(locale, namespace.subkeys.more_subkeys.another_subvalue);
}
You can use the scope_locale! macro here:
fn foo(locale: Locale) {
let locale = scope_locale!(locale, namespace.subkeys);
td!(locale, value);
td!(locale, more_subkeys.subvalue);
td!(locale, more_subkeys.another_subvalue);
}
And again, it is chainable:
fn foo(locale: Locale) {
let locale = scope_locale!(locale, namespace.subkeys);
td!(locale, value);
let locale = scope_locale!(locale, more_subkeys);
td!(locale, subvalue);
td!(locale, another_subvalue);
}
The define_scope! Macro
Allow to define a marker type that represent a scope, it takes a path to the i18n module and the key path:
type SubkeysScope = define_scope!(crate::i18n, namespace.subkeys);
fn foo(locale: Locale) {
let locale = locale.scope::<SubkeysScope>();
td!(locale, value);
let locale = scope_locale!(locale, more_subkeys);
td!(locale, subvalue);
td!(locale, another_subvalue);
}
fn bar() -> impl IntoView {
let i18n = use_i18n_scoped::<SubkeysScope>();
// equivalent to `use_i18n().scope::<SubkeysScope>()`
t!(i18n, value)
}
Unfortunately, can't be extended (yet ?).
This is usefull when you have namespaces/subkeys for large parts of your frontend, you have types to reuse:
type SubkeysScope = define_scope!(crate::i18n, namespace.subkeys);
type SubkeysCtx = I18nContext<Locale, SubkeysScope>;
let i18n: SubkeysCtx = use_i18n_scoped();
Also allow const scoping of Locale, still quite cumbersome though:
use leptos_i18n::ConstLocale;
const FR_SUBKEYS: ConstLocale<Locale, SubkeysScope> = ConstLocale::new(Locale::fr);
How it works (for nerds only)
Unfortunately, it looks too good to be true... What's the catch? Where is the tradeoff?
To make this possible, it uses a typestate pattern, but some of the types are hard to access as a user as they are defined deep in the generated i18n module.
This makes it difficult to write the type of a scoped context or a scoped locale.
By default, I18nContext<L, S> is only generic over L because the S scope is the "default" one provided by L, so you can easily write I18nContext<Locale>.
But once you scope it, the S parameters will look like i18n::namespaces::ns_namespace::subkeys::sk_subkeys::subkeys_subkeys.
Yes. This is the path to the struct holding the keys of namespace.subkeys.
This makes it difficult to pass a scoped type around, as it would require writing I18nContext<Locale, i18n::namespaces::ns_namespace::subkeys::sk_subkeys::subkeys_subkeys>.
That's the type the define_scope! macro returns, it recreate the path and retrieve the underlying type.
If you look at the generated code for use_i18n_scoped! you will see something like this:
let i18n = { leptos_i18n::__private::scope_ctx_util(use_i18n(), |_k| &_k.$keys) };
Hmm, what is this closure for? It’s just here for type inference and key checking! The function parameter is even _:fn(&OS) -> &NS, it's never used.
The function is even const (not for scope_locale! though).
But being a typestate, using it or not actually results in the same code path. And with how aggressive Rust is with inlining small functions, it probably compiles to the exact same thing.