Next.jsをRosettaで多言語対応したメモ

## 概要
Next.jsの多言語化。
[Rosetta Example](https://github.com/zeit/next.js/tree/canary/examples/with-i18n-rosetta)をTypescriptで書いてみたメモ。

[この時点のソース](https://github.com/hibohiboo/create-now/tree/cc6d3043f909eb112982f33bca3ac8bd7d0287e8/)

// 追記
[型を付けたメモ](https://qiita.com/hibohiboo/items/2824fc2030ef3b91c479)で、型をちゃんと付けたので、そちらも参照のこと。

## ソース

```tsx:lib/i18n.tsx
import { createContext, useState, useRef, useEffect } from 'react'
import rosetta from 'rosetta'

const i18n = rosetta()

export const defaultLanguage = 'ja'
export const languages = ['ja', 'en']
export const contentLanguageMap = { ja: 'ja-JP', en: 'en-US' }

export const I18nContext = createContext(null)

// default language
i18n.locale(defaultLanguage)

export default function I18n({ children, locale, lngDict }) {
  const [activeDict, setActiveDict] = useState(() => lngDict)
  const activeLocaleRef = useRef(locale || defaultLanguage)
  const [, setTick] = useState(0)
  const firstRender = useRef(true)

  // for initial SSR render
  if (locale && firstRender.current === true) {
    firstRender.current = false
    i18n.locale(locale)
    i18n.set(locale, activeDict)
  }

  useEffect(() => {
    if (locale) {
      i18n.locale(locale)
      i18n.set(locale, activeDict)
      activeLocaleRef.current = locale
      // force rerender
      setTick((tick) => tick + 1)
    }
  }, [locale, activeDict])

  const i18nWrapper = {
    activeLocale: activeLocaleRef.current,
    t: (...args) => i18n.t(...(args as [any, ...any[]])),
    locale: (l, dict) => {
      i18n.locale(l)
      activeLocaleRef.current = l
      if (dict) {
        i18n.set(l, dict)
        setActiveDict(dict)
      } else {
        setTick((tick) => tick + 1)
      }
    },
  }

  return (
    <I18nContext.Provider value={i18nWrapper}>{children}</I18nContext.Provider>
  )
}
```


```tsx:hooks/use-i18n.tsx
import { useContext } from 'react'
import { I18nContext } from '~/lib/i18n'

export default function useI18n() {
  const i18n = useContext(I18nContext)
  return i18n
}
```

```diff:pages/_app.tsx
import * as React from 'react'
import { Provider } from 'react-redux'
import App from 'next/app'
import { ThemeProvider } from '@material-ui/core/styles'
import CssBaseline from '@material-ui/core/CssBaseline'
import Head from 'next/head'
import { setupStore } from '~/store'
import theme from '~/theme'
import '~/styles/global.css'
+ import I18n from '~/lib/i18n'

const store = setupStore()

class MyApp extends App {
  componentDidMount() {
    // Remove the server-side injected CSS.
    const jssStyles = document.querySelector('#jss-server-side')
    if (jssStyles) {
      jssStyles.parentElement.removeChild(jssStyles)
    }
  }
  public render() {
    const { Component, pageProps } = this.props
    return (
      <Provider store={store}>
        <React.Fragment>
          <Head>
            <title>Create Now</title>
            <link rel="icon" href="/favicon.ico" />
          </Head>
          <ThemeProvider theme={theme}>
            {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
            <CssBaseline />
+            <I18n lngDict={pageProps.lngDict} locale={pageProps.lng}>
              <Component {...pageProps} />
+            </I18n>
          </ThemeProvider>
        </React.Fragment>
      </Provider>
    )
  }
}

export default MyApp
```

```diff:pages/lostrpg/index.tsx
import { useEffect } from 'react'
import { NextPage } from 'next'
import Head from 'next/head'
import Link from '~/components/atoms/mui/Link'
import Container from '~/components/organisms/lostrpg/LostrpgContainer'
+ import useI18n from '~/hooks/use-i18n'
+ import { contentLanguageMap } from '~/lib/i18n'
+ import EN from '~/locales/en.json'
+ import JA from '~/locales/ja.json'

const Page: NextPage = () => {
+ const i18n = useI18n()


+  useEffect(() => {
+    i18n.locale('ja', JA)
+  }, [])
  return (
    <Container>
      <Head>
        <meta
+          httpEquiv="content-language"
+          content={contentLanguageMap[i18n.activeLocale]}
        />
+        <title>{i18n.t('lostrpg.index.title')}</title>
      </Head>
      <h2>{i18n.t('lostrpg.index.title')}</h2>
      <div style={{ padding: '5px' }}>
        <a
          href="#"
          onClick={() => {
+           i18n.locale('en', EN)
          }}
        >
          English
        </a>

        <a
          href="#"
          style={{ marginLeft: '10px' }}
          onClick={() => {

+           i18n.locale('ja', JA)
          }}
        >
          日本語
        </a>
      </div>
      <ul>
        <li>
          <Link href="/lostrpg/camps/list">
+            {i18n.t('lostrpg.index.campList')}
          </Link>
        </li>
      </ul>

      <Link href="/"> {i18n.t('common.back')}</Link>
    </Container>
  )
}

export default Page
```



## 参考

[TypeScript + React で i18n (国際化/多言語) 対応を楽して続けるためのアレコレ](https://qiita.com/mojibakeo/items/25930bb6eee33594af54)
[TypeScriptで最低n個の要素を持った配列の型を宣言する方法](https://qiita.com/uhyo/items/80ce7c00f413c1d1be56)
[rosetta](https://github.com/lukeed/rosetta)