Skip to content

Commit

Permalink
i18n Setup : Simpler configuration + examples (#1847)
Browse files Browse the repository at this point in the history
* Replace example with createMany

* change comments + add an example for all databases

* return to current seed.js

* Rewrite a simpler configuration of i18n

* Add a template for translation locale files

* add sync node_module and locale files

* add comment and todo

* return to path.join

* exemple with default generate home page

* remove async for writeFile tasks

* missing a "," in json files

* add LanguageDetector

* add error handling

* remove "See this doc for info:"

* Fix typo

Co-authored-by: Claire Froelich <claire.froelich@gmail.com>

* Update packages/cli/src/commands/setup/i18n/i18n.js

* index --> app

* fix path.join error

* add import './i18n' after the last import

* clean addI18nImport function

* fix "Module not found: Error: Can't resolve ..."

* function name typo

* remove Sync yarn.lock step

Co-authored-by: Claire Froelich <claire.froelich@gmail.com>
Co-authored-by: David Price <thedavid@thedavidprice.com>
  • Loading branch information
3 people committed Mar 9, 2021
1 parent 4a899b5 commit 42ec5eb
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 85 deletions.
155 changes: 124 additions & 31 deletions packages/cli/src/commands/setup/i18n/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,152 @@ export const builder = (yargs) => {
})
}

const APP_JS_PATH = getPaths().web.app

const i18nImportExist = (appJS) => {
let content = appJS.toString()

const hasBaseImport = () => /import '.\/i18n'/.test(content)

return hasBaseImport()
}
const addI18nImport = (appJS) => {
var content = appJS.toString().split('\n').reverse()
const index = content.findIndex((value) => /import/.test(value))
content.splice(index, 0, "import './i18n'")
return content.reverse().join(`\n`)
}

const i18nConfigExists = () => {
return fs.existsSync(path.join(getPaths().web.src, 'i18n.js'))
}
const localesExists = (lng) => {
return fs.existsSync(path.join(getPaths().web.src, 'locales', lng + '.json'))
}

export const handler = async ({ force }) => {
const INDEX_JS_PATH = getPaths().web.app
const tasks = new Listr([
{
title: 'Installing packages...',
task: async () => {
await execa('yarn', [
'workspace',
'web',
'add',
'i18n',
'i18next',
'i18next-browser-languagedetector',
'i18next-http-backend',
'react-i18next',
return new Listr([
{
title:
'Install i18n, i18next, react-i18next and i18next-browser-languagedetector',
task: async () => {
/**
* Install i18n, i18next, react-i18next and i18next-browser-languagedetector
*/
await execa('yarn', [
'workspace',
'web',
'add',
'i18n',
'i18next',
'react-i18next',
'i18next-browser-languagedetector',
])
},
},
])
},
},
{
title: 'Configuring i18n...',
task: () => {
/**
* Write i18n.js in web/src
* Write i18n.js in web/src
*
* Check if i18n config already exists.
* If it exists, throw an error.
*/
return writeFile(
path.join(getPaths().web.src, 'i18n.js'),
fs
.readFileSync(
path.resolve(__dirname, 'templates', 'i18n.js.template')
)
.toString(),
{ overwriteExisting: force }
)
if (!force && i18nConfigExists()) {
throw new Error(
'i18n config already exists.\nUse --force to override existing config.'
)
} else {
return writeFile(
path.join(getPaths().web.src, 'i18n.js'),
fs
.readFileSync(
path.resolve(__dirname, 'templates', 'i18n.js.template')
)
.toString(),
{ overwriteExisting: force }
)
}
},
},
{
title: "Adding locale file for 'site' namespace",
task() {
return writeFile(getPaths().web.src + '/locales/en/site.json')
title: 'Adding locale file for French...',
task: () => {
/**
* Make web/src/locales if it doesn't exist
* and write fr.json there
*
* Check if fr.json already exists.
* If it exists, throw an error.
*/

if (!force && localesExists('fr')) {
throw new Error(
'fr.json config already exists.\nUse --force to override existing config.'
)
} else {
return writeFile(
path.join(getPaths().web.src, '/locales/fr.json'),
fs
.readFileSync(
path.resolve(__dirname, 'templates', 'fr.json.template')
)
.toString(),
{ overwriteExisting: force }
)
}
},
},
{
title: 'Adding import to App.{js,tsx}...',
title: 'Adding locale file for English...',
task: () => {
/**
* Add i18n import to the top of App.{js,tsx}
* Make web/src/locales if it doesn't exist
* and write en.json there
*
* Check if en.json already exists.
* If it exists, throw an error.
*/
let indexJS = fs.readFileSync(INDEX_JS_PATH)
indexJS = [`import './i18n'`, indexJS].join(`\n`)
fs.writeFileSync(INDEX_JS_PATH, indexJS)
if (!force && localesExists('en')) {
throw new Error(
'en.json already exists.\nUse --force to override existing config.'
)
} else {
return writeFile(
path.join(getPaths().web.src, '/locales/en.json'),
fs
.readFileSync(
path.resolve(__dirname, 'templates', 'en.json.template')
)
.toString(),
{ overwriteExisting: force }
)
}
},
},
{
title: 'Adding import to App.{js,tsx}...',
task: (_ctx, task) => {
/**
* Add i18n import to the last import of App.{js,tsx}
*
* Check if i18n import already exists.
* If it exists, throw an error.
*/
let appJS = fs.readFileSync(APP_JS_PATH)
if (i18nImportExist(appJS)) {
task.skip('Import already exists in App.js')
} else {
fs.writeFileSync(APP_JS_PATH, addI18nImport(appJS))
}
},
},
{
Expand All @@ -79,9 +175,6 @@ export const handler = async ({ force }) => {
${chalk.hex('#e8e8e8')(
'https://react.i18next.com/guides/quick-start/'
)}
${chalk.hex('#e8e8e8')(
'https://github.com/i18next/i18next-browser-languageDetector\n'
)}
`
},
},
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/commands/setup/i18n/templates/en.json.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Welcome to RedwoodJS": "Welcome to RedwoodJS",
"info": "This is your English translation file",
"see": "https://www.i18next.com/translation-function/essentials",
"HomePage" : {
"title": "Home Page",
"info": "Find me in",
"route": "My default route is named",
"link": "link to me with"
}
}
11 changes: 11 additions & 0 deletions packages/cli/src/commands/setup/i18n/templates/fr.json.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Welcome to RedwoodJS": "Bienvenu sur RedwoodJS",
"info": "Ceci est votre fichier de traduction",
"see": "https://www.i18next.com/translation-function/essentials",
"HomePage": {
"title": "Page d'accueil",
"info": "Trouve moi dans",
"route": "Ma route par default se nomme",
"link": "le lien vers moi avec"
}
}
97 changes: 43 additions & 54 deletions packages/cli/src/commands/setup/i18n/templates/i18n.js.template
Original file line number Diff line number Diff line change
@@ -1,64 +1,53 @@
import i18n from 'i18next'
import HttpApi from 'i18next-http-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import fr from './locales/fr.json'
import en from './locales/en.json'

// This is a simple i18n configuration with English and French translation.
// You can find the translation on web/src/locales/{language}.json
// see : https://react.i18next.com
// Here an example of how to use it in your components, pages or layouts :
/*
import { Link, routes } from '@redwoodjs/router'
import { useTranslation } from 'react-i18next'

const HomePage = () => {
const { t, i18n } = useTranslation()
return (
<>
<h1>{t('HomePage.title')}</h1>
<button onClick={() => i18n.changeLanguage('fr')}>fr</button>
<button onClick={() => i18n.changeLanguage('en')}>en</button>
<p>
{t('HomePage.info')} <code>./web/src/pages/HomePage/HomePage.js</code>
</p>
<p>
{t('HomePage.route')} <code>home</code>, {t('HomePage.link')}`
<Link to={routes.home()}>Home</Link>`
</p>
</>
)
}

export default HomePage
*/
i18n
.use(HttpApi)
.use(LanguageDetector)
.use(initReactI18next)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
.init({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
addPath: '/locales/{{lng}}/{{ns}}.json',
},
load: 'all',
ns: ['site'],
defaultNS: 'site',
fallbackNS: 'site',
fallbackLng: 'en',
whitelist: ['en'],
preload: ['en'],
interpolation: { escapeValue: false }, // React already does escaping
lng: 'en',
lowerCaseLng: true,
// saveMissing: true,
initImmediate: true,
detection: {
// order and from where user language should be detected
order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag', 'path', 'subdomain'],

// keys or params to lookup language from
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
lookupSessionStorage: 'i18nextLng',
lookupFromPathIndex: 0,
lookupFromSubdomainIndex: 0,

// cache user language on
caches: ['localStorage', 'cookie'],
excludeCacheFor: ['cimode'], // languages to not persist (cookie, localStorage)

// optional expire and domain for set cookie
cookieMinutes: 10,
cookieDomain: 'myDomain',

// optional htmlTag with lang attribute, the default is:
htmlTag: document.documentElement,

// optional set cookie options, reference:[MDN Set-Cookie docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)
cookieOptions: { path: '/', sameSite: 'strict' }
},
react: {
wait: true,
useSuspense: false,
transSupportBasicHtmlNodes: true,
},
interpolation: {
escapeValue: false, // react already safes from xss
fallbackLng: 'en',
resources: {
en: {
translation: en,
},
fr: {
translation: fr,
},
},
nsSeparator: ':',
keySeparator: '.',
})

export default i18n

0 comments on commit 42ec5eb

Please sign in to comment.