• Stars
    star
    1,264
  • Rank 37,217 (Top 0.8 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 2 years ago
  • Updated 9 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Type-safe internationalization (i18n) for Next.js


Type-safe internationalization (i18n) for Next.js


Features

  • 100% Type-safe: Locales in TS or JSON, type-safe t() & scopedT(), type-safe params
  • Small: 1.2 KB gzipped (1.7 KB uncompressed), no dependencies
  • Simple: No webpack configuration, no CLI, just pure TypeScript
  • SSR: Load only the required locale, SSRed

Note: You can now build on top of the types used by next-international using international-types!

Usage

pnpm install next-international
  1. Make sure that you've followed Next.js Internationalized Routing, and that strict is set to true in your tsconfig.json

  2. Create locales/index.ts with your locales:

import { createI18n } from 'next-international'

export const { useI18n, I18nProvider, getLocaleProps } = createI18n({
  en: () => import('./en'),
  fr: () => import('./fr'),

})

Each locale file should export a default object (don't forget as const):

// locales/en.ts
export default {
  hello: 'Hello',
  welcome: 'Hello {name}!',
} as const;
  1. Wrap your whole app with I18nProvider inside _app.tsx:
// pages/_app.tsx
import { I18nProvider } from '../locales'

function App({ Component, pageProps }) {
  return (
    <I18nProvider locale={pageProps.locale}>
      <Component {...pageProps} />
    </I18nProvider>
  );
}
  1. Add getLocaleProps to your pages, or wrap your existing getStaticProps (this will allows SSR locales, see Load initial locales client-side if you want to load the initial locale client-side):
export const getStaticProps = getLocaleProps()

// or with an existing `getStaticProps` function:
export const getStaticProps = getLocaleProps((ctx) => {
  // your existing code
  return {
    ...
  }
})

If you already have getServerSideProps on this page, you can't use getStaticProps. In this case, you can still use getLocaleProps the same way:

export const getServerSideProps = getLocaleProps()

// or with an existing `getServerSideProps` function:
export const getServerSideProps = getLocaleProps((ctx) => {
  // your existing code
  return {
    ...
  }
})
  1. Use useI18n:
import { useI18n } from '../locales';

function App() {
  const t = useI18n();
  return (
    <div>
      <p>{t('hello')}</p>
      <p>{t('welcome', { name: 'John' })}</p>
      <p>{t('welcome', { name: <strong>John</strong> })}</p>
    </div>
  );
}

Examples

Scoped translations

When you have a lot of keys, you may notice in a file that you always use and such duplicate the same scope:

// We always repeat `pages.settings`
t('pages.settings.title');
t('pages.settings.description', { identifier });
t('pages.settings.cta');

We can avoid this using the useScopedI18n hook. Export it from createI18n:

// locales/index.ts
export const {
  useScopedI18n,
  ...
} = createI18n({
  ...
})

Then use it in your component:

import { useScopedI18n } from '../locales';

function App() {
  const t = useScopedI18n('pages.settings');

  return (
    <div>
      <p>{t('title')}</p>
      <p>{t('description', { identifier })}</p>
      <p>{t('cta')}</p>
    </div>
  );
}

And of course, the scoped key, subsequents keys and params will still be 100% type-safe.

Change and get current locale

Export useChangeLocale and useCurrentLocale from createI18n:

// locales/index.ts
export const {
  useChangeLocale,
  useCurrentLocale,
  ...
} = createI18n({
  ...
})

Then use this as a hook:

import { useChangeLocale, useCurrentLocale } from '../locales'

function App() {
  const changeLocale = useChangeLocale()
  const locale = useCurrentLocale()
  //    ^ typed as 'en' | 'fr'

  return (
    <>
    <p>Current locale: <span>{locale}</span></p>
    <button onClick={() => changeLocale('en')}>English</button>
    <button onClick={() => changeLocale('fr')}>French</button>
    <>
  )
}

Fallback locale for missing translations

It's common to have missing translations in an application. By default, next-international outputs the key when no translation is found for the current locale, to avoid sending to users uncessary data.

You can provide a fallback locale that will be used for all missing translations:

// pages/_app.tsx
import { I18nProvider } from '../locales';
import en from '../locales/en';

<I18nProvider locale={pageProps.locale} fallbackLocale={en}>
  ...
</I18nProvider>;

Use JSON files instead of TS for locales

Currently, this breaks the parameters type-safety, so we recommend using the TS syntax. See this issue: microsoft/TypeScript#32063.

You can still get type-safety by explicitly typing the locales

// locales/index.ts
import { createI18n } from 'next-international'

export const { useI18n, I18nProvider, getLocaleProps } = createI18n({
  en: () => import('./en.json'),
  fr: () => import('./fr.json'),
});

Explicitly typing the locales

If you want to explicitly type the locale, you can create an interface that extends BaseLocale and use it as the generic in createI18n:

// locales/index.ts
import { createI18n } from 'next-international';

type Locale = {
  hello: string;
  welcome: string;
}

type Locales = {
  en: Locale;
  fr: Locale;
}

export const {
  ...
} = createI18n<any, Locales>({
  en: () => import('./en.json'),
  fr: () => import('./fr.json'),
})

Load initial locales client-side

Warning: This should not be used unless you know what you're doing and what that implies.

If for x reason you don't want to SSR the initial locale, you can load it on the client. Simply remove the getLocaleProps from your pages.

You can also provide a fallback component while waiting for the initial locale to load inside I18nProvider:

<I18nProvider locale={pageProps.locale} fallback={<p>Loading locales...</p>}>
  ...
</I18nProvider>

Type-safety on locales files

Using defineLocale, you can make sure all your locale files implements all the keys of the base locale:

// locales/index.ts
export const {
  defineLocale
  ...
} = createI18n({
  ...
})

It's a simple wrapper function around other locales:

// locales/fr.ts
export default defineLocale({
  hello: 'Bonjour',
  welcome: 'Bonjour {name}!',
});

Use the types for my own library

We also provide a separate package called international-types that contains the utility types for next-international. You can build a library on top of it and get the same awesome type-safety.

Testing

In case you want to make tests with next-international, you will need to create a custom render. The following example uses @testing-library and Vitest, but should work with Jest too.

// customRender.tsx
import { cleanup, render } from '@testing-library/react';
import { afterEach } from 'vitest';

afterEach(() => {
  cleanup();
});

const customRender = (ui: React.ReactElement, options = {}) =>
  render(ui, {
    // wrap provider(s) here if needed
    wrapper: ({ children }) => children,
    ...options,
  });

export * from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event';
// override render export
export { customRender as render };

You will also need a locale created, or one for testing purposes.

// en.ts
export default {
  hello: 'Hello',
} as const;

Then, you can later use it in your tests like this.

// *.test.tsx
import { describe, vi } from 'vitest';
import { createI18n } from 'next-international';
import { render, screen, waitFor } from './customRender'; // Our custom render function.
import en from './en'; // Your locales.

// Don't forget to mock the "next/router", not doing this may lead to some console errors.
beforeEach(() => {
  vi.mock('next/router', () => ({
    useRouter: vi.fn().mockImplementation(() => ({
      locale: 'en',
      defaultLocale: 'en',
      locales: ['en', 'fr'],
    })),
  }));
});

afterEach(() => {
  vi.clearAllMocks();
});

describe('Example test', () => {
  it('just an example', async () => {
    const { useI18n, I18nProvider } = createI18n<typeof import('./en')>({
      en: () => import('./en'),
      // Other locales you might have.
    });

    function App() {
      const t = useI18n();

      return <p>{t('hello')}</p>;
    }

    render(
      <I18nProvider locale={en}>
        <App />
      </I18nProvider>,
    );

    expect(screen.queryByText('Hello')).not.toBeInTheDocument();

    await waitFor(() => {
      expect(screen.getByText('Hello')).toBeInTheDocument();
    });
  });
});

License

MIT