diff --git a/packages/cli/src/commands/setup/i18n/i18n.js b/packages/cli/src/commands/setup/i18n/i18n.js index 64dc6e498dfe..dd0d2ca79b12 100644 --- a/packages/cli/src/commands/setup/i18n/i18n.js +++ b/packages/cli/src/commands/setup/i18n/i18n.js @@ -19,21 +19,53 @@ 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', + ]) + }, + }, ]) }, }, @@ -41,34 +73,98 @@ export const handler = async ({ force }) => { 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)) + } }, }, { @@ -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' - )} ` }, }, diff --git a/packages/cli/src/commands/setup/i18n/templates/en.json.template b/packages/cli/src/commands/setup/i18n/templates/en.json.template new file mode 100644 index 000000000000..aa0e11b73b6c --- /dev/null +++ b/packages/cli/src/commands/setup/i18n/templates/en.json.template @@ -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" + } + } diff --git a/packages/cli/src/commands/setup/i18n/templates/fr.json.template b/packages/cli/src/commands/setup/i18n/templates/fr.json.template new file mode 100644 index 000000000000..7fd9454d7033 --- /dev/null +++ b/packages/cli/src/commands/setup/i18n/templates/fr.json.template @@ -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" + } +} diff --git a/packages/cli/src/commands/setup/i18n/templates/i18n.js.template b/packages/cli/src/commands/setup/i18n/templates/i18n.js.template index a9277fe19a85..891897093f03 100644 --- a/packages/cli/src/commands/setup/i18n/templates/i18n.js.template +++ b/packages/cli/src/commands/setup/i18n/templates/i18n.js.template @@ -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 ( + <> +

{t('HomePage.title')}

+ + +

+ {t('HomePage.info')} ./web/src/pages/HomePage/HomePage.js +

+

+ {t('HomePage.route')} home, {t('HomePage.link')}` + Home` +

+ + ) +} + +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