diff --git a/examples/with-i18n-rosetta/README.md b/examples/with-i18n-rosetta/README.md new file mode 100644 index 0000000000000..fb932e1dbdb90 --- /dev/null +++ b/examples/with-i18n-rosetta/README.md @@ -0,0 +1,44 @@ +# rosetta example + +This example uses [rosetta](https://github.com/lukeed/rosetta), react hooks and context to provide a SSR, SSG, CSR compatible i18n solution. + +In `next.config.js` you can configure the fallback language. + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-i18n-rosetta) + +## How to use + +### Using `create-next-app` + +Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +npm init next-app --example with-i18n-rosetta with-i18n-rosetta +# or +yarn create next-app --example with-i18n-rosetta with-i18n-rosetta +``` + +### Download manually + +Download the example: + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-i18n-rosetta +cd with-i18n-rosetta +``` + +Install it and run: + +```bash +npm install +npm run dev +# or +yarn +yarn dev +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/with-i18n-rosetta/components/title.js b/examples/with-i18n-rosetta/components/title.js new file mode 100644 index 0000000000000..912afe370c241 --- /dev/null +++ b/examples/with-i18n-rosetta/components/title.js @@ -0,0 +1,6 @@ +import useI18n from '../hooks/use-i18n' + +export default function Title({ username }) { + const i18n = useI18n() + return

{i18n.t('intro.welcome', { username })}

+} diff --git a/examples/with-i18n-rosetta/hooks/use-i18n.js b/examples/with-i18n-rosetta/hooks/use-i18n.js new file mode 100644 index 0000000000000..457eb9add8d78 --- /dev/null +++ b/examples/with-i18n-rosetta/hooks/use-i18n.js @@ -0,0 +1,7 @@ +import { useContext } from 'react' +import { I18nContext } from '../lib/i18n' + +export default function useI18n() { + const i18n = useContext(I18nContext) + return i18n +} diff --git a/examples/with-i18n-rosetta/lib/i18n.js b/examples/with-i18n-rosetta/lib/i18n.js new file mode 100644 index 0000000000000..6353c0d6a4378 --- /dev/null +++ b/examples/with-i18n-rosetta/lib/i18n.js @@ -0,0 +1,57 @@ +import { createContext, useState, useRef, useEffect } from 'react' +import rosetta from 'rosetta' +// import rosetta from 'rosetta/debug'; + +const i18n = rosetta() + +export const defaultLanguage = 'en' +export const languages = ['de', 'en'] +export const contentLanguageMap = { de: 'de-DE', en: 'en-US' } + +export const I18nContext = createContext() + +// 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), + locale: (l, dict) => { + i18n.locale(l) + activeLocaleRef.current = l + if (dict) { + i18n.set(l, dict) + setActiveDict(dict) + } else { + setTick(tick => tick + 1) + } + }, + } + + return ( + {children} + ) +} diff --git a/examples/with-i18n-rosetta/locales/de.json b/examples/with-i18n-rosetta/locales/de.json new file mode 100644 index 0000000000000..5bd93d0cfbf7c --- /dev/null +++ b/examples/with-i18n-rosetta/locales/de.json @@ -0,0 +1,10 @@ +{ + "intro": { + "welcome": "Willkommen, {{username}}!", + "text": "Ich hoffe, du findest das nützlich.", + "description": "Das Beispiel zeigt, wie man die Sprache für SSG und SSG optimierte Seiten wechselt." + }, + "dashboard": { + "description": "Das Beispiel zeigt, wie man die Sprache nur Frontendseitig verändert. Nützlich für Dashboards wo SEO nicht relevant ist." + } +} diff --git a/examples/with-i18n-rosetta/locales/en.json b/examples/with-i18n-rosetta/locales/en.json new file mode 100644 index 0000000000000..070362e7809f3 --- /dev/null +++ b/examples/with-i18n-rosetta/locales/en.json @@ -0,0 +1,10 @@ +{ + "intro": { + "welcome": "Welcome, {{username}}!", + "text": "I hope you find this useful.", + "description": "This example demonstrate how to change the language for SSG and SSR optimized pages." + }, + "dashboard": { + "description": "This example demonstrate how to change the language on client side only. Useful for dashboards because they don't require SEO." + } +} diff --git a/examples/with-i18n-rosetta/next.config.js b/examples/with-i18n-rosetta/next.config.js new file mode 100644 index 0000000000000..3e6c176989606 --- /dev/null +++ b/examples/with-i18n-rosetta/next.config.js @@ -0,0 +1,13 @@ +module.exports = { + experimental: { + redirects() { + return [ + { + source: '/', + permanent: true, + destination: '/en', + }, + ] + }, + }, +} diff --git a/examples/with-i18n-rosetta/package.json b/examples/with-i18n-rosetta/package.json new file mode 100644 index 0000000000000..06d61053b968a --- /dev/null +++ b/examples/with-i18n-rosetta/package.json @@ -0,0 +1,16 @@ +{ + "name": "with-rosetta", + "version": "1.0.0", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "latest", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "rosetta": "1.0.0" + }, + "license": "ISC" +} diff --git a/examples/with-i18n-rosetta/pages/[lng]/index.js b/examples/with-i18n-rosetta/pages/[lng]/index.js new file mode 100644 index 0000000000000..0870f5055af7d --- /dev/null +++ b/examples/with-i18n-rosetta/pages/[lng]/index.js @@ -0,0 +1,46 @@ +import Link from 'next/link' +import Head from 'next/head' +import Title from '../../components/title' +import useI18n from '../../hooks/use-i18n' +import { languages, contentLanguageMap } from '../../lib/i18n' + +const HomePage = () => { + const i18n = useI18n() + + return ( +
+ + + + + <h2>{i18n.t('intro.text')}</h2> + <h3>{i18n.t('intro.description')}</h3> + <div>Current locale: {i18n.activeLocale}</div> + <Link href="/de"> + <a>Use client-side routing to change language to 'de'</a> + </Link> + </div> + ) +} + +export async function getStaticProps({ params }) { + const { default: lngDict = {} } = await import( + `../../locales/${params.lng}.json` + ) + + return { + props: { lng: params.lng, lngDict }, + } +} + +export async function getStaticPaths() { + return { + paths: languages.map(l => ({ params: { lng: l } })), + fallback: false, + } +} + +export default HomePage diff --git a/examples/with-i18n-rosetta/pages/_app.js b/examples/with-i18n-rosetta/pages/_app.js new file mode 100644 index 0000000000000..f2f8d1db40a19 --- /dev/null +++ b/examples/with-i18n-rosetta/pages/_app.js @@ -0,0 +1,10 @@ +import React from 'react' +import I18n from '../lib/i18n' + +export default function MyApp({ Component, pageProps }) { + return ( + <I18n lngDict={pageProps.lngDict} locale={pageProps.lng}> + <Component {...pageProps} /> + </I18n> + ) +} diff --git a/examples/with-i18n-rosetta/pages/dashboard.js b/examples/with-i18n-rosetta/pages/dashboard.js new file mode 100644 index 0000000000000..7d7aeb6af365d --- /dev/null +++ b/examples/with-i18n-rosetta/pages/dashboard.js @@ -0,0 +1,41 @@ +import { useEffect } from 'react' +import Head from 'next/head' +import useI18n from '../hooks/use-i18n' +import Title from '../components/title' +import { contentLanguageMap } from '../lib/i18n' +import EN from '../locales/en.json' +import DE from '../locales/de.json' + +const Dashboard = () => { + const i18n = useI18n() + + useEffect(() => { + i18n.locale('en', EN) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + <div> + <Head> + <meta + httpEquiv="content-language" + content={contentLanguageMap[i18n.activeLocale]} + /> + </Head> + <Title username="Peter" /> + <h2>{i18n.t('intro.text')}</h2> + <h3>{i18n.t('dashboard.description')}</h3> + <div>Current locale: {i18n.activeLocale}</div> + <a + href="#" + onClick={() => { + i18n.locale('de', DE) + }} + > + Change language client-side to 'de' + </a> + </div> + ) +} + +export default Dashboard