diff --git a/__tests__/getFallbackPageNamespaces.test.js b/__tests__/getFallbackPageNamespaces.test.js
new file mode 100644
index 00000000..07dc782e
--- /dev/null
+++ b/__tests__/getFallbackPageNamespaces.test.js
@@ -0,0 +1,100 @@
+import getFallbackPageNamespaces from '../src/getFallbackPageNamespaces'
+
+describe('getFallbackPageNamespaces', () => {
+ let ctx
+ beforeAll(() => {
+ ctx = { query: {} }
+ })
+
+ describe('empty', () => {
+ test('should not return any namespace with empty pages', () => {
+ const input = [{ pages: {} }, '/test-page', ctx]
+ const output = getFallbackPageNamespaces(...input)
+
+ expect(output.length).toBe(0)
+ })
+ test('should not return any namespace with pages as undefined', () => {
+ const input = [{}, '/test-page', ctx]
+ const output = getFallbackPageNamespaces(...input)
+
+ expect(output.length).toBe(0)
+ })
+ })
+
+ describe('regular expressions', () => {
+ test('should return namespaces that match the rgx', () => {
+ const config = {
+ pages: {
+ '*': ['common'],
+ '/example/form': ['valid'],
+ '/example/form/other': ['invalid'],
+ 'rgx:/form$': ['form'],
+ 'rgx:/invalid$': ['invalid'],
+ 'rgx:^/example': ['example'],
+ },
+ }
+ const input = [config, '/example/form']
+ const output = getFallbackPageNamespaces(...input)
+
+ expect(output.length).toBe(4)
+ expect(output[0]).toBe('common')
+ expect(output[1]).toBe('valid')
+ expect(output[2]).toBe('form')
+ expect(output[3]).toBe('example')
+ })
+ })
+
+ describe('as array', () => {
+ test('should return the page namespace', () => {
+ const input = [
+ { pages: { '/test-page': ['test-ns'] } },
+ '/test-page',
+ ctx,
+ ]
+ const output = getFallbackPageNamespaces(...input)
+ const expected = ['test-ns']
+
+ expect(output.length).toBe(1)
+ expect(output[0]).toBe(expected[0])
+ })
+
+ test('should return the page namespace + common', () => {
+ const input = [
+ {
+ pages: {
+ '*': ['common'],
+ '/test-page': ['test-ns'],
+ },
+ },
+ '/test-page',
+ ctx,
+ ]
+ const output = getFallbackPageNamespaces(...input)
+ const expected = ['common', 'test-ns']
+
+ expect(output.length).toBe(2)
+ expect(output[0]).toBe(expected[0])
+ expect(output[1]).toBe(expected[1])
+ })
+ })
+
+ describe('as function', () => {
+ test('should work as a fn', () => {
+ ctx.query.example = '1'
+ const input = [
+ {
+ pages: {
+ '/test-page': ({ query }) => (query.example ? ['test-ns'] : []),
+ },
+ },
+ '/test-page',
+ ctx,
+ ]
+ const output = getFallbackPageNamespaces(...input)
+ const expected = ['test-ns']
+
+ expect(output.length).toBe(1)
+ expect(output[0]).toBe(expected[0])
+ })
+ })
+})
diff --git a/examples/complex/i18n.js b/examples/complex/i18n.js
index 097d3baf..34da3b34 100644
--- a/examples/complex/i18n.js
+++ b/examples/complex/i18n.js
@@ -1,3 +1,5 @@
+const fs = require('fs')
+
module.exports = {
locales: ['en', 'ca', 'es'],
defaultLocale: 'en',
@@ -14,4 +16,10 @@ module.exports = {
},
loadLocaleFrom: (locale, namespace) =>
import(`./src/translations/${namespace}_${locale}`).then((m) => m.default),
+
+ loadLocaleFromSync: (locale, namespace) => {
+ return JSON.parse(
+ fs.readFileSync(`./src/translations/${namespace}_${locale}.json`)
+ )
+ },
}
diff --git a/examples/complex/src/pages/more-examples/fallback/[slug].tsx b/examples/complex/src/pages/more-examples/fallback/[slug].tsx
new file mode 100644
index 00000000..3f654783
--- /dev/null
+++ b/examples/complex/src/pages/more-examples/fallback/[slug].tsx
@@ -0,0 +1,43 @@
+import { GetStaticProps } from 'next'
+import getT from 'next-translate/getT'
+import useTranslation from 'next-translate/useTranslation'
+import withFallbackTranslation from 'next-translate/withFallbackTranslation'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+function DynamicRoute({ title = '' }) {
+ const { query } = useRouter()
+ const { t, lang } = useTranslation()
+
+ console.log({ query })
+
+ return (
+ <>
+
{title}
+ {t`more-examples:dynamic-route`}
+
+ {query.slug} - {lang}
+
+
+ {t`more-examples:go-to-home`}
+
+ >
+ )
+}
+
+export function getStaticPaths({ locales }: any) {
+ return {
+ paths: locales.map((locale: string) => ({
+ locale,
+ params: { slug: 'example' },
+ })),
+ fallback: true,
+ }
+}
+
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
+ const t = await getT(locale, 'common')
+ return { props: { title: t('title') }, revalidate: 5 }
+}
+
+export default withFallbackTranslation(DynamicRoute)
diff --git a/package.json b/package.json
index a1627ea4..448115b2 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
"scripts": {
"build": "yarn clean && cross-env NODE_ENV=production && yarn tsc",
"clean": "yarn clean:build && yarn clean:examples",
- "clean:build": "rm -rf lib plugin appWith* Dynamic* I18n* index _context loadNa* setLang* Trans useT* withT* getP* getC* *.d.ts getT transC* wrapT* types",
+ "clean:build": "rm -rf lib plugin appWith* Dynamic* I18n* index _context loadNa* loadFa* setLang* Trans useT* withT* getFa* getP* getC* *.d.ts getT transC* wrapT* withFall* types",
"clean:examples": "rm -rf examples/**/.next && rm -rf examples/**/node_modules && rm -rf examples/**/yarn.lock",
"example": "yarn example:complex",
"example:basic": "yarn build && cd examples/basic && yarn && yarn dev",
diff --git a/src/appWithI18n.tsx b/src/appWithI18n.tsx
index a06b91f3..76a8d882 100644
--- a/src/appWithI18n.tsx
+++ b/src/appWithI18n.tsx
@@ -36,13 +36,58 @@ export default function appWithI18n(
function AppWithTranslations(props: Props) {
const { defaultLocale } = config
+ var ns = {}
+ var pageProps
+
+ if (typeof window === 'undefined') {
+ if (
+ props.router &&
+ props.router.isFallback &&
+ props.Component &&
+ typeof props.Component.__PAGE_NEXT_NAMESPACES === 'function'
+ ) {
+ ns =
+ props.Component.__PAGE_NEXT_NAMESPACES({
+ locale: props.router.locale,
+ pathname: props.router.pathname,
+ }) || {}
+
+ pageProps = { ...ns, ...props.pageProps }
+ }
+ } else {
+ if (
+ props.Component &&
+ typeof props.Component.__PAGE_NEXT_NAMESPACES === 'function'
+ ) {
+ ns = props.Component.__PAGE_NEXT_NAMESPACES() || {}
+
+ pageProps = { ...ns, ...props.pageProps }
+ }
+ }
+
+ if (pageProps == null) {
+ pageProps = props.pageProps
+ }
+
+ var newProps: any = {
+ ...props,
+ pageProps,
+ }
+
return (
-
+
+
)
}
diff --git a/src/getFallbackPageNamespaces.tsx b/src/getFallbackPageNamespaces.tsx
new file mode 100644
index 00000000..1b2dbf24
--- /dev/null
+++ b/src/getFallbackPageNamespaces.tsx
@@ -0,0 +1,35 @@
+import { I18nConfig, PageValue } from '.'
+
+// @todo Replace to [].flat() in the future
+function flat(a: string[][]): string[] {
+ return a.reduce((b, c) => b.concat(c), [])
+}
+
+/**
+ * Get fallback page namespaces
+ *
+ * @param {object} config
+ * @param {string} page
+ */
+export default function getFallbackPageNamespaces(
+ { pages = {} }: I18nConfig,
+ page: string,
+ ctx: object
+): string[] {
+ const rgx = 'rgx:'
+ const getNs = (ns: PageValue): string[] =>
+ typeof ns === 'function' ? ns(ctx) : ns || []
+
+ // Namespaces promises using regex
+ const rgxs = Object.keys(pages).reduce((arr: string[][], p) => {
+ if (
+ p.substring(0, rgx.length) === rgx &&
+ new RegExp(p.replace(rgx, '')).test(page)
+ ) {
+ arr.push(getNs(pages[p]))
+ }
+ return arr
+ }, [])
+
+ return [...getNs(pages['*']), ...getNs(pages[page]), ...flat(rgxs)]
+}
diff --git a/src/index.tsx b/src/index.tsx
index 5760b7a5..9d67ffc8 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -38,10 +38,16 @@ export type LocaleLoader = (
namespace: string
) => Promise
+export type LocaleLoaderSync = (
+ language: string | undefined,
+ namespace: string
+) => I18nDictionary
+
export interface I18nConfig {
defaultLocale?: string
locales?: string[]
loadLocaleFrom?: LocaleLoader
+ loadLocaleFromSync?: LocaleLoaderSync
pages?: Record
logger?: I18nLogger
staticsHoc?: Function
diff --git a/src/loadFallbackPageNamespaces.tsx b/src/loadFallbackPageNamespaces.tsx
new file mode 100644
index 00000000..01f915d1
--- /dev/null
+++ b/src/loadFallbackPageNamespaces.tsx
@@ -0,0 +1,63 @@
+import { LoaderConfig, LocaleLoaderSync } from '.'
+import getConfig from './getConfig'
+import getFallbackPageNamespaces from './getFallbackPageNamespaces'
+
+export default function loadFallbackPageNamespaces(
+ config: LoaderConfig = {}
+): {
+ __lang: string
+ __namespaces?: Record
+} {
+ const conf = { ...getConfig(), ...config }
+ const __lang: string =
+ conf.locale || conf.router?.locale || conf.defaultLocale || ''
+
+ if (!conf.pathname) {
+ console.warn(
+ '🚨 [next-translate] You forgot to pass the "pathname" inside "loadNamespaces" configuration'
+ )
+ return { __lang }
+ }
+
+ if (!conf.loaderName && conf.loader !== false) {
+ console.warn(
+ '🚨 [next-translate] You can remove the "loadNamespaces" helper, unless you set "loader: false" in your i18n config file.'
+ )
+ }
+
+ const page = removeTrailingSlash(conf.pathname.replace(/\/index$/, '')) || '/'
+ const namespaces = getFallbackPageNamespaces(conf, page, conf)
+ const defaultLoader: LocaleLoaderSync = () => ({})
+ const pageNamespaces = namespaces.map((ns) =>
+ typeof conf.loadLocaleFromSync === 'function'
+ ? conf.loadLocaleFromSync(__lang, ns)
+ : defaultLoader(__lang, ns)
+ )
+
+ if (conf.logBuild !== false && typeof window === 'undefined') {
+ const color = (c: string) => `\x1b[36m${c}\x1b[0m`
+ console.log(
+ color('next-translate'),
+ `- compiled page:`,
+ color(page),
+ '- locale:',
+ color(__lang),
+ '- namespaces:',
+ color(namespaces.join(', ')),
+ '- used loader:',
+ color(conf.loaderName || '-')
+ )
+ }
+
+ return {
+ __lang,
+ __namespaces: namespaces.reduce((obj: Record, ns, i) => {
+ obj[ns] = pageNamespaces[i]
+ return obj
+ }, {}),
+ }
+}
+
+function removeTrailingSlash(path = '') {
+ return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path
+}
diff --git a/src/plugin/index.ts b/src/plugin/index.ts
index f44d4eff..c81155f7 100644
--- a/src/plugin/index.ts
+++ b/src/plugin/index.ts
@@ -81,6 +81,13 @@ export default function nextTranslate(nextConfig: any = {}) {
'@next-translate-root': path.resolve(dir),
}
+ // translating Next.js fallback pages requires the use of `fs` module
+ if (!options.isServer) {
+ config.node = {
+ fs: 'empty',
+ }
+ }
+
// we give the opportunity for people to use next-translate without altering
// any document, allowing them to manually add the necessary helpers on each
// page to load the namespaces.
diff --git a/src/withFallbackTranslation.tsx b/src/withFallbackTranslation.tsx
new file mode 100644
index 00000000..42a838ac
--- /dev/null
+++ b/src/withFallbackTranslation.tsx
@@ -0,0 +1,38 @@
+import { NextComponentType } from 'next'
+import React from 'react'
+import loadFallbackPageNamespaces from './loadFallbackPageNamespaces'
+
+/**
+ * HOC to use translations for Next.js fallback pages.
+ */
+export default function withFallbackTranslation(
+ Component: React.ComponentType
| NextComponentType
+) {
+ const WithTranslation = (props: P) => {
+ return
+ }
+
+ WithTranslation.__PAGE_NEXT_NAMESPACES = ({
+ locale,
+ pathname,
+ }: { locale?: string; pathname?: string } = {}) => {
+ // hydrate translations on client from DOM
+ if (typeof window !== 'undefined') {
+ const namespacesScript = document.getElementById(
+ '__NEXT_NAMESPACES_DATA__'
+ )
+
+ if (namespacesScript) {
+ return JSON.parse(namespacesScript.innerHTML)
+ }
+
+ return {}
+ }
+
+ const ns = loadFallbackPageNamespaces({ locale, pathname })
+
+ return { ...ns }
+ }
+
+ return WithTranslation
+}