npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@typebot.io/next-international

v0.3.8

Published

Type-safe internationalization (i18n) for Next.js

Downloads

2

Readme


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'
import type Locale from './en'

export const { useI18n, I18nProvider, getLocaleProps } = createI18n<typeof Locale>({
  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):
// locales/index.tsx
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 scoped translations with the scopedT function from useI18n:

const { scopedT } = useI18n()
const t = scopedT('pages.settings')

t('title')
t('description', { identifier })
t('ct')

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

Change current locale

Export useChangeLocale from createI18n:

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

Then use this as a hook:

import { useChangeLocale } from '../locales'

function App() {
  const changeLocale = useChangeLocale()

  return (
    <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: https://github.com/microsoft/TypeScript/issues/32063.

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

// locales/index.ts
import { createI18n } from 'next-international'
import type Locale from './en.json'

export const { useI18n, I18nProvider, getLocaleProps } = createI18n<typeof Locale>({
  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, BaseLocale } from 'next-international'

interface Locale extends BaseLocale {
  'hello': string
  'welcome': string
}

export const {
  ...
} = createI18n<Locale>({
  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