![require(esm) Backported to Node.js 20, Paving the Way for ESM-Only Packages](https://cdn.sanity.io/images/cgdhsj6q/production/be8ab80c8efa5907bc341c6fefe9aa20d239d890-1600x1097.png?w=400&fit=max&auto=format)
Security News
require(esm) Backported to Node.js 20, Paving the Way for ESM-Only Packages
require(esm) backported to Node.js 20, easing the transition to ESM-only packages and reducing complexity for developers as Node 18 nears end-of-life.
@solid-primitives/i18n
Advanced tools
Library of primitives for providing internationalization support.
Library of primitives for providing internationalization support.
npm i @solid-primitives/i18n
# or
pnpm add @solid-primitives/i18n
# or
yarn add @solid-primitives/i18n
The library consists of multiple small and composable primitives that can be used together to create an internationalization solution that fits your needs.
Dictionary is any plain js object that contains translations for a given language. It can be nested and contain functions.
Dictionaries can be defined in inline in js, or imported from json files.
const en_dict = {
hello: "hello {{ name }}, how are you?",
goodbye: (name: string) => `goodbye ${name}`,
food: {
meat: "meat",
fruit: "fruit",
},
};
type Dict = typeof en_dict;
const fr_dict: Dict = {
hello: "bonjour {{ name }}, comment vas-tu ?",
goodbye: (name: string) => `au revoir ${name}`,
food: {
meat: "viande",
fruit: "fruit",
},
};
createResource
Example of using @solid-primitives/i18n
with createResource
to dynamically load directories for selected languages.
import * as i18n from "@solid-primitives/i18n";
/*
Assuming the dictionaries are in the following structure:
./i18n
en.ts
fr.ts
es.ts
And all exports a `dict` object
*/
// use `type` to not include the actual dictionary in the bundle
import type * as en from "./i18n/en.js";
export type Locale = "en" | "fr" | "es";
export type RawDictionary = typeof en.dict;
export type Dictionary = i18n.Flatten<RawDictionary>;
async function fetchDictionary(locale: Locale): Promise<Dictionary> {
const dict: RawDictionary = (await import(`./i18n/${locale}.ts`)).dict;
return i18n.flatten(dict); // flatten the dictionary to make all nested keys available top-level
}
const App: Component = () => {
const [locale, setLocale] = createSignal<Locale>("en");
const [dict] = createResource(locale, fetchDictionary);
dict(); // => Dictionary | undefined
// (undefined when the dictionary is not loaded yet)
const t = i18n.translator(dict);
t("hello"); // => string | undefined
return (
<Suspense>
<Show when={dict()}>
{dict => {
dict(); // => Dictionary (narrowed by Show)
const t = i18n.translator(dict);
t("hello"); // => string
return (
<div>
<p>Current locale: {locale()}</p>
<div>
<button onClick={() => setLocale("en")}>English</button>
<button onClick={() => setLocale("fr")}>French</button>
<button onClick={() => setLocale("es")}>Spanish</button>
</div>
<h4>{t("hello", { name: "John" })}</h4>
<h4>{t("goodbye", { name: "John" })}</h4>
<h4>{t("food.meat")}</h4>
</div>
);
}}
</Show>
</Suspense>
);
};
Instead of narrowing the current dictionary with Show
, you can also provide an initial dictionary to createResource
.
// en dictionary will be included in the bundle
import { dict as en_dict } from "./i18n/en.js";
const [dict] = createResource(locale, fetchDictionary, {
initialValue: i18n.flatten(en_dict),
});
dict(); // => Dictionary
Since the dictionary is a resource, you can use solid's transitions when switching the locale.
const [dict] = createResource(locale, fetchDictionary);
const [duringTransition, startTransition] = useTransition();
function switchLocale(locale: Locale) {
startTransition(() => setLocale(locale));
}
return (
<div style={{ opacity: duringTransition() ? 0.5 : 1 }}>
<Suspense>
<App />
</Suspense>
</div>
);
If you don't need to load dictionaries dynamically, you can use createMemo
instead of createResource
.
import * as en from "./i18n/en.js";
import * as fr from "./i18n/fr.js";
import * as es from "./i18n/es.js";
const dictionaries = {
en: en.dict,
fr: fr.dict,
es: es.dict,
};
const [locale, setLocale] = createSignal<Locale>("en");
const dict = createMemo(() => i18n.flatten(dictionaries[locale()]));
const t = i18n.translator(dict);
Templates are strings that can contain placeholders. Placeholders are defined with double curly braces {{ placeholder }}
.
Templates can be resolved by calling resolveTemplate
function. e.g.
i18n.resolveTemplate("hello {{ name }}!", { name: "John" }); // => 'hello John!'
By default, the translator
function will not resolve templates. You can pass resolveTemplate
as the second argument to translator
to enable template resolution. Or use a custom template resolver.
const dict = {
hello: "hello {{ name }}!",
};
const t1 = i18n.translator(() => dict);
t1("hello", { name: "John" }); // => 'hello {{ name }}!'
const t2 = i18n.translator(() => dict, i18n.resolveTemplate);
t2("hello", { name: "John" }); // => 'hello John!'
Splitting the dictionary into multiple modules can be useful when you have a large dictionary and want to avoid loading the entire dictionary at once.
For example if out app had a separate login
and dashboard
modules, we could split the dictionary into 3 modules: (common
, login
and dashboard
).
i18n/
en.json
pl.json
modules/
login/
i18n/
en.json
pl.json
login.ts
...
root.ts
Translations in root.ts
would be available in all modules. Translations in login.ts
would be available only in login
module, and the same for other modules.
// root.ts
const [locale, setLocale] = createSignal<Locale>("en");
const [commonDict] = createResource(locale, fetchCommonDictionary);
const t = i18n.translator(commonDict);
// login/login.ts
const [loginDict] = createResource(locale, fetchLoginDictionary);
// translator only for login module
const loginT = i18n.translator(loginDict);
t("welcome"); // => 'Welcome from common translations!'
loginT("welcome"); // => 'Welcome from login translations!'
Or combine multiple dictionaries into one. While prefixing the keys with the module name.
const combined_dict = createMemo(() => ({
...i18n.prefix(commonDict(), "common"),
...i18n.prefix(loginDict(), "login"),
}));
const t = i18n.translator(combined_dict);
t("common.welcome"); // => 'Welcome from common translations!'
t("login.welcome"); // => 'Welcome from login translations!'
To scope an existing translator to a module, you can use scopedTranslator
.
const dict = {
"login.username": "User name",
"login.password": "Password",
"login.login": "Login",
// ...
};
const t = i18n.translator(() => dict);
const loginT = i18n.scopedTranslator(t, "login");
loginT("username"); // => 'User name'
String paths passesd to the translator don't allow for taking advantage of TypeScript's "Go to definition", and "Find all references", "Rename" features.
If you prefer to use nested objects instead of dot notation, you can use chainedTranslator
helper.
It takes a dictionary (not flattened) to map it's shape and a translator function for resolving the translations.
const dict = {
greetings: {
hello: "hello {{ name }}!",
hi: "hi!",
},
goodbye: (name: string) => `goodbye ${name}!`,
};
const flat_dict = i18n.flatten(dict);
const t = i18n.translator(() => flat_dict, i18n.resolveTemplate);
const chained = i18n.chainedTranslator(dict, t);
chained.greetings.hello({ name: "John" }); // => "hello John!"
chained.greetings.hi(); // => "hi!"
chained.goodbye("John"); // => "goodbye John!"
Alternatively you can use proxyTranslator
that is implemented using new Proxy
so it doesn't require a directory object to be passed as source.
const proxy = i18n.proxyTranslator(t);
proxy.greetings.hello({ name: "John" }); // => "hello John!"
proxy.greetings.hi(); // => "hi!"
proxy.goodbye("John"); // => "goodbye John!"
Using a proxy will have a slight performance impact, so it's recommended to use chainedTranslator
if possible. But it can be useful when you don't have access to the dictionary object. Or want to mock the translations in tests.
const proxy = i18n.proxyTranslator(path => path);
proxy.greetings.hello({ name: "John" }); // => "greetings.hello"
proxy.greetings.hi(); // => "greetings.hi"
proxy.goodbye("John"); // => "goodbye"
the i18n package is also being used in solidjs.com, you can see the source code here
See CHANGELOG.md
FAQs
Library of primitives for providing internationalization support.
The npm package @solid-primitives/i18n receives a total of 3,439 weekly downloads. As such, @solid-primitives/i18n popularity was classified as popular.
We found that @solid-primitives/i18n demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
require(esm) backported to Node.js 20, easing the transition to ESM-only packages and reducing complexity for developers as Node 18 nears end-of-life.
Security News
PyPI now supports iOS and Android wheels, making it easier for Python developers to distribute mobile packages.
Security News
Create React App is officially deprecated due to React 19 issues and lack of maintenance—developers should switch to Vite or other modern alternatives.