Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rosetta i18n example #11841

Merged
merged 16 commits into from
Apr 21, 2020
Merged
44 changes: 44 additions & 0 deletions examples/with-rosetta/README.md
Original file line number Diff line number Diff line change
@@ -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 [ZEIT Now](https://zeit.co/now):

[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/import/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-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-rosetta with-rosetta
# or
yarn create next-app --example with-rosetta with-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-rosetta
cd with-rosetta
```

Install it and run:

```bash
npm install
npm run dev
# or
yarn
yarn dev
```

Deploy it to the cloud with [ZEIT Now](https://zeit.co/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
39 changes: 39 additions & 0 deletions examples/with-rosetta/hooks/use-i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useContext, useState, useRef, useEffect } from 'react'
import { I18nContext, defaultLanguage } from '../lib/i18n'

export default function useI18n(loc) {
// OR detect language based on navigator.language OR user setting (cookie)
const activeLocaleRef = useRef(loc || defaultLanguage)
const [, setTick] = useState(0)

const i18n = useContext(I18nContext)
if (loc) {
i18n.locale(loc)
}

useEffect(() => {
if (loc) {
i18n.locale(loc)
activeLocaleRef.current = loc
// force rerender
setTick(tick => tick + 1)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loc])

return {
activeLocale: activeLocaleRef.current,
set: (...args) => {
i18n.set(...args)
// force rerender
setTick(tick => tick + 1)
},
t: (...args) => i18n.t(...args),
locale: l => {
i18n.locale(l)
activeLocaleRef.current = l
// force rerender
setTick(tick => tick + 1)
},
}
}
38 changes: 38 additions & 0 deletions examples/with-rosetta/lib/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createContext } from 'react'
import rosetta from 'rosetta'

// import rosetta from 'rosetta/debug';

const i18n = rosetta({
en: {
contact: {
email: '[email protected]',
},
intro: {
welcome: 'Welcome, {{username}}!',
text: 'I hope you find this useful.',
},
},
de: {
contact: {
email: '[email protected]',
},
intro: {
welcome: 'Willkommen, {{username}}!',
text: 'Ich hoffe, du findest das nützlich.',
},
},
})

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 }) {
return <I18nContext.Provider value={i18n}>{children}</I18nContext.Provider>
}
15 changes: 15 additions & 0 deletions examples/with-rosetta/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
experimental: {
pages404: true,
polyfillsOptimization: true,
redirects() {
return [
{
source: '/',
permanent: true,
destination: '/en',
},
]
},
},
}
16 changes: 16 additions & 0 deletions examples/with-rosetta/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
42 changes: 42 additions & 0 deletions examples/with-rosetta/pages/[lng]/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Link from 'next/link'
import Head from 'next/head'
import useI18n from '../../hooks/use-i18n'
import { languages, contentLanguageMap } from '../../lib/i18n'

const HomePage = ({ lng }) => {
const i18n = useI18n(lng)

console.log(lng)

return (
<div>
<Head>
<meta
http-equiv="content-language"
content={contentLanguageMap[i18n.activeLocale]}
/>
</Head>
<h1>{i18n.t('intro.welcome', { username: 'Peter' })}</h1>
<h3>{i18n.t('intro.text')}</h3>
<div>Current locale: {i18n.activeLocale}</div>
<Link href="/de">
<a>Change language SSG to 'de'</a>
</Link>
</div>
)
}

export async function getStaticProps({ params }) {
return {
props: { lng: params.lng },
}
}

export async function getStaticPaths() {
return {
paths: languages.map(l => ({ params: { lng: l } })),
fallback: true,
}
}

export default HomePage
10 changes: 10 additions & 0 deletions examples/with-rosetta/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import I18n from '../lib/i18n'

export default function MyApp({ Component, pageProps, store }) {
return (
<I18n>
<Component {...pageProps} />
</I18n>
)
}
31 changes: 31 additions & 0 deletions examples/with-rosetta/pages/contact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Link from 'next/link'
import Head from 'next/head'
import useI18n from '../hooks/use-i18n'
import { contentLanguageMap } from '../lib/i18n'

const Contact = ({ lng }) => {
const i18n = useI18n(lng)

return (
<div>
<Head>
<meta http-equiv="content-language" content={contentLanguageMap[lng]} />
</Head>
<h1>{i18n.t('contact.email')}</h1>
<div>Current locale: {i18n.activeLocale}</div>
<Link href={{ pathname: '/contact', query: { lng: 'de' } }}>
<a>Change language SSR to 'de'</a>
</Link>
</div>
)
}

export async function getServerSideProps({ query }) {
return {
props: {
lng: query.lng || 'en', // OR detect default language based on header OR user setting
}, // will be passed to the page component as props
}
}

export default Contact
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this page to pages/[lng]/contact.js and use getStaticProps, there's no good use case for having a serverless function decide the language of the page if you can do that at build time 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right but it must not be a good use case. I want to demonstrate the different modes.

Copy link
Member

@lfades lfades Apr 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prefer to demonstrate good use cases in examples always, rather than show the usage of some method, we have docs and learning lessons for that.

For this case getStaticProps is better.

Copy link
Contributor Author

@StarpTech StarpTech Apr 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you get my point? I'd like to demonstrate that the i18n solution works in all modes CSR, SSG, SSR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and it surely does, but you don't have to demonstrate it, instead you have to show the community the best way of using it 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth having 3 separate examples? Whichever you decide should be default behavior can be with-i18n and then you can have the two other mode be with-i18n-(csr|ssg).

While I understand and relate to both points, I do think as an official example, something should be as concise and as close to prod-ready as possible instead of forcing the user to figure out what's actually core/necessary.

I suggest with-i18n since no one will know what "rosetta" is when browsing the directory list ¯\(ツ)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lfades I updated it. @lukeed that was my first idea too but there are other i18n solutions here. I renamed it to with-rosetta-i18n

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, then with-i18n-rosetta would be better for grouping 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

18 changes: 18 additions & 0 deletions examples/with-rosetta/pages/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import useI18n from '../hooks/use-i18n'

const Dashboard = () => {
const i18n = useI18n()

return (
<div>
<h1>{i18n.t('intro.welcome', { username: 'Peter' })}</h1>
<h3>Client side only.</h3>
<div>Current locale: {i18n.activeLocale}</div>
<a href="#" onClick={() => i18n.locale('de')}>
Change language client-side to 'de'
</a>
</div>
)
}

export default Dashboard
StarpTech marked this conversation as resolved.
Show resolved Hide resolved