npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@solid-primitives/i18n

v2.1.1

Published

Library of primitives for providing internationalization support.

Downloads

16,161

Readme

@solid-primitives/i18n

turborepo size size stage

Library of primitives for providing internationalization support.

Installation

npm i @solid-primitives/i18n
# or
pnpm add @solid-primitives/i18n
# or
yarn add @solid-primitives/i18n

How to use it

The library consists of multiple small and composable primitives that can be used together to create an internationalization solution that fits your needs.

Defining dictionaries

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",
  },
};

With 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>
  );
};

With initial dictionary

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

With transitions

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>
);

Static dictionaries

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

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!'

Modules

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 our 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'

Nested objects syntax

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"

Demo

Live example | Source code

the i18n package is also being used in solidjs.com, you can see the source code here

Changelog

See CHANGELOG.md