Gathering detailed insights and metrics for remix-intl
Gathering detailed insights and metrics for remix-intl
Gathering detailed insights and metrics for remix-intl
Gathering detailed insights and metrics for remix-intl
npm install remix-intl
Typescript
Module System
Node Version
NPM Version
TypeScript (93.71%)
JavaScript (4%)
Dockerfile (1.53%)
CSS (0.77%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
3 Stars
45 Commits
1 Watchers
1 Branches
1 Contributors
Updated on May 11, 2025
Latest Version
0.0.16
Package Id
remix-intl@0.0.16
Unpacked Size
42.29 kB
Size
12.16 kB
File Count
34
NPM Version
10.8.2
Node Version
20.18.3
Published on
May 10, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
No dependencies detected.
The best internationalization(i18n) library for your Remix apps.
Features:
1// app/._index.tsx 2import { ActionFunctionArgs, json, type MetaFunction } from '@remix-run/node'; 3import { Form, useActionData, useLoaderData } from '@remix-run/react'; 4import { useT } from 'remix-intl'; 5import { getT } from 'remix-intl/server'; 6 7export const meta: MetaFunction = ({ location }) => { 8 const { t } = getT(location); 9 return [{ title: t('title') }]; 10}; 11 12export default function Index() { 13 const { locales } = useLoaderData<typeof loader>(); 14 const actionData = useActionData<typeof action>(); 15 const { t } = useT(); 16 17 return ( 18 <div> 19 <h1>{t('create_todo')}</h1> 20 <Form method="post"> 21 <input type="text" name="title" /> 22 23 {actionData?.errors?.title ? <em>{actionData?.errors.title}</em> : null} 24 25 <button type="submit">{t('create_todo')}</button> 26 </Form> 27 </div> 28 ); 29} 30 31export async function action({ request }: ActionFunctionArgs) { 32 const body = await request.formData(); 33 const { t } = getT(request.url); 34 if (!body.get('title')) { 35 return json({ errors: { title: t('required') } }); 36 } 37}
public/locales/en/index.json
1{ 2 "title": "Remix App", 3 "hi": "Hello", 4 "required": "Required", 5 "create_todo": "Create Todo" 6}
1# npm 2npm install remix-intl i18next 3 4# pnpm 5pnpm add remix-intl i18next 6 7# yarn 8yarn add remix-intl i18next
app/i18n.ts
1// app/i18n.ts 2import { createInstance } from 'i18next'; 3import type { GetLocalesRes, GetMessagesRes, IntlConfig } from 'remix-intl/types'; 4import { setIntlConfig } from 'remix-intl/i18n'; 5 6const defaultNS = 'remix_intl'; 7const i18next = createInstance({ defaultNS, ns: [defaultNS], resources: {} }); 8i18next.init({ 9 defaultNS, 10 ns: [defaultNS], 11 resources: {}, 12}); 13 14async function getLocales(): Promise<GetLocalesRes> { 15 // you can fetch dynamic locales from others API 16 return { locales: ['zh-CN', 'en'] }; 17} 18 19async function getMessages(locale: string, ns?: string): Promise<GetMessagesRes> { 20 // you can fetch dynamic messages from others API 21 const messages = await fetch( 22 `http://localhost:5173/locales/${locale}/${ns || 'index'}.json` 23 ).then((res) => res.json()); 24 return { messages, locale, ns }; 25} 26 27export const intlConfig: IntlConfig = { 28 mode: 'search', 29 paramKey: 'lang', 30 cookieKey: 'remix_intl', 31 defaultNS, 32 clientKey: 'remix_intl', 33 defaultLocale: '', 34 getLocales, 35 getMessages, 36 i18next, 37}; 38 39export function setupIntlConfig() { 40 setIntlConfig(intlConfig); 41} 42setupIntlConfig(); 43 44export default i18next;
app/i18n.server.ts
1// app/i18n.server.ts 2import { createCookie } from '@remix-run/node'; 3import { intlConfig } from './i18n'; 4 5export const i18nCookie = createCookie(intlConfig.cookieKey);
app/navigation.tsx
1// app/navigation.tsx 2import { setupIntlConfig } from './i18n'; 3import { createSharedPathnamesNavigation } from 'remix-intl/navigation'; 4 5setupIntlConfig(); 6 7const { Link, NavLink, useNavigate, SwitchLocaleLink } = createSharedPathnamesNavigation(); 8 9export { Link, NavLink, useNavigate, SwitchLocaleLink };
app/entry.server.tsx
: 3 changes
1// app/entry.server.tsx
2import { PassThrough } from 'node:stream';
3
4import type { AppLoadContext, EntryContext } from '@remix-run/node';
5import { createReadableStreamFromReadable } from '@remix-run/node';
6import { RemixServer } from '@remix-run/react';
7import { isbot } from 'isbot';
8import { renderToPipeableStream } from 'react-dom/server';
9
10/* --- 1.IMPORT THIS --- */
11import { initIntl } from 'remix-intl/server';
12import { i18nCookie } from './i18n.server';
13/* --- 1.IMPORT THIS END --- */
14
15const ABORT_DELAY = 5_000;
16
17/* --- 2.ADD `async` --- */
18export default async function handleRequest(
19 /* --- 2.ADD `async` end --- */
20 request: Request,
21 responseStatusCode: number,
22 responseHeaders: Headers,
23 remixContext: EntryContext,
24 loadContext: AppLoadContext
25) {
26 /* --- 3.ADD THIS --- */
27 await initIntl(request, i18nCookie);
28 /* --- 3.ADD THIS END --- */
29
30 return isbot(request.headers.get('user-agent') || '')
31 ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext)
32 : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext);
33}
34
35function handleBotRequest(
36 request: Request,
37 responseStatusCode: number,
38 responseHeaders: Headers,
39 remixContext: EntryContext
40) {
41 return new Promise((resolve, reject) => {
42 let shellRendered = false;
43 const { pipe, abort } = renderToPipeableStream(
44 <RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
45 {
46 onAllReady() {
47 shellRendered = true;
48 const body = new PassThrough();
49 const stream = createReadableStreamFromReadable(body);
50
51 responseHeaders.set('Content-Type', 'text/html');
52
53 resolve(
54 new Response(stream, {
55 headers: responseHeaders,
56 status: responseStatusCode,
57 })
58 );
59
60 pipe(body);
61 },
62 onShellError(error: unknown) {
63 reject(error);
64 },
65 onError(error: unknown) {
66 responseStatusCode = 500;
67 if (shellRendered) {
68 console.error(error);
69 }
70 },
71 }
72 );
73
74 setTimeout(abort, ABORT_DELAY);
75 });
76}
77
78function handleBrowserRequest(
79 request: Request,
80 responseStatusCode: number,
81 responseHeaders: Headers,
82 remixContext: EntryContext
83) {
84 return new Promise((resolve, reject) => {
85 let shellRendered = false;
86 const { pipe, abort } = renderToPipeableStream(
87 <RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
88 {
89 onShellReady() {
90 shellRendered = true;
91 const body = new PassThrough();
92 const stream = createReadableStreamFromReadable(body);
93
94 responseHeaders.set('Content-Type', 'text/html');
95
96 resolve(
97 new Response(stream, {
98 headers: responseHeaders,
99 status: responseStatusCode,
100 })
101 );
102
103 pipe(body);
104 },
105 onShellError(error: unknown) {
106 reject(error);
107 },
108 onError(error: unknown) {
109 responseStatusCode = 500;
110 if (shellRendered) {
111 console.error(error);
112 }
113 },
114 }
115 );
116
117 setTimeout(abort, ABORT_DELAY);
118 });
119}
app/entry.client.tsx
: 2 changes
1// app/entry.client.tsx 2import { RemixBrowser } from '@remix-run/react'; 3import { startTransition, StrictMode } from 'react'; 4import { hydrateRoot } from 'react-dom/client'; 5 6/* --- 1.IMPORT THIS --- */ 7import { ClientProvider as IntlProvider } from 'remix-intl'; 8/* --- 1.IMPORT THIS END --- */ 9 10startTransition(() => { 11 hydrateRoot( 12 document, 13 <StrictMode> 14 {/* --- 2.ADD THIS --- */} 15 <IntlProvider> 16 <RemixBrowser /> 17 </IntlProvider> 18 {/* --- 2.ADD THIS END--- */} 19 </StrictMode> 20 ); 21});
root.tsx
app/root.tsx
: 4 changes
1// app/root.tsx 2 3/* --- 1.IMPORT THIS --- */ 4import { setupIntlConfig } from './i18n'; 5import { parseLocale } from 'remix-intl/server'; 6import { IntlScript } from 'remix-intl'; 7import { i18nCookie } from './i18n.server'; 8/* --- 1.IMPORT THIS END --- */ 9 10import { 11 Links, 12 Meta, 13 Outlet, 14 Scripts, 15 ScrollRestoration, 16 useLoaderData, 17 json, 18 redirect, 19} from '@remix-run/react'; 20import { LoaderFunctionArgs } from '@remix-run/node'; 21 22/* --- 1.1 Add THIS --- */ 23setupIntlConfig(); 24/* --- 1.1 Add THIS --- */ 25 26export async function loader({ request }: LoaderFunctionArgs) { 27 /* --- 2.ADD THIS --- */ 28 const res = await parseLocale(request, i18nCookie); 29 if (res.isRedirect) { 30 return redirect(res.redirectURL); 31 } 32 return json(res, { 33 headers: { 34 'Set-Cookie': await i18nCookie.serialize(res.locale), 35 }, 36 }); 37 /* --- 2.ADD THIS END --- */ 38} 39 40export function Layout({ children }: { children: React.ReactNode }) { 41 /* --- 3.ADD THIS --- */ 42 const { locale, dir } = useLoaderData<typeof loader>(); 43 return ( 44 <html lang={locale} dir={dir}> 45 {/* --- 3.ADD THIS END --- */} 46 <head> 47 <meta charSet="utf-8" /> 48 <meta name="viewport" content="width=device-width, initial-scale=1" /> 49 <Meta /> 50 <Links /> 51 </head> 52 <body> 53 {children} 54 <ScrollRestoration /> 55 <Scripts /> 56 {/* --- 4.ADD THIS --- */} 57 <IntlScript /> 58 {/* --- 4.ADD THIS END --- */} 59 </body> 60 </html> 61 ); 62} 63 64export default function App() { 65 return <Outlet />; 66}
public/locales/en/index.json
1{ 2 "hi": "Hello" 3}
public/locales/zh-CN/index.json
1{ 2 "hi": "您好" 3}
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.
No need refresh page example:
1import { SwitchLocaleLink } from '~/navigation'; 2 3const langs = [ 4 { 5 text: 'English', 6 code: 'en', 7 }, 8 { 9 text: '简体中文', 10 code: 'zh-CN', 11 }, 12]; 13 14export default function LanguageSwitcher() { 15 return ( 16 <div> 17 {langs.map((item, idx) => { 18 return ( 19 <SwitchLocaleLink key={item.locale} locale={item.code} query={{ idx }}> 20 {item.text} 21 </SwitchLocaleLink> 22 ); 23 })} 24 </div> 25 ); 26}
Refresh page example:
1import { SwitchLocaleLink } from '~/navigation'; 2 3const langs = [ 4 { 5 text: 'English', 6 code: 'en', 7 }, 8 { 9 text: '简体中文', 10 code: 'zh-CN', 11 }, 12]; 13 14export default function LanguageSwitcher() { 15 return ( 16 <div> 17 {langs.map((item) => { 18 return ( 19 <SwitchLocaleLink reloadDocument key={item.locale} locale={item.code}> 20 {item.text} 21 </SwitchLocaleLink> 22 ); 23 })} 24 </div> 25 ); 26}
Link
, NavLink
and useNavigate
1import { Link, NavLink, useNavigate } from '~/navigation'; 2 3export default function LinkNavigate() { 4 const navigate = useNavigate(); 5 return ( 6 <div> 7 {/* /docs?lang=[locale] */} 8 <Link to="/docs">Documents</Link> 9 {/* /docs?lang=[locale] */} 10 <NavLink to="/docs">Documents</NavLink> 11 <button 12 onClick={() => { 13 /* /docs?lang=[locale] */ 14 navigate('/docs'); 15 }}> 16 Go to Documents 17 </button> 18 </div> 19 ); 20}
useT
and useLocale
In React components, we can use useLocale
to get current locale code,
and useT
can get t
function to translate:
1import { useLocale, useT } from 'remix-intl'; 2 3export default function RemixIntlExample() { 4 const locale = useLocale(); 5 const { t, locale: sameWithLocale } = useT(); 6 // or const { t, locale: sameWithLocale } = useT(namespace); 7 8 return ( 9 <div> 10 <h1>{t('i18n_key')}</h1> 11 <p>current locale: {locale}</p> 12 </div> 13 ); 14}
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:
1import { getLocale, getT } from 'remix-intl/server'; 2 3// in `meta` 4export const meta: MetaFunction = ({ location }) => { 5 const { t, locale } = getT(location); // `getT` can receive location object or string pathname?search 6 // or const { t, locale } = getT(location, namespace); 7 const sameWithLocale = getLocale(location); // `getLocale` same paramater with `getT` 8 9 return [{ title: t('i18n_key') }]; 10}; 11 12// in `loader` 13 14export const loader = async ({ request }: LoaderFunctionArgs) => { 15 const { t } = getT(request.url); 16 const locale = getLocale(request.url); 17 return json({ title: t('i18n_key'), locale }); 18}; 19 20// in `action` 21export async function action({ request }: ActionFunctionArgs) { 22 const body = await request.formData(); 23 const { t } = getT(request.url); 24 if (!body.get('title')) { 25 return json({ errors: { title: t('required') } }); 26 } 27 return redirect(request.url); 28}
1// hooks 2import { useT, useLocale } from 'remix-intl'; 3 4// components 5import { ClientProvider, IntlScript } from 'remix-intl'; 6import { 7 Link, NavLink, SwitchLocaleLink, useNavigate 8} from from '~/navigation' 9 10// api for server 11import { getT, getLocale } from 'remix-intl/server'; 12 13// utils 14import { isClient, stringSimilarity, acceptLanguageMatcher } from 'remix-intl/utils';
1import { getIntlConfig } from 'remix-intl/i18n'; 2 3getIntlConfig().i18next.addResouceBundle; 4getIntlConfig().i18next.dir; 5getIntlConfig().i18next.getResouceBundle;
More i18next
API: https://www.i18next.com/
👉 https://remix-intl.tsdk.dev (WIP 🙇🏻♂️)
No vulnerabilities found.
No security vulnerabilities found.