From f1b4417e00836480278441d6a6c786da5ba4648a Mon Sep 17 00:00:00 2001 From: Adam Brauer <400763+ambrauer@users.noreply.github.com> Date: Tue, 17 Nov 2020 07:48:13 -0600 Subject: [PATCH] [NextJs] i18n updates (#493) * Source Next.js i18n defaultLocale from package config * Fallback to using package config language for page props (allows sample to run when Next.js i18n is disabled) * Add locale variants of configured rewrites * Upgraded next-localization (w/ rosetta) library as dictionary solution of choice (atm). Simplified usage examples. Updated comments in _app.tsx. * Remove export scripts and build logic for now (since it doesn't work with next.js v10 i18n and image optimization), to be addressed in separate task --- samples/nextjs/next.config.js | 45 ++++++++++++++----- samples/nextjs/package-lock.json | 6 +-- samples/nextjs/package.json | 4 +- samples/nextjs/src/components/Layout.tsx | 8 ++-- .../Styleguide-Multilingual/index.tsx | 6 +-- samples/nextjs/src/pages/[[...path]].tsx | 39 ++++++---------- samples/nextjs/src/pages/_app.tsx | 5 ++- .../src/pages_examples/[[...path]].SSR.tsx | 4 +- 8 files changed, 66 insertions(+), 51 deletions(-) diff --git a/samples/nextjs/next.config.js b/samples/nextjs/next.config.js index ec4ad2fad3..078f4746e9 100644 --- a/samples/nextjs/next.config.js +++ b/samples/nextjs/next.config.js @@ -1,20 +1,25 @@ const jssConfig = require('./src/temp/config'); +const packageConfig = require('./package.json').config; const disconnectedServerUrl = `http://localhost:${process.env.DISCONNECTED_SERVER_PORT || 3042}/`; const disconnected = process.env.JSS_MODE === 'disconnected'; module.exports = (phase) => { - + const env = { // Expose current Next.js phase as an environment variable // See available phases here: https://github.com/vercel/next.js/blob/canary/packages/next/next-server/lib/constants.ts#L1-L4 - NEXT_PHASE: phase - } + NEXT_PHASE: phase, + }; const i18n = { + // These are all the locales you want to support in your application. + // These should generally match (or at least be a subset of) those in Sitecore. locales: ['en', 'da-DK'], - defaultLocale: 'en' - } + // This is the locale that will be used when visiting a non-locale + // prefixed path e.g. `/styleguide`. + defaultLocale: packageConfig.language, + }; async function rewrites() { if (disconnected) { @@ -24,12 +29,20 @@ module.exports = (phase) => { source: '/sitecore/:path*', destination: `${disconnectedServerUrl}/sitecore/:path*`, }, + { + source: '/:locale/sitecore/:path*', + destination: `${disconnectedServerUrl}/sitecore/:path*`, + }, // media items { source: '/data/media/:path*', destination: `${disconnectedServerUrl}/data/media/:path*`, }, - ] + { + source: '/:locale/data/media/:path*', + destination: `${disconnectedServerUrl}/data/media/:path*`, + }, + ]; } else { // When in connected mode we want to proxy Sitecore paths off to Sitecore return [ @@ -37,23 +50,35 @@ module.exports = (phase) => { source: '/sitecore/:path*', destination: `${jssConfig.sitecoreApiHost}/sitecore/:path*`, }, + { + source: '/:locale/sitecore/:path*', + destination: `${jssConfig.sitecoreApiHost}/sitecore/:path*`, + }, // media items { source: '/-/:path*', destination: `${jssConfig.sitecoreApiHost}/-/:path*`, }, + { + source: '/:locale/-/:path*', + destination: `${jssConfig.sitecoreApiHost}/-/:path*`, + }, // visitor identification { source: '/layouts/:path*', destination: `${jssConfig.sitecoreApiHost}/layouts/:path*`, }, - ] + { + source: '/:locale/layouts/:path*', + destination: `${jssConfig.sitecoreApiHost}/layouts/:path*`, + }, + ]; } - } + }; return { env, i18n, - rewrites + rewrites, }; -}; \ No newline at end of file +}; diff --git a/samples/nextjs/package-lock.json b/samples/nextjs/package-lock.json index 5d9b2764a9..b3d6ec7982 100644 --- a/samples/nextjs/package-lock.json +++ b/samples/nextjs/package-lock.json @@ -6330,9 +6330,9 @@ } }, "next-localization": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/next-localization/-/next-localization-0.9.3.tgz", - "integrity": "sha512-hUdxaOENTYZ2WsgpjMYTjVbTecL/EmFPXcaF9tsAFN2vC8uxXiMYNDNmCHgo+aJHmVjxtt2xcxh7CoQoxTnkPA==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/next-localization/-/next-localization-0.10.0.tgz", + "integrity": "sha512-VjooqaJBuHhqUFbeiEqsKrs9y1Ivzh+gc0aTsK2mRfQUxor0mS8zGQpQVUQQ+cMuKSWrBd70oXwi71U8PxF9TA==", "requires": { "eslint": "^7.12.1", "rosetta": "1.1.0" diff --git a/samples/nextjs/package.json b/samples/nextjs/package.json index 357f539371..2a9af9d1df 100644 --- a/samples/nextjs/package.json +++ b/samples/nextjs/package.json @@ -47,7 +47,7 @@ "graphql": "~14.5.7", "graphql-tag": "~2.10.1", "next": "^10.0.0", - "next-localization": "^0.9.3", + "next-localization": "^0.10.0", "nprogress": "~0.2.0", "react": "^16.9.0", "react-apollo": "~3.1.1", @@ -86,11 +86,9 @@ "lint": "eslint --fix ./src/**/*.tsx ./src/**/*.ts ./sitecore/definitions/**/*.ts ./scripts/**/*.ts ./data/**/*.yml", "bootstrap": "ts-node --project tsconfig.scripts.json scripts/bootstrap.ts", "build": "npm-run-all --serial bootstrap next:build", - "export": "cross-env-shell BUILD_MODE=export \"npm-run-all --serial bootstrap next:build next:export\"", "graphql:update": "ts-node --project tsconfig.scripts.json ./scripts/update-graphql-fragment-data.ts", "next:build": "next build", "next:dev": "cross-env NODE_OPTIONS='--inspect' next dev", - "next:export": "next export", "next:start": "next start", "scaffold": "ts-node --project tsconfig.scripts.json scripts/scaffold-component.ts", "start": "npm-run-all --serial bootstrap --parallel next:dev start:watch-components", diff --git a/samples/nextjs/src/components/Layout.tsx b/samples/nextjs/src/components/Layout.tsx index 7ec835c34f..738b9cea21 100644 --- a/samples/nextjs/src/components/Layout.tsx +++ b/samples/nextjs/src/components/Layout.tsx @@ -15,7 +15,7 @@ const LOGO_SIZE = { WIDTH: 221, HEIGHT: 48 }; // This is boilerplate navigation for sample purposes. Most apps should throw this away and use their own navigation implementation. // Most apps may also wish to use GraphQL for their navigation construction; this sample does not simply to support disconnected mode. const Navigation = () => { - const i18n = useI18n(); + const { t } = useI18n(); return (
@@ -38,13 +38,13 @@ const Navigation = () => { target="_blank" rel="noopener noreferrer" > - {i18n.t('Documentation')} + {t('Documentation')} - {i18n.t('Styleguide')} + {t('Styleguide')} - {i18n.t('GraphQL')} + {t('GraphQL')}
diff --git a/samples/nextjs/src/components/Styleguide-Multilingual/index.tsx b/samples/nextjs/src/components/Styleguide-Multilingual/index.tsx index cc9a75d9f7..15e68ef618 100644 --- a/samples/nextjs/src/components/Styleguide-Multilingual/index.tsx +++ b/samples/nextjs/src/components/Styleguide-Multilingual/index.tsx @@ -16,7 +16,7 @@ type StyleguideMultilingualProps = StyleguideComponentWithContextProps & * multiple languages. */ const StyleguideMultilingual = (props: StyleguideMultilingualProps): JSX.Element => { - const i18n = useI18n(); + const { t, locale } = useI18n(); return ( @@ -25,7 +25,7 @@ const StyleguideMultilingual = (props: StyleguideMultilingualProps): JSX.Element

This is a static dictionary entry from /data/dictionary :  - {i18n.t('styleguide-sample')} + {t('styleguide-sample')}

@@ -38,7 +38,7 @@ const StyleguideMultilingual = (props: StyleguideMultilingualProps): JSX.Element

-

The current language is: {i18n.locale()}

+

The current language is: {locale()}

); }; diff --git a/samples/nextjs/src/pages/[[...path]].tsx b/samples/nextjs/src/pages/[[...path]].tsx index deae538b91..9a76bb3db4 100644 --- a/samples/nextjs/src/pages/[[...path]].tsx +++ b/samples/nextjs/src/pages/[[...path]].tsx @@ -7,6 +7,7 @@ import { SitecorePageProps, extractPath } from 'lib/page-props'; import componentFactory from 'temp/componentFactory'; import { configBasedLayoutService as layoutService } from 'lib/layout-service'; import { configBasedDictionaryService as dictionaryService } from 'lib/dictionary-service'; +import { config as packageConfig } from '../../package.json'; const SitecorePage = ({ layoutData }: SitecorePageProps): JSX.Element => { if (!layoutData?.sitecore?.route) { @@ -32,30 +33,17 @@ const SitecorePage = ({ layoutData }: SitecorePageProps): JSX.Element => { // This function gets called at build and export time to determine // pages for SSG ("paths", as tokenized array). export const getStaticPaths: GetStaticPaths = async () => { - if (process.env.BUILD_MODE === 'export') { - // If performing an export, fallback is not allowed. - // Use hard-coded values for now for demo-purposes. - // Ultimately, this is where we'll request a "sitemap" from Sitecore. - return { - paths: [ - { params: { path: [''] }, locale: 'en' }, - { params: { path: ['styleguide'] }, locale: 'en' }, - { params: { path: ['styleguide', 'custom-route-type'] }, locale: 'en' }, - { params: { path: ['graphql'] }, locale: 'en' }, - ], - fallback: false, - }; - } else { - // Fallback, along with revalidate in getStaticProps (below), - // enables Incremental Static Regeneration. This allows us to - // leave certain (or all) paths empty if desired and static pages - // will be generated on request. - // See https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration - return { - paths: [], - fallback: 'blocking', - }; - } + // Fallback, along with revalidate in getStaticProps (below), + // enables Incremental Static Regeneration. This allows us to + // leave certain (or all) paths empty if desired and static pages + // will be generated on request. + // See https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration + // + // Ultimately, this is where we'll also be able to request a "sitemap" from Sitecore. + return { + paths: [], + fallback: 'blocking', + }; }; // This function gets called at build time on server-side. @@ -65,7 +53,8 @@ export const getStaticProps: GetStaticProps = async ({ params, locale }) => { const path = extractPath(params); const props: SitecorePageProps = { - locale: locale ?? 'en', + // Use context locale if Next.js i18n is configured, otherwise use language defined in package.json + locale: locale ?? packageConfig.language, layoutData: null, dictionary: null, }; diff --git a/samples/nextjs/src/pages/_app.tsx b/samples/nextjs/src/pages/_app.tsx index ce15ae94c8..a440db6348 100644 --- a/samples/nextjs/src/pages/_app.tsx +++ b/samples/nextjs/src/pages/_app.tsx @@ -21,8 +21,9 @@ function App({ Component, pageProps }: AppProps): JSX.Element { const { dictionary, ...rest } = pageProps; return ( - // Will be doing further evaluation on best i18n library to use for dictionary translation with Next.js. - // 'i18next' and 'react-i18next' seem heavy-handed, 'next-localization' (used here) is promising ... + // Use the next-localization (w/ rosetta) library to provide our translation dictionary to the app. + // Note Next.js does not (currently) provide anything for translation, only i18n routing. + // If your app is not multilingual, next-localization and references to it can be removed. diff --git a/samples/nextjs/src/pages_examples/[[...path]].SSR.tsx b/samples/nextjs/src/pages_examples/[[...path]].SSR.tsx index 420922ede6..fa6cdd791c 100644 --- a/samples/nextjs/src/pages_examples/[[...path]].SSR.tsx +++ b/samples/nextjs/src/pages_examples/[[...path]].SSR.tsx @@ -7,6 +7,7 @@ import { SitecorePageProps, extractPath } from 'lib/page-props'; import componentFactory from 'temp/componentFactory'; import { configBasedLayoutService as layoutService } from 'lib/layout-service'; import { configBasedDictionaryService as dictionaryService } from 'lib/dictionary-service'; +import { config as packageConfig } from '../../package.json'; const SitecorePage = ({ layoutData }: SitecorePageProps): JSX.Element => { if (!layoutData?.sitecore?.route) { @@ -34,7 +35,8 @@ export const getServerSideProps: GetServerSideProps = async ({ params, locale, r const path = extractPath(params); const props: SitecorePageProps = { - locale: locale ?? 'en', + // Use context locale if Next.js i18n is configured, otherwise use language defined in package.json + locale: locale ?? packageConfig.language, layoutData: null, dictionary: null, };