npm package: @liberfi.io/i18n
Description: Internationalization for Liberfi React SDK
Latest version: 0.1.153

# @liberfi.io/i18n

Internationalization and CLI tools for Liberfi React SDK. Built on the i18next ecosystem, this package provides locale management, language detection, async resource loading, and CLI tools for translation workflows. Consumed by `@liberfi.io/react-app` and downstream apps.

## Design Philosophy

- **Inversion of Control** — Side effects (e.g. language-change callbacks, resource loading URLs) are injected via props/options rather than hardcoded. The `Backend` class receives its i18n instance via constructor injection for testability.
- **Provider-optional** — The package exports a singleton `i18n` instance with sensible defaults (English fallback). Components work without `LocaleProvider`; the provider adds resource loading, language switching callbacks, and context-based configuration.
- **Separation of concerns** — Pure utility functions (`parseI18nLang`, `removeLangPrefix`, etc.) live in `utils/` with no dependencies on React or i18n state, making them usable in both client and server contexts.
- **Single source of truth** — All locale types, enums, and default language data are defined once in `types.ts` and `constant.ts`, shared across client and server entry points.

## Installation

```bash
pnpm add @liberfi.io/i18n
```

Peer dependencies: `react >= 18`, `react-dom >= 18`.

## API Reference

### Components

| Component        | Props                 | Description                                                                                                    |
| ---------------- | --------------------- | -------------------------------------------------------------------------------------------------------------- |
| `LocaleProvider` | `LocaleProviderProps` | Wraps the app with i18next context, manages resource loading, language detection, and exposes `LocaleContext`. |

**`LocaleProviderProps`**:

| Prop                      | Type                                  | Description                                                  |
| ------------------------- | ------------------------------------- | ------------------------------------------------------------ |
| `locale`                  | `LocaleCode`                          | Current locale to force.                                     |
| `resource`                | `Record<string, string>`              | Resource bundle for the current locale.                      |
| `resources`               | `Resources`                           | All synchronously loaded resources keyed by locale.          |
| `supportedLanguages`      | `LocaleCode[]`                        | Subset of `defaultLanguages` to expose.                      |
| `convertDetectedLanguage` | `(lang: string) => LocaleCode`        | Custom conversion for the detected browser language.         |
| `backend`                 | `BackendOptions`                      | Options for async resource loading via HTTP (`loadPath`).    |
| `languages`               | `Language[]`                          | Fully custom language list (overrides `supportedLanguages`). |
| `beforeLanguageChange`    | `(lang: LocaleCode) => Promise<void>` | Called before language switch (e.g. load resources).         |
| `afterLanguageChange`     | `(lang: LocaleCode) => Promise<void>` | Called after language switch (e.g. update cookie/URL).       |

### Hooks

| Hook                            | Return                                  | Description                                                                                                                   |
| ------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `useTranslation(ns?, options?)` | `UseTranslationResponse`                | Wraps `react-i18next`'s `useTranslation`, auto-falls back to the singleton i18n instance when used outside `I18nextProvider`. |
| `useLocale()`                   | `LocaleCode`                            | Returns the current locale code and re-renders on language change.                                                            |
| `useChangeLocale()`             | `(locale: LocaleCode) => Promise<void>` | Returns a function that triggers `beforeLanguageChange` → `i18n.changeLanguage` → `afterLanguageChange`.                      |
| `useLocaleContext()`            | `LocaleContextState`                    | Access `languages`, `beforeLanguageChange`, `afterLanguageChange` from context.                                               |

### Utilities

| Function                    | Signature                                                                        | Description                                                                    |
| --------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `parseI18nLang`             | `(lang: string, localeCodes?: LocaleCode[], defaultLang?: LocaleCode) => string` | Normalize a browser language string (e.g. `en-US`) to a supported locale code. |
| `getLocalePathFromPathname` | `(pathname: string, localeCodes?: string[]) => string \| null`                   | Extract the locale segment from a URL pathname (e.g. `/en/markets` → `"en"`).  |
| `removeLangPrefix`          | `(pathname: string, localeCodes?: string[]) => string`                           | Strip the locale prefix from a pathname.                                       |
| `generatePath`              | `(params: { path: string; locale?: string; search?: string }) => string`         | Build a localized URL path with locale prefix and search params.               |

### Types & Enums

| Export                 | Description                                                                       |
| ---------------------- | --------------------------------------------------------------------------------- |
| `LocaleEnum`           | Enum of all 16 supported locale codes.                                            |
| `LocaleCode`           | `keyof typeof LocaleEnum \| (string & {})` — locale code type allowing extension. |
| `Language`             | `{ localCode: LocaleCode; displayName: string }`                                  |
| `Resources<T>`         | Map of `LocaleCode` → partial locale messages, optionally extended with `T`.      |
| `LocaleMessages`       | Full English message type merged with `ExtendLocaleMessages`.                     |
| `ExtendLocaleMessages` | `Record<"extend.${string}", string>` — type for custom extension keys.            |
| `BackendOptions`       | `{ loadPath: (lang, ns) => string \| string[] }`                                  |
| `LocaleContextState`   | Context shape: `languages`, `beforeLanguageChange`, `afterLanguageChange`.        |

### Constants

| Export                | Value                    | Description                                    |
| --------------------- | ------------------------ | ---------------------------------------------- |
| `defaultLanguages`    | `Language[]`             | All 16 supported languages with display names. |
| `defaultLng`          | `"en"`                   | Default fallback language.                     |
| `defaultNS`           | `"translation"`          | Default i18next namespace.                     |
| `i18nLocalStorageKey` | `"liberfi_i18nLng"`      | localStorage key for persisted language.       |
| `i18nCookieKey`       | `"liberfi_i18nLng"`      | Cookie key for persisted language.             |
| `en`                  | `Record<string, string>` | Built-in English translations.                 |

## Integration Guide

Follow these steps to integrate localization support in your app using Liberfi React SDK:

### 1. Wrap Your App with LocaleProvider

The LocaleProvider is the core component that supplies localized resources to your application. Make sure to wrap your app’s root component with LocaleProvider.

```tsx
import { LocaleProvider } from "@liberfi.io/i18n";

<LocaleProvider>
  <YourApp />
</LocaleProvider>;
```

### 2. Provide Locale Data

#### Default Language

- English (`en`) is included by default.

#### Supported Locales

We currently support **16 locales**

| Locale Code | Language   |
| ----------- | ---------- |
| `en`        | English    |
| `zh`        | Chinese    |
| `ja`        | Japanese   |
| `es`        | Spanish    |
| `ko`        | Korean     |
| `vi`        | Vietnamese |
| `de`        | German     |
| `fr`        | French     |
| `ru`        | Russian    |
| `id`        | Indonesian |
| `tr`        | Turkish    |
| `it`        | Italian    |
| `pt`        | Portuguese |
| `uk`        | Ukrainian  |
| `pl`        | Polish     |
| `nl`        | Dutch      |

#### CSV for Easy Translation

- Each release generates a `dist/locale.csv` file to simplify translation workflows.
- We provide a CLI tool to convert between CSV and JSON formats.

### 3. Extending Locale Files

You can localize both the SDK UI and your own custom components.

- When adding custom keys, prefix them with `extend.` to avoid conflicts with default keys.

```json
{
  "extend.custom.button.label": "My Custom Button"
}
```

## Example

Here's a complete example of how to set up the i18n integration:

### Async load locale files

```typescript
import { FC, ReactNode } from "react";
import { WalletConnectorProvider } from "@liberfi.io/wallet-connector";
import { LiberfiAppProvider } from "@liberfi.io/react-app";
import { LocaleProvider, LocaleEnum, LocaleCode } from "@liberfi.io/i18n";

const LiberfiProvider: FC<{ children: ReactNode }> = (props) => {
  const onLanguageChanged = async (lang: LocaleCode) => {};

  // please copy build-in locale files (@liberfi.io/i18n/locales) to you public/locales
  // and copy you extend locale files to public/locales/extend
  const loadPath = (lang: LocaleCode) => {
    if (lang === LocaleEnum.en) {
      // because en is built-in, we need to load the en extend only
      return `/locales/extend/${lang}.json`;
    }
    return [`/locales/${lang}.json`, `/locales/extend/${lang}.json`];
  };

  return (
    <LocaleProvider
      onLanguageChanged={onLanguageChanged}
      backend={{ loadPath }}
    >
      <WalletConnectorProvider>
        <LiberfiAppProvider
          brokerId="liberfi"
          brokerName="Liberfi"
          networkId="testnet"
        >
          {props.children}
        </LiberfiAppProvider>
      </WalletConnectorProvider>
    </LocaleProvider>
  );
};
```

### Sync Load locale data

```typescript
import { FC, ReactNode } from "react";
import { WalletConnectorProvider } from "@liberfi.io/wallet-connector";
import { LiberfiAppProvider } from "@liberfi.io/react-app";
import { LocaleProvider, LocaleCode, Resources } from "@liberfi.io/i18n";
import zh from "@liberfi.io/i18n/locales/zh.json";
import ja from "@liberfi.io/i18n/locales/ja.json";
import ko from "@liberfi.io/i18n/locales/ko.json";

// extend or overrides English translations
const extendEn = {
  "extend.trading": "Trading",
};

// extend or overrides chinese translations
const extendZh = {
  "extend.trading": "交易",
};

// extend or overrides japanese translations
const extendJa = {
  "extend.trading": "取引",
};

// extend or overrides korean translations
const extendKo = {
  "extend.trading": "거래",
};

// define language resources
const resources: Resources = {
  en: extendEn,
  zh: {
    ...zh,
    ...extendZh,
  },
  ja: {
    ...ja,
    ...extendJa,
  },
  ko: {
    ...ko,
    ...extendKo,
  },
};

const LiberfiProvider: FC<{ children: ReactNode }> = (props) => {
  const onLanguageChanged = (locale: LocaleCode) => {};

  return (
    <LocaleProvider resources={resources} onLanguageChanged={onLocaleChange}>
      <WalletConnectorProvider>
        <LiberfiAppProvider
          brokerId="liberfi"
          brokerName="Liberfi"
          networkId="testnet"
        >
          {props.children}
        </LiberfiAppProvider>
      </WalletConnectorProvider>
    </LocaleProvider>
  );
};
```

### Add custom languages

We also support adding more custom languages

```typescript
import { FC, ReactNode } from "react";
import { WalletConnectorProvider } from "@liberfi.io/wallet-connector";
import { LiberfiAppProvider } from "@liberfi.io/react-app";
import {
  LocaleProvider,
  Resources,
  LocaleEnum,
  LocaleCode,
  Language,
} from "@liberfi.io/i18n";

// japanese locale
const ja = {
  "extend.ja": "日本語",
};

// korean locale
const ko = {
  "extend.ko": "한국어",
};

// define language resources
const resources: Resources = {
  ja,
  ko,
};

// custom languages
const languages: Language[] = [
  { localCode: LocaleEnum.en, displayName: "English" },
  { localCode: LocaleEnum.ja, displayName: "日本語" },
  { localCode: LocaleEnum.ko, displayName: "한국어" },
];

const LiberfiProvider: FC<{ children: ReactNode }> = (props) => {
  const onLanguageChanged = (locale: LocaleCode) => {};

  return (
    <LocaleProvider
      resources={resources}
      languages={languages}
      onLanguageChanged={onLanguageChanged}
    >
      <WalletConnectorProvider>
        <LiberfiAppProvider
          brokerId="liberfi"
          brokerName="Liberfi"
          networkId="testnet"
        >
          {props.children}
        </LiberfiAppProvider>
      </WalletConnectorProvider>
    </LocaleProvider>
  );
};
```

## CLI

## Usage

The package provides a CLI tool for managing Internationalization files.

```bash
npx @liberfi.io/i18n <command> [options]
```

## Commands

### csv2json

Convert a locale CSV file to multiple locale JSON files.

```bash
npx @liberfi.io/i18n csv2json <input> <outputDir>
```

Example:

```bash
npx @liberfi.io/i18n csv2json ./dist/locale.csv ./dist/locales
```

### json2csv

Convert multiple locale JSON files to a single locale CSV file.

```bash
npx @liberfi.io/i18n json2csv <inputDir> <output>
```

Example:

```bash
npx @liberfi.io/i18n json2csv ./locales ./dist/locale.csv
```

### diffcsv

Compare two locale CSV files to find differences.

```bash
npx @liberfi.io/i18n diffcsv <oldFile> <newFile>
```

Example:

```bash
npx @liberfi.io/i18n diffcsv ./dist/locale1.csv ./dist/locale2.csv
```

### fillJson

Fill values from an input locale JSON file and generate a new locale JSON file.

```bash
npx @liberfi.io/i18n fillJson <input> <output>
```

Example:

```bash
npx @liberfi.io/i18n fillJson ./src/locale/zh.json ./dist/locale/zh.json
```

### separateJson

Separate JSON files into default and extend key values based on a specified key.

```bash
npx @liberfi.io/i18n separateJson <inputDir> <outputDir> <separateKey>
```

Example:

```bash
npx @liberfi.io/i18n separateJson ./locales ./dist/locales extend
```

### mergeJson

Merge default and extend JSON files back into one file.

```bash
npx @liberfi.io/i18n mergeJson <inputDir> <outputDir>
```

Example:

```bash
npx @liberfi.io/i18n mergeJson ./dist/locales1 ./dist/locales2
```

## Testing

Run unit tests:

```bash
pnpm test
```

Tests cover:

- **Utility functions** — `parseI18nLang`, `getLocalePathFromPathname`, `removeLangPrefix`, `generatePath`
- **Backend class** — async resource fetching, caching, error handling
- **React hooks** — `useLocale`, `useChangeLocale` lifecycle and state updates
- **LocaleProvider** — rendering, resource injection, language switching, context propagation

## Future Improvements

- Accept an `i18n` instance prop on `LocaleProvider` instead of relying on the module-level singleton, enabling multiple isolated i18n contexts.
- Add `onError` callback to `BackendOptions` for caller-controlled error handling instead of `console.warn`.
- Expose loading/error state from `Backend` so consumers can show loading indicators.
- Consider lazy-loading locale modules to reduce initial bundle size for apps that only need a subset of languages.