Nano Stores I18n

repository·main·Indexed 43 days ago

https://github.com/nanostores/i18n

A tiny (≈600 bytes), zero-dependency i18n library for React, Preact, Vue, Svelte, and plain JS. It integrates with Nano Stores and the native JS Internationalization API to provide reactive localization, pluralization via Intl.PluralRules, and lazy loading of translation JSON files. Version 1.3.3.

Tokens
8.9K
Snippets
28
Records
38
Agent score
54%

What's inside Nano Stores I18n

  1. Implement lazy loading for translations

    main

    To avoid downloading all translations at once, you can use component name prefixes to split translation files into chunks.

    1. Prefix components: Use names like main/post or settings/user when calling i18n().
    2. Organize JSON: Store translations in files matching those prefixes (e.g., public/translations/ru/main.json).
    3. Custom get logic: In createI18n, use the components argument in the get function to identify which chunks need to be fetched.

    When a component renders, i18n tracks which component names are used and triggers the get function for any missing prefixes.

    export const i18n = createI18n(locale, {
      async get(code, components) {
        // Extract unique prefixes (e.g., 'main' from 'main/post')
        let prefixes = components.map(name => name.split('/')[0])
        let unique = Array.from(new Set(prefixes))
        
        // Fetch only the required chunks
        return Promise.all(
          unique.map(chunk => fetchJSON(`/translations/${code}/${chunk}.json`))
        )
      }
    })
  2. Quickstart: Basic usage of Nano Stores I18n

    main

    Nano Stores I18n provides a tiny (approx. 1 KB) and flexible way to manage translations in web applications. It leverages Nano Stores for state management and the JS Internationalization API for formatting. It is compatible with React, Preact, Vue, Svelte, and plain JS.

    To use it, you typically:

    1. Define a locale store.
    2. Create an i18n instance with a loader function (e.g., using fetch).
    3. Create a formatter for dates and numbers.
    4. Define your message stores using i18n() and helper functions like params() or count().
    // components/post.jsx
    import { params, count } from '@nanostores/i18n'
    import { useStore } from '@nanostores/react'
    import { i18n, format } from '../stores/i18n.js'
    
    export const messages = i18n('post', {
      title: 'Post details',
      published: params('Was published at {at}'),
      comments: count({
        one: '{count} comment',
        other: '{count} comments'
      })
    })
    
    export const Post = ({ author, comments, publishedAt }) => {
      const t = useStore(messages)
      const { time } = useStore(format)
      return (
        <article>
          <h1>{t.title}</h1>
          <p>{t.published({ at: time(publishedAt) })}</p>
          <p>{t.comments(comments.length)}</p>
        </article>
      )
    }
  3. Handle Server-Side Rendering (SSR) and Server-Only Rendering

    main

    Server-Side Rendering (SSR)

    When using SSR with hydration, you should use your own locale store and provide a cache in createI18n to prevent the server from attempting to fetch translations over the network.

    Server-Only Rendering

    If you are rendering content entirely on the server without client hydration, use loadTranslations(messages) to ensure all required translations are loaded before the component renders.

    import { loadTranslations } from '@nanostores/i18n'
    import { i18n } from '../stores/i18n.js'
    
    // The messages store created via i18n()
    export const messages = i18n('post', { post: 'Post details' })
    
    async function Post() {
      // Wait for translations to be ready
      const t = await loadTranslations(messages)
      return <span>{t.post}</span>
    }
  4. Configure the I18n store and locale

    main

    You can set up a robust i18n system by combining @nanostores/i18n utilities with other Nano Stores.

    Common pattern:

    • Use localeFrom to derive the active locale from a persistent store (like localStorage) or from the browser's settings using browser().
    • Use formatter(locale) to create a store that provides localized date, number, and time formatting.
    • Use createI18n(locale, options) to initialize the translation engine. The options object requires a get(code) function that returns the translation data (e.g., via fetch).
    // stores/i18n.js
    import { createI18n, localeFrom, browser, formatter } from '@nanostores/i18n'
    import { persistentAtom } from '@nanostores/persistent'
    
    export const setting = persistentAtom<string | undefined>('locale', undefined)
    
    export const locale = localeFrom(
      setting, // User’s locale from localStorage
      browser({
        // or browser’s locale auto-detect
        available: ['en', 'fr', 'ru'],
        fallback: 'en'
      })
    )
    
    export const format = formatter(locale)
    
    export const i18n = createI18n(locale, {
      get(code) {
        return fetchJSON(`/translations/${code}.json`)
      }
    })
  5. Format dates, numbers, and relative time with `formatter()`

    main

    The formatter(locale) function creates a store containing formatting functions that react to changes in the locale store.

    It provides three main functions:

    1. time(date, options): Formats a date using Intl.DateTimeFormat options.
    2. number(value, options): Formats a number using Intl.NumberFormat options.
    3. relativeTime(value, unit, options): Formats relative time (e.g., "yesterday") using Intl.RelativeTimeFormat options.

    These functions are reactive; when the locale store updates, the formatting logic automatically adjusts.

    import { formatter } from '@nanostores/i18n'
    
    // Create the formatter store linked to your locale
    export const format = formatter(locale)
    
    // Usage in a component (React example)
    import { useStore } from '@nanostores/react'
    
    export const DateDisplay = ({ date }) => {
      const { time } = useStore(format)
      return <span>{time(date, { month: 'long' })}</span>
    }
  6. Create an I18n object with `createI18n()`

    main

    The createI18n(locale, options) function is the core of the library. It manages the loading and caching of translations.

    Key Options:

    • get(code, components?): An async function used to fetch translations for a specific locale code. If components are provided, you can implement lazy loading based on component prefixes.
    • cache: An object to provide pre-loaded translations (useful for SSR).
    • preprocessors: An array of functions to transform all messages (e.g., applying typography rules).
    • processors: An array of custom processors that allow translations to change based on other application state (e.g., screen size).
    import { createI18n } from '@nanostores/i18n'
    
    export const i18n = createI18n(locale, {
      async get(code) {
        return await fetchJSON(`/translations/${code}.json`)
      }
    })
  7. Define base translations and use `i18n()`

    main

    Base translations are defined directly in your component source code. They serve two purposes: providing TypeScript types and acting as the default fallback.

    Use the i18n(namespace, definition) function to create a translation store for a specific component or section.

    Translation Helpers:

    • params(template): Replaces placeholders in a string (e.g., params('Hello, {name}')).
    • count(options): Handles pluralization using Intl.PluralRules (e.g., count({ one: '...', other: '...' })).
    • transform(fn): Allows creating custom translation transforms.
    import { i18n, params, count } from '@nanostores/i18n'
    
    // Define base translations for a component
    export const messages = i18n('post', {
      title: 'Post details',
      published: params('Was published at {at}'),
      comments: count({
        one: '{count} comment',
        other: '{count} comments'
      })
    })
  8. Determine the user's locale with `localeFrom()`

    main

    The localeFrom() function is a store builder used to find the user's current locale by checking a list of provided stores in order.

    Common patterns include:

    • Browser settings: Use browser({ available: [...] }) to detect the locale from the user's browser settings. You must provide a list of available locales.
    • Manual override: Combine a persistent store (like localStorage) with the browser store so users can manually switch languages.
    • URL Router: Use a computed store to extract the locale from URL parameters.

    The locale store can then be used with any Nano Stores framework integration (e.g., @nanostores/react).

    import { localeFrom, browser } from '@nanostores/i18n'
    
    // Example: Priority given to manual settings, then browser settings
    export const locale = localeFrom(
      localeSettings, 
      browser({ available: ['en', 'fr', 'ru'] as const })
    )
  9. Extend translations with `createProcessor()`

    main

    Processors allow you to create custom translation types that react to external application state. This is useful for changing text based on things like screen size, user roles, or theme.

    1. Create a processor using createProcessor(someStore).
    2. Inside the processor, return a function that maps state values to specific translation strings.
    3. Add the processor to the processors array in createI18n.
    4. Use the processor inside your i18n() definition.
    import { createI18n, createProcessor } from '@nanostores/i18n'
    import { atom } from 'nanostores'
    
    const screenSize = atom('big')
    export const size = createProcessor(screenSize)
    
    export const i18n = createI18n(locale, {
      get: ..., 
      processors: [size]
    })
    
    // In your component definition
    export const messages = i18n({
      send: size({
        big: 'Send message',
        small: 'send'
      })
    })
  10. Define translation JSON files

    main

    Translations should be provided as plain flat JSON files. This format is compatible with online translation services like Weblate. The structure should match the keys used in your i18n() calls.

    {
      "post": {
        "title": "Данные о публикации",
        "published": "Опубликован {at}",
        "comments": {
          "one": "{count} комментарий",
          "few": "{count} комментария",
          "many": "{count} комментариев"
        }
      }
    }