https://github.com/suhaotian/remix-intl
The best remix i18n library.
https://github.com/suhaotian/remix-intl
i18n i18next intl locales react remix remix-intl
Last synced: about 2 months ago
JSON representation
The best remix i18n library.
- Host: GitHub
- URL: https://github.com/suhaotian/remix-intl
- Owner: suhaotian
- Created: 2024-04-02T05:38:59.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2024-10-26T20:39:36.000Z (7 months ago)
- Last Synced: 2025-04-12T04:10:05.458Z (about 2 months ago)
- Topics: i18n, i18next, intl, locales, react, remix, remix-intl
- Language: TypeScript
- Homepage:
- Size: 377 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# remix-intl
The best internationalization(i18n) library for your Remix apps.
**Features:**
- đĨŗ Powerful and fully under your control
- đ Minimal size, less dependencies## What does it look like?
```tsx
// app/._index.tsx
import { ActionFunctionArgs, json, type MetaFunction } from '@remix-run/node';
import { Form, useActionData, useLoaderData } from '@remix-run/react';
import { useT } from 'remix-intl';
import { getT } from 'remix-intl/server';export const meta: MetaFunction = ({ location }) => {
const { t } = getT(location);
return [{ title: t('title') }];
};export default function Index() {
const { locales } = useLoaderData();
const actionData = useActionData();
const { t } = useT();return (
{t('create_todo')}
{actionData?.errors?.title ? {actionData?.errors.title} : null}
{t('create_todo')}
);
}export async function action({ request }: ActionFunctionArgs) {
const body = await request.formData();
const { t } = getT(request.url);
if (!body.get('title')) {
return json({ errors: { title: t('required') } });
}
}
```> public/locales/en/index.json
```json
{
"title": "Remix App",
"hi": "Hello",
"required": "Required",
"create_todo": "Create Todo"
}
```## Table of Contents
- [remix-intl](#remix-intl)
- [What does it look like?](#what-does-it-look-like)
- [Table of Contents](#table-of-contents)
- [Installing](#installing)
- [Configuration](#configuration)
- [1. Create files](#1-create-files)
- [Create i18n config file](#create-i18n-config-file)
- [Create i18n cookie file](#create-i18n-cookie-file)
- [Create i18n navigation components file](#create-i18n-navigation-components-file)
- [2. Update](#2-update)
- [Update server entry](#update-server-entry)
- [Update client entry](#update-client-entry)
- [Update `root.tsx`](#update-roottsx)
- [Create i18n messages](#create-i18n-messages)
- [Usage](#usage)
- [Different mode: `segment` or `search`](#different-mode-segment-or-search)
- [`paramKey`](#paramkey)
- [Switch different languages](#switch-different-languages)
- [`Link`, `NavLink` and `useNavigate`](#link-navlink-and-usenavigate)
- [`useT` and `useLocale`](#uset-and-uselocale)
- [`getT` and `getLocale` in `meta` / `loader` / `action`](#gett-and-getlocale-in-meta--loader--action)
- [API](#api)
- [remix-intl API](#remix-intl-api)
- [i18next API](#i18next-api)
- [Website and example](#website-and-example)
- [Support](#support)## Installing
```sh
# npm
npm install remix-intl i18next# pnpm
pnpm add remix-intl i18next# yarn
yarn add remix-intl i18next
```## Configuration
### 1. Create files
#### Create i18n config file
`app/i18n.ts`
```ts
// app/i18n.ts
import { createInstance } from 'i18next';
import type { GetLocalesRes, GetMessagesRes, IntlConfig } from 'remix-intl/types';
import { setIntlConfig } from 'remix-intl/i18n';const defaultNS = 'remix_intl';
const i18next = createInstance({ defaultNS, ns: [defaultNS], resources: {} });
i18next.init({
defaultNS,
ns: [defaultNS],
resources: {},
});async function getLocales(): Promise {
// you can fetch dynamic locales from others API
return { locales: ['zh-CN', 'en'] };
}async function getMessages(locale: string, ns?: string): Promise {
// you can fetch dynamic messages from others API
const messages = await fetch(
`http://localhost:5173/locales/${locale}/${ns || 'index'}.json`
).then((res) => res.json());
return { messages, locale, ns };
}export const intlConfig: IntlConfig = {
mode: 'search',
paramKey: 'lang',
cookieKey: 'remix_intl',
defaultNS,
clientKey: 'remix_intl',
defaultLocale: '',
getLocales,
getMessages,
i18next,
};setIntlConfig(intlConfig);
export default i18next;
```#### Create i18n cookie file
`app/i18n.server.ts`
```ts
// app/i18n.server.ts
import { createCookie } from '@remix-run/node';
import { intlConfig } from './i18n';export const i18nCookie = createCookie(intlConfig.cookieKey);
```#### Create i18n navigation components file
`app/navigation.tsx`
```tsx
// app/navigation.tsx
import { createSharedPathnamesNavigation } from 'remix-intl/navigation';const { Link, NavLink, useNavigate, SwitchLocaleLink } = createSharedPathnamesNavigation();
export { Link, NavLink, useNavigate, SwitchLocaleLink };
```### 2. Update
#### Update server entry
`app/entry.server.tsx`: **3 changes**
```tsx
// app/entry.server.tsx
import { PassThrough } from 'node:stream';import type { AppLoadContext, EntryContext } from '@remix-run/node';
import { createReadableStreamFromReadable } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import { isbot } from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';/* --- 1.IMPORT THIS --- */
import { initIntl } from 'remix-intl/server';
import { i18nCookie } from './i18n.server';
/* --- 1.IMPORT THIS END --- */const ABORT_DELAY = 5_000;
/* --- 2.ADD `async` --- */
export default async function handleRequest(
/* --- 2.ADD `async` end --- */
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext
) {
/* --- 3.ADD THIS --- */
await initIntl(request, i18nCookie);
/* --- 3.ADD THIS END --- */return isbot(request.headers.get('user-agent') || '')
? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext)
: handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext);
}function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);responseHeaders.set('Content-Type', 'text/html');
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
if (shellRendered) {
console.error(error);
}
},
}
);setTimeout(abort, ABORT_DELAY);
});
}function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);responseHeaders.set('Content-Type', 'text/html');
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
if (shellRendered) {
console.error(error);
}
},
}
);setTimeout(abort, ABORT_DELAY);
});
}
```#### Update client entry
`app/entry.client.tsx`: **2 changes**
```tsx
// app/entry.client.tsx
import { RemixBrowser } from '@remix-run/react';
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';/* --- 1.IMPORT THIS --- */
import { ClientProvider as IntlProvider } from 'remix-intl';
/* --- 1.IMPORT THIS END --- */startTransition(() => {
hydrateRoot(
document,
{/* --- 2.ADD THIS --- */}
{/* --- 2.ADD THIS END--- */}
);
});
```#### Update `root.tsx`
`app/root.tsx`: **4 changes**
```tsx
// app/root.tsx/* --- 1.IMPORT THIS --- */
import './i18n';
import { parseLocale } from 'remix-intl/server';
import { IntlScript } from 'remix-intl';
import { i18nCookie } from './i18n.server';
/* --- 1.IMPORT THIS END --- */import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
json,
redirect,
} from '@remix-run/react';
import { LoaderFunctionArgs } from '@remix-run/node';export async function loader({ request }: LoaderFunctionArgs) {
/* --- 2.ADD THIS --- */
const res = await parseLocale(request, i18nCookie);
if (res.isRedirect) {
return redirect(res.redirectURL);
}
return json(res, {
headers: {
'Set-Cookie': await i18nCookie.serialize(res.locale),
},
});
/* --- 2.ADD THIS END --- */
}export function Layout({ children }: { children: React.ReactNode }) {
/* --- 3.ADD THIS --- */
const { locale, dir } = useLoaderData();
return (
{/* --- 3.ADD THIS END --- */}
{children}
{/* --- 4.ADD THIS --- */}
{/* --- 4.ADD THIS END --- */}
);
}export default function App() {
return ;
}
```### Create i18n messages
`public/locales/en/index.json`
```json
{
"hi": "Hello"
}
````public/locales/zh-CN/index.json`
```json
{
"hi": "æ¨åĨŊ"
}
```## Usage
### Different mode: `segment` or `search`
**segment mode:** `https://example.com/locale/path`
**search mode:** `https://example.com/path?lang=locale`
Default is `search` mode, you can update `mode` in `app/i18n.ts` config file.
> If you choose `segment` mode, don't forget add file prefix `($lang).` to your routes files
### `paramKey`
Default is `lang`, you can change to others you like.
### Switch different languages
No need refresh page example:
```tsx
import { SwitchLocaleLink } from '~/navigation';const langs = [
{
text: 'English',
code: 'en',
},
{
text: 'įŽäŊ䏿',
code: 'zh-CN',
},
];export default function LanguageSwitcher() {
return (
{langs.map((item, idx) => {
return (
{item.text}
);
})}
);
}
```Refresh page example:
```tsx
import { SwitchLocaleLink } from '~/navigation';const langs = [
{
text: 'English',
code: 'en',
},
{
text: 'įŽäŊ䏿',
code: 'zh-CN',
},
];export default function LanguageSwitcher() {
return (
{langs.map((item) => {
return (
{item.text}
);
})}
);
}
```### `Link`, `NavLink` and `useNavigate`
```tsx
import { Link, NavLink, useNavigate } from '~/navigation';export default function LinkNavigate() {
const navigate = useNavigate();
return (
{/* /docs?lang=[locale] */}
Documents
{/* /docs?lang=[locale] */}
Documents
{
/* /docs?lang=[locale] */
navigate('/docs');
}}>
Go to Documents
);
}
```### `useT` and `useLocale`
In React components, we can use `useLocale` to get current locale code,
and `useT` can get `t` function to translate:
```tsx
import { useLocale, useT } from 'remix-intl';export default function RemixIntlExample() {
const locale = useLocale();
const { t, locale: sameWithLocale } = useT();
// or const { t, locale: sameWithLocale } = useT(namespace);return (
{t('i18n_key')}
current locale: {locale}
);
}
```### `getT` and `getLocale` in `meta` / `loader` / `action`
Out of react components, like inside `meta`, `loader` or `action`, we can use `getT` to get `t` function and translate:
```tsx
import { getLocale, getT } from 'remix-intl/server';// in `meta`
export const meta: MetaFunction = ({ location }) => {
const { t, locale } = getT(location); // `getT` can receive location object or string pathname?search
// or const { t, locale } = getT(location, namespace);
const sameWithLocale = getLocale(location); // `getLocale` same paramater with `getT`return [{ title: t('i18n_key') }];
};// in `loader`
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { t } = getT(request.url);
const locale = getLocale(request.url);
return json({ title: t('i18n_key'), locale });
};// in `action`
export async function action({ request }: ActionFunctionArgs) {
const body = await request.formData();
const { t } = getT(request.url);
if (!body.get('title')) {
return json({ errors: { title: t('required') } });
}
return redirect(request.url);
}
```## API
#### remix-intl API
```tsx
// hooks
import { useT, useLocale } from 'remix-intl';// components
import { ClientProvider, IntlScript } from 'remix-intl';
import {
Link, NavLink, SwitchLocaleLink, useNavigate
} from from '~/navigation'// api for server
import { getT, getLocale } from 'remix-intl/server';// utils
import { isClient, stringSimilarity, acceptLanguageMatcher } from 'remix-intl/utils';
```#### i18next API
```ts
import { getIntlConfig } from 'remix-intl/i18n';getIntlConfig().i18next.addResouceBundle;
getIntlConfig().i18next.dir;
getIntlConfig().i18next.getResouceBundle;
```More `i18next` API: https://www.i18next.com/
## Website and example
đ [https://remix-intl.tsdk.dev](https://remix-intl.tsdk.dev) (WIP đđģââī¸)
## Support
- Any questions, feel free create issues đ