Skip to content

A set of reactive state for commonly used features in web apps

License

Notifications You must be signed in to change notification settings

mathieuprog/solid-compose

Repository files navigation

Solid Compose

solid-compose provides a set of reactive state for commonly used features in web apps.

Currently, it includes

Internationalization (i18n)

Solid Compose provides i18n support allowing to build multilingual apps.

First, add your app's translations:

import { addTranslations } from 'solid-compose';

addTranslations('en' {
  "hello": "Hello, {{ name }}!"
});

addTranslations('fr' {
  "hello": "Bonjour, {{ name }} !",
});

// from JSON files
// (make sure to have TS config "resolveJsonModule" set to true)

import enTranslations from './translations/en.json';
import frTranslations from './translations/fr.json';

addTranslations('en', enTranslations);
addTranslations('fr', frTranslations);

Then initialize and configure the locale and i18n global primitives:

import {
  createI18nPrimitive,
  createLocalePrimitive,
  getSupportedLanguageTags
} from 'solid-compose';

createLocalePrimitive({
  // getSupportedLanguageTags() returns the language tags
  // for which translations exist
  supportedLanguageTags: getSupportedLanguageTags()
});

createI18nPrimitive({
  fallbackLanguageTag: 'en'
});

createI18nPrimitive accepts 2 optional configuration params:

  • fallbackLanguageTag: the locale to fallback to if no translation is found for the current locale;
  • keySeparator: allows to have nested translations.
Example using keySeparator
addTranslations('fr', {
  "welcome": {
    "hello": "Bonjour !"
  }
});

createI18nPrimitive({
  fallbackLanguageTag: 'en',
  keySeparator: '.'
});

translate('welcome.hello') // Bonjour !

Translate your app:
import { useI18n, useLocale } from 'solid-compose';

function Hello() {
  const [locale, { setLanguageTag }] = useLocale();
  const translate = useI18n();

  return <>
    <div>{translate('hello', { name: 'John' })}</div>
    <div>Current locale: {locale.languageTag}</div>
    <div>Switch locale: {setLanguageTag('fr')}</div>
  </>;
}

You may also have objects as parameters:

addTranslations('en' {
  "hello": "Hello, {{ user.name }}!"
});

function Hello() {
  const translate = useI18n();

  return <>
    <div>{translate('hello', { user: { name: 'John' }})}</div>
  </>;
}

To translate in a given language rather than the current locale, you may pass the locale as a third argument:

translate('hello', { user: { name: 'John' }}, 'fr')

Pluralization support

Languages have different rules for plurals.

Solid Compose allows you to define a translation per plural rule:

addTranslations('en', {
  "messages": {
    "one": "One message received.",
    "other": "{{ cardinal }} messages received.",
    "zero": "No messages received."
  },
  "position": {
    "one": "{{ ordinal }}st",
    "two": "{{ ordinal }}nd",
    "few": "{{ ordinal }}rd",
    "other": "{{ ordinal }}th",
  }
});

Either a cardinal or ordinal parameter must be present when translating, for the library to pick the right message:

translate('messages', { cardinal: 1 }); // One message received.
translate('position', { ordinal: 1 }); // 1st

Namespaces

Namespaces allow to load only a subset of the available translations, which eases the handling of key collisions in larger apps.

Say for instance that your application is made of multiple sub-apps, you may have a "todo" namespace including the translations for the todo sub-app, a "scheduler" namespace for the scheduler sub-app, etc.

addTranslations optionally accepts as second argument a namespace:

addTranslations('en', {
  // common translations
});

addTranslations('en', 'todo', {
  // translations for the todo app
});

addTranslations('en', 'scheduler', {
  // translations for the scheduler app
});

addTranslations('en', 'time', {
  // translations related to time
});

You may then use the I18nProvider component to scope the translations per namespace:

import { I18nProvider } from 'solid-compose';

render(() =>
  <>
    <I18nProvider namespaces={['todo']}>
      <TodoApp/>
    </I18nProvider>

    <I18nProvider namespaces={['time', 'scheduler']}>
      <SchedulerApp/>
    </I18nProvider>
  </>
);

localStorage (client-side storage)

Solid Compose makes localStorage values reactive:

import { useLocalStorage } from 'solid-compose';

const [value, { set: setValue, remove: removeValue }] =
  useLocalStorage('myKey', 'defaultValue');

useLocalStorage accepts as 3rd argument an object containing the functions serializing and deserializing values to be stored and to be retrieved.

By default, the following object is used:

{
  serialize: JSON.stringify,
  deserialize: JSON.parse
}

Localization (l10n)

Solid Compose stores user locale parameters into a store.

First, initialize and configure the locale primitive:

import { createLocalePrimitive } from 'solid-compose';

createLocalePrimitive({
  supportedLanguageTags: ['en', 'de', 'fr'],
  defaultLanguageTag: 'de'
});

The supportedLanguageTags configuration field is mandatory and specifies which language tags are supported by your application. The defaultLanguageTag field is optional and utilized if the user's preferred language tag is not included in supportedLanguageTags field.

You may then access the locale parameters:

import { useLocale } from 'solid-compose';

const [locale] = useLocale();

console.log(locale.languageTag);
console.log(locale.textDirection);
console.log(locale.numberFormat);
console.log(locale.timeZone);
console.log(locale.dateFormat);
console.log(locale.timeFormat);
console.log(locale.firstDayOfWeek);
console.log(locale.colorScheme);

All of those parameters are reactive.

How is the initial language tag determined?

The library looks for a language tag that is both supported by your application (in supportedLanguageTags configuration) and listed in the user's browser as one of their preferred language tags (in navigator.languages).

If a matching language tag cannot be found, the optional defaultLanguageTag configuration is utilized. If not provided, either an English language tag is used or the first language tag in the list of supported language tags.

When you have information about the user's preferences, you can use it to initialize the locale data:

import { createLocalePrimitive } from 'solid-compose';

createLocalePrimitive({
  supportedLanguageTags: ['en', 'de', 'fr'],
  defaultLanguageTag: 'de',
  initialValues: {
    languageTag: user.languageTag,
    numberFormat: user.numberFormat,
    timeZone: user.timeZone,
    dateFormat: user.dateFormat,
    timeFormat: user.timeFormat,
    firstDayOfWeek: user.firstDayOfWeek,
    colorScheme: user.colorScheme
  }
});

Every field in initialValues is optional and if not provided, the value is inferred from the user's browser and system parameters.

Color scheme (dark, light mode)

Solid Compose provides color scheme toggling (light vs dark mode).

import { createColorSchemeEffect } from 'solid-compose';

createColorSchemeEffect();

The effect includes the color-scheme meta tag, the CSS styling property color-scheme on the html tag, as well as a data attribute "data-color-scheme" on the html tag.

The data attribute enables the selection of CSS selectors based on the color scheme, allowing you to set CSS variables for the current color scheme:

html[data-color-scheme='dark'] {
  --primary-text-color: var(--grey-200);
  --secondary-text-color: var(--grey-500);
}

If you wish to include external files instead of including the theming css in the css bundle, you may also add the ColorSchemeStylesheet component in your app which will pick the right stylesheet according to the current color scheme.

import { ColorSchemeStylesheet } from 'solid-compose';

const App: VoidComponent = () => {
  return (
    <>
      <ColorSchemeStylesheet
        dark="./css/themes/dark-theme.css"
        light="./css/themes/light-theme.css"
      />

      <div></div>
    </>
  );
};

In addition to adding the necessary stylesheets,

setColorScheme allows to switch the color scheme:

import { useLocale } from 'solid-compose';

const [locale, { setColorScheme }] = useLocale();

The initial color scheme is derived from the system or user agent, unless the initialValues include a colorScheme property.

If you intend to incorporate additional themes beyond just the dark and light modes, refer to the Theming.

Language tag

setLanguageTag allows to change the language:

import { useLocale } from 'solid-compose';

const [locale, { setLanguageTag }] = useLocale();

getClosestSupportedLanguageTag allows to find the best language supported by your app:

import { getClosestSupportedLanguageTag } from 'solid-compose';

createLocalePrimitive({ supportedLanguageTags: ['en-US', 'fr'] });
getClosestSupportedLanguageTag('en') // 'en-US'
getClosestSupportedLanguageTag('fr-BE') // 'fr'
getClosestSupportedLanguageTag('es') // 'en-US'

Number format

setNumberFormat allows to change the number format:

import { useLocale } from 'solid-compose';

const [locale, { setNumberFormat }] = useLocale();

formatNumber allows to format a number according to the current locale's number formatting setting.

import { formatNumber, useLocale } from 'solid-compose';

const [locale, { setNumberFormat }] = useLocale();

setNumberFormat(NumberFormat.SpaceComma);

formatNumber(1000.01) // 1 000,01

parseNumber allows to parse a localized number string according to the current locale's number formatting setting.

import { parseNumber, useLocale } from 'solid-compose';

const [locale, { setNumberFormat }] = useLocale();

setNumberFormat(NumberFormat.SpaceComma);

parseNumber('1 000,01') // 1000.01

Date format

setDateFormat allows to change the date format:

import { useLocale } from 'solid-compose';

const [locale, { setDateFormat }] = useLocale();

formatDate allows to format a date according to the current locale's date formatting setting.

import { formatDate, useLocale } from 'solid-compose';

const [locale, { setDateFormat }] = useLocale();

setDateFormat({ endianness: DateEndianness.MiddleEndian });

formatDate(Temporal.PlainDate.from('2000-12-31')) // 12/31/2000

Time format

setTimeFormat allows to change the time format:

import { useLocale } from 'solid-compose';

const [locale, { setTimeFormat }] = useLocale();

formatTime allows to format a time according to the current locale's time formatting setting.

import { formatTime, useLocale } from 'solid-compose';

const [locale, { setTimeFormat }] = useLocale();

setTimeFormat({ is24HourClock: false });

formatTime(Temporal.PlainTime.from('00:30:05'), { precision: 'minute', omitZeroUnits: true }) // 12:30 AM

Time zone

setTimeZone allows to change the time zone:

import { useLocale } from 'solid-compose';

const [locale, { setTimeZone }] = useLocale();

First day of the week

setFirstDayOfWeek allows to change the first day of the week:

import { useLocale } from 'solid-compose';

const [locale, { setFirstDayOfWeek }] = useLocale();

Text direction

createTextDirectionEffect function allows the text direction of the entire page to be changed by setting the dir attribute on the html tag to "ltr" or "rtl" based on the current locale:

import { createTextDirectionEffect } from 'solid-compose';

createTextDirectionEffect();

Theming

Solid Compose provides theming support.

First, initialize and configure the locale primitive (in order to fetch the user's preferred color scheme):

createLocalePrimitive({ supportedLanguageTags: ['en'] });

Then, initialize and configure the theme primitive:

createThemePrimitive({
  themes: [
    {
      name: 'fooTheme',
      colorScheme: ColorScheme.Dark,
      default: true
    },
    {
      name: 'barTheme',
      colorScheme: ColorScheme.Light,
      default: true
    },
    {
      name: 'bazTheme',
      colorScheme: ColorScheme.Dark
    }
  ]
});

The initial theme is selected according to the user's preferred color scheme. You therefore need to specify one default dark theme and one default light theme.

In cases where the theme is based on a color that is not distinctly dark or light, it is still needed to specify a default color scheme in case of missing styles.

When you know the user's preferred theme, you don't need to specify any defaults and instead you may set the user's theme via the initialTheme config:

createThemePrimitive({
  themes: [
    {
      name: 'fooTheme',
      colorScheme: ColorScheme.Dark
    },
    {
      name: 'barTheme',
      colorScheme: ColorScheme.Light
    },
    {
      name: 'bazTheme',
      colorScheme: ColorScheme.Dark
    }
  ],
  initialTheme: 'fooTheme'
});

You may then call the effect:

import { createThemeEffect } from 'solid-compose';

createThemeEffect();

The effect includes the color-scheme meta tag, the CSS styling property color-scheme on the html tag, as well as the data attributes "data-color-scheme" and "data-theme" on the html tag.

The data attributes enable the selection of CSS selectors based on the selected theme or current color scheme, allowing you to set CSS variables for the current theme:

html[data-theme='my-theme'] {
  --primary-text-color: var(--grey-200);
  --secondary-text-color: var(--grey-500);
}

html[data-color-scheme='dark'] {
  --primary-text-color: var(--grey-200);
  --secondary-text-color: var(--grey-500);
}

If you wish to include external files instead of including the theming css in the css bundle, you may also add the ThemeStylesheet component in your app which will pick the right stylesheet according to the selected theme. In this case you have to specify the paths to the css files:

createThemePrimitive({
  themes: [
    {
      name: 'fooTheme',
      path: 'https://example.com/themes/foo.css',
      colorScheme: ColorScheme.Dark
    },
    {
      name: 'barTheme',
      path: 'https://example.com/themes/bar.css',
      colorScheme: ColorScheme.Light
    },
    {
      name: 'bazTheme',
      path: 'https://example.com/themes/baz.css',
      colorScheme: ColorScheme.Dark
    }
  ],
  initialTheme: 'fooTheme'
});

You may then add the ThemeStylesheet component in your app which will pick the right stylesheet according to the selected theme.

import { ThemeStylesheet } from 'solid-compose';

const App: VoidComponent = () => {
  return (
    <>
      <ThemeStylesheet />

      <div></div>
    </>
  );
};

setTheme allows to switch the theme:

import { useTheme } from 'solid-compose';

const [theme, setTheme] = useTheme();

Viewport

Solid Compose allows to listen for changes to the viewport dimension and orientation.

First, initialize and configure the viewport primitive:

import { createViewportPrimitive } from 'solid-compose';

createViewportPrimitive({
  widthSwitchpoints: {
    small: { // width < 768
      max: 768
    },
    medium: { // 768 <= width < 1280
      min: 768,
      max: 1280
    },
    large: { // 1280 <= width
      min: 1280
    },
  }
});

createViewportPrimitive() allows you to configure two properties: widthSwitchpoints and heightSwitchpoints. Both of these properties are objects that let you define custom size names and corresponding size ranges for the viewport dimensions.

The keys in each object represent the custom name for the size, while the values are sub-objects containing min and/or max allowing you to configure the switchpoints for either width or height.

You may then get the current viewport size and orientation and listen for changes:

import { useViewport, ViewportOrientation } from 'solid-compose';

const viewport = useViewport();

console.log(viewport.width); // "large"
console.log(viewport.height); // undefined (height switchpoint names not defined in the config object)
console.log(viewport.orientation === ViewportOrientation.Landscape);

You may define your custom switchpoints names with TypeScript enums in order to catch errors when comparing width and height values:

export enum Viewport {
  SmallWidth =  'SMALL_WIDTH',
  MediumWidth = 'MEDIUM_WIDTH',
  LargeWidth = 'LARGE_WIDTH'
}

createViewportPrimitive({
  widthSwitchpoints: {
    [Viewport.SmallWidth]: {
      max: 768
    },
    [Viewport.MediumWidth]: {
      min: 768,
      max: 1280
    },
    [Viewport.LargeWidth]: {
      min: 1280
    },
  }
});

const viewport = useViewport();

if (viewport.width === Viewport.SmallWidth) {
  // ...
}

You may call the effect:

import { createViewportEffect } from 'solid-compose';

createViewportEffect();

The effect sets the data attributes "data-viewport-width-switchpoint", "data-viewport-height-switchpoint" and "data-viewport-orientation" on the html tag.

Authentication

Solid Compose provides a primitive for making the current user's information accessible across the application.

import { createCurrentUserPrimitive } from 'solid-compose';

const createCurrentUserResource = () => createResource<CurrentUser>(() => /* ... */);

createCurrentUserPrimitive({
  createCurrentUserResource,
  isUnauthenticatedError: (error: any) => isUnauthenticatedError(error),
  isAuthenticated: (data: unknown) => data.__typename === 'User'
});

const [currentUser, { authenticationStatus, authenticationError }] = useCurrentUser<CurrentUser>();

<Switch>
  <Match when={currentUser.loading}>
    <Loader />
  </Match>
  <Match when={authenticationStatus() === AuthenticationStatus.Unauthenticated}>
    <Navigate href={'/login'} />
  </Match>
  <Match when={authenticationStatus() === AuthenticationStatus.Authenticated}>
    <Outlet />
  </Match>
</Switch>

Developer utilities

addLocaleHotkeyListener() simplifies the testing of web interfaces that support multiple locale settings by allowing developers to quickly switch between different locales using customizable hotkeys. This enables you to switch between the following settings with ease:

  • Color schemes
  • Themes
  • Languages
  • Text directions
  • Number formats
  • Time zones
  • Date formats
  • Time formats
  • First day of the week

To use addLocaleHotkeyListener(), simply pass in the hotkeys as optional functions, as shown in the code snippet below. Each hotkey is optional, so if you don't need to test a particular locale setting, simply omit it.

import { addLocaleHotkeyListener } from 'solid-compose';

addLocaleHotkeyListener({
  hotkeys: {
    colorScheme: (e) => e.shiftKey && e.code === 'KeyQ',
    theme: (e) => e.shiftKey && e.code === 'KeyW',
    languageTag: (e) => e.shiftKey && e.code === 'KeyA',
    textDirection: (e) => e.shiftKey && e.code === 'KeyS',
    numberFormat: (e) => e.shiftKey && e.code === 'KeyD',
    timeZone: (e) => e.shiftKey && e.code === 'KeyZ',
    dateFormat: (e) => e.shiftKey && e.code === 'KeyX',
    timeFormat: (e) => e.shiftKey && e.code === 'KeyC',
    firstDayOfWeek: (e) => e.shiftKey && e.code === 'KeyV',
  },
  timeZones: ['Asia/Bangkok', 'Europe/London']
});

Install

You can get solid-compose via npm.

npm install solid-compose

About

A set of reactive state for commonly used features in web apps

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published