typesafe-i18n

repository·main·Indexed 25 days ago

https://github.com/codingcommons/typesafe-i18n

A fully type-safe and lightweight internationalization library for TypeScript and JavaScript projects. It provides full type safety for translation keys and arguments to prevent i18n errors at compile time. The library includes adapters and integration guides for Angular, Node.js, React, Next.js, Expo, React Native, and SolidJS.

Tokens
30.4K
Snippets
98
Records
172
Agent score
81%

What's inside typesafe-i18n

  1. Overview of typesafe-i18n features

    main

    typesafe-i18n is a lightweight (~1kb), fast, and type-safe internationalization library for TypeScript and JavaScript projects.

    Key capabilities include:

    • Type Safety: Prevents mistakes in translation keys and arguments (supports JSDoc for plain JS).
    • Advanced Logic: Supports plural rules and switch-case statements (e.g., for gender-specific output).
    • Formatting: Supports locale-dependent formatting for dates and numbers.
    • Optimization: Supports multiple namespaces and asynchronous loading of locales.
    • Environment Support: Works with frontend, backend (Node.js), and SSR (Server-Side Rendering) environments.
    • Locale Detection: Includes detectors for both browser and server environments.
    • Extensibility: Supports importing/exporting translations and integrates with various frameworks via adapters.
    • Zero Dependencies: The library has no external dependencies.
  2. Supported Frameworks and Integration Options

    main

    The typesafe-i18n package is highly versatile and provides official adapters for several major frameworks. You can use it in:

    • Angular (via @typesafe-i18n/adapter-angular)
    • React / Next.js (via @typesafe-i18n/adapter-react)
    • Solid.js (via @typesafe-i18n/adapter-solid)
    • Svelte / SvelteKit / Sapper (via @typesafe-i18n/adapter-svelte)
    • Vue.js / Nuxt.js (via @typesafe-i18n/adapter-vue)
    • Node.js (for APIs, backends, and scripts)
    • Browser (via CDN)

    For frameworks without an official adapter, you can use the functions generated in the i18n-util.ts file to create your own custom wrapper.

  3. Understand the generated folder structure

    main

    The generator uses an opinionated structure inside src/i18n. Do not manually edit files that are auto-generated, as they will be overwritten.

    • src/i18n/{baseLocale}/index.ts: Contains your base translations.
    • src/i18n/custom-types.ts: Used for defining custom types for translation arguments.
    • src/i18n/formatters.ts: Configuration for translation formatters.
    • src/i18n/i18n-types.ts: The generated TypeScript definitions (do not edit).
    • src/i18n/i18n-util.async.ts: Logic for asynchronous locale loading.
    • src/i18n/i18n-util.sync.ts: Logic for synchronous locale loading.
    • src/i18n/i18n-util.ts: Type-safe wrappers around base i18n functions.
  4. Achieve full typesafety with the generator

    main

    To get the maximum benefit from typesafe-i18n, you should use the generator. The generator creates TypeScript definitions and boilerplate code based on your base locale, providing:

    • Auto-completion for all defined locales.
    • Auto-completion for all available translation keys.
    • Argument validation: Errors if you forget to pass arguments or pass the wrong types.
    • Consistency checks: Errors if a translation is missing an argument or if a key is missing in a specific locale.

    Even if you are using plain JavaScript, you can benefit from full typesafety via JSDoc-annotations generated by the tool.

  5. Use Namespaces to split and lazy-load translations

    main

    Namespaces allow you to split your translations into multiple files and load them on demand (lazy-loading). This is useful for performance, such as loading specific translations only when a user visits a particular page (e.g., a settings page).

    Creating a Namespace

    1. Within your base locale folder, create a new folder named after your namespace.
    2. Inside that folder, create an index.ts file.
    3. The index.ts file must export a Dictionary (or use the BaseTranslation type) containing the translations for that namespace.

    Example Folder Structure (Base locale en):

    src/
       i18n/
          en/
             settings/
                index.ts
             index.ts

    Example index.ts for a namespace:

    import type { BaseTranslation } from '../../i18n-types'
    
    const en_settings: BaseTranslation = { }
    
    export default en_settings

    Once created, the generator automatically creates boilerplate namespace files for all other locales with the correct types assigned.

    Loading a Namespace

    By default, namespace translations are not loaded. You must manually load them using loadNamespaceAsync before calling setLocale.

    Note: Always call setLocale after you have successfully loaded the new namespace.

    const displaySettingsPage = async (locale) => {
       await loadNamespaceAsync(locale, 'settings')
       setLocale(locale)
    
       // goto settings page
    }
  6. Integrate with external translation services

    main

    You can connect typesafe-i18n to other services to read or update translations using the following packages:

    • @typesafe-i18n/importer: To import translations from external sources.
    • @typesafe-i18n/exporter: To export translations to external services.
    • Inlang Plugin: An official plugin (inlang-plugin-typesafe-i18n) is available to use typesafe-i18n with Inlang tooling.
  7. Format passed-in arguments

    main

    You can apply formatters to arguments within the translation string using the | pipe syntax.

    Single formatter

    LLL('Today is {date|weekday}', { date: now }) // => 'Today is Friday'

    Formatter chaining

    Formatters are called from left to right.

    LLL('Today is {date|weekday|uppercase}', { date: now }) // => 'Today is FRIDAY'
    LLL('Today is {date|weekday|uppercase|shorten}', { date: now }) // => 'Today is FRI'
  8. Define translation dictionaries using various formats

    main

    The BaseTranslation type is highly flexible, allowing you to define your dictionary in several ways depending on your workflow (e.g., manual entry vs. CMS integration).

    Supported Formats:

    • Nested key-value pairs (Recommended): Provides the best readability and flexibility. Note: You cannot use the . character in keys.
    • Key-value pairs: Simple flat structure.
    • Arrays and Nested Arrays: Useful for translations coming from external services.
    • Mixed: A combination of objects and arrays.

    Restrictions: Due to JavaScript reserved keywords, you cannot use 'length', 'caller', 'callee', or 'arguments' as keys in your dictionary.

  9. Implement a custom translation exporter

    main

    To export translations to a custom service or API, you must implement your own logic using the provided typesafe-i18n/exporter utilities. The workflow is:

    1. Call readTranslationFromDisk or readTranslationsFromDisk to fetch the data from your local files.
    2. Map the resulting dictionary-representation to the specific format required by your service.
    3. Send the formatted data to your translation service.

    Important: This script should be run during development or as part of a CI process, not at application runtime. You can use tools like tsx to execute your implementation script.

  10. Specify and chain custom formatters

    main

    You can define custom formatters as an object where each key is a formatter name and the value is a function that takes an input and returns a modified value.

    To apply multiple formatters to the same argument, use the pipe | operator. Formatters are applied from left to right.

    const formatters = {
       sqrt: (value) => Math.sqrt(value),
       round: (value) => Math.round(value),
    }
    
    LLL('Result: {0|sqrt|round}', 5)
    // => 'Result: 2'
  11. How to create and initialize custom detectors

    main

    A detector is a function that returns an array of strings (() => string[]).

    If your detector needs access to request-specific variables (like a Node.js Request object), you must use an initialization pattern. You create an initialization function that accepts the required context and returns the actual detector function. This allows you to bind request-specific data to the detector before passing it to detectLocale.

    // 1. Define an initialization function that captures context
    const initIpDetector = (req: Request) => {
       return () => {
          const locale = detectLocaleFromIpAddress(req)
          return [locale]
       }
    }
    
    // 2. Use it within your application logic
    app.use((req: Request, res: Response) => {
       const ipDetector = initIpDetector(req)
       const detectedLocale = detectLocale(fallbackLocale, availableLocales, ipDetector)
    
       res.json({ locale: detectedLocale })
    })