Next.js utility to generate internationalized (i18n) pages according to custom roots rules and with no need to use Vercel dev server, Rewrites neither Routes. The package has zero dependencies.
This package is highly inspired by next-translate.
It solves some additional features like static routing schema
, URL tokenizing
, page meta
, injecting config directly to pages
... and is completely TypeScript friendly!
Similar to next-translate
this package holds all pages implementation in a separate directory. We call it roots
. Required pages
directory is then created during build
time.
A complete example can be seen in the example
directory.
- Add the package to your project dependencies
yarn add next-roots
- Add pages builder script to your
package.json
{
"scripts": {
"dev": "yarn next-roots && next dev",
"build": "yarn next-roots && next build"
}
}
- Create roots.config.js in your project root
- Add RootsContext to your
_app
- Run
yarn dev
The default behavior is to have a roots.config.js
file placed in your project root folder (next to your package.json file).
This file defines roots schema for your pages and config params for pages builder.
Basic configuration can look like:
module.exports = {
locales: ['en', 'cs'],
defaultLocale: 'cs',
defaultSuffix: '.htm',
prototypes: [
{
root: '*',
metaData: [
{
locale: '*',
data: { title: 'Next Roots', background: 'grey', footer: false },
},
],
},
{
root: 'auth/(.*)',
metaData: [
{ locale: '*', data: { footer: true } },
{ locale: 'en', data: { section: 'Authorization' } },
{ locale: 'cs', data: { section: 'OvěřenĂ' } },
{ locale: 'es', data: { section: 'AutorizaciĂłn' } },
],
},
],
schemas: [
{
root: 'home',
pages: [{ locale: '*', path: 'index', suffix: '' }],
},
{
root: 'auth/signup',
pages: [
{ locale: 'en', path: 'auth/signup-:token' },
{ locale: 'cs', path: 'overeni/registrace-:token' },
],
params: { token: 'p1' },
metaData: [
{ locale: 'en', data: { title: 'Signup' } },
{ locale: 'cs', data: { title: 'Registrace' } },
],
},
{
root: 'dynamic',
pages: [{ locale: '*', path: '[...slug]', suffix: '' }],
params: { token: 'p1' },
metaData: [{ locale: '*', data: { background: 'magenta' } }],
},
],
}
NOTE: all following examples are based on above config.
Before you run a build process with the above config your project structure needs to look like this:
.
├── roots
│ ├── home.tsx
│ ├── dynamic.tsx
│ └── auth
│ └── signup.tsx
After you run a build process with the above config your project structure will look like this:
.
├── roots
│ ├── home.tsx
│ └── auth
│ └── signup.tsx
├── pages
│ └── en
│ └── index.tsx
│ └── [...slug].tsx
│ └── auth
│ └── signup-p1.htm.tsx
│ └── cs
│ └── index.tsx
│ └── [...slug].tsx
│ └── auth
│ └── registrace-p1.htm.tsx
Static schema file roots.schema.js
will also be generated and placed to the project root folder. This file contains a routing map for each page in your roots configuration, available locales, and default locale.
module.exports = {
locales: ['en', 'cs'],
defaultLocale: 'cs',
rules: [
{
key: 'en:home',
href: '/en',
},
{
key: 'cs:home',
href: '/cs',
},
{
key: 'en:auth/signup',
href: '/en/auth-signup-p1.htm',
},
{
key: 'cs:auth/signup',
href: '/cs/ucet-registrace-p1.htm',
},
{ key: 'en:dynamic', href: '/en/[...slug]' },
{ key: 'cs:dynamic', href: '/cs/[...slug]' },
],
}
NOTE: If some rule does not contain
as
it means that it is the same ashref
.
Name | Default | Description |
---|---|---|
schemas | [] | builder rules for generating pages |
locales | [] | all allowed locales which will be generated |
defaultLocale | '' | locale which will be used as default when no locale is detected |
defaultSuffix | '' | default page suffix which will be added to page name |
basePath | . |
path to base folder where roots and pages are located |
dirRoot | roots |
source folder with all roots files |
dirPages | pages |
target folder where pages will be generated into |
staticRoots | ['api', '_app', '_document', '_error', '404'] |
static roots which will be generated outside locales folders |
extRoots | ['.tsx'] |
suffix of all roots files |
Each schema rule represents one root + page
combination. This is the main way how to define the routing map for your localized pages.
{
root: 'auth/signup',
pages: [
{ locale: 'en', path: 'auth/signup-:token' },
{ locale: 'cs', path: 'auth/registrace-:token' },
],
params: { token: 'p1' },
metaData: [
{ locale: 'en', data: { title: 'Signup' } },
{ locale: 'cs', data: { title: 'Registrace' } },
],
}
root
- source file path (will be used as root name if rootName options is empty)rootName
- explicit root name (used when creating RootLinks)pages
- localized aliases for current rootparams
- params which will be used as replace value in pagepath
oralias
during buildmetaData
- custom params which can will be injected directly into page and can by obtained usinguseRootMeta
hook in runtime.
Each schema rule must define pages array. Otherwise it must be defined as prototype rule.
pages: [{
locale: '*',
path: 'index',
suffix: ''
}],
locale
- the name of the folder where the page will be generated (use*
to generate the same schema for all locales)path
- page file name which will also be used for routing as linkhref
alias
- page alias which will be used for routing as linkas
suffix
- custom suffix which will be appended topath
param
Each schema rule can define custom metadata array. Each array item has to define two properties:
locale
- must be one oflocales
value or*
to be used for each available localedata
- custom values type ofRecord<string, ReactText>
metaData: [
{ locale: 'en', data: { background: 'magenta' } }
],
This data can be used to change layout, CSS, background images, ... based on your requirements.
Prototype rule is used for setting general/default metadata values which will be then merged with page-specific metadata values
{
root: 'account/*',
metaData: [
{ locale: '*', data: { background: 'red' } }
{ locale: 'en', data: { section: 'Account' } }
],
isPrototype: true
},
{
root: 'account/profile',
metaData: [
{ locale: 'en', data: { title: 'Profile' } }
],
// ...
},
Final metadata for account/profile
root will be then
{
background: 'red',
section: 'Account',
title: 'Profile'
}
Root property works here like RegExp. The prototype schema will be merged into any root passing the condition realSchema.root.match(new Regexp(prototypeSchema.root))
The Next-roots package provides handy hooks to read and manipulate its context values.
Provides main roots values according to the current router path.
locales: string[]
- all active localesdefaultLocale: string
- default locale valuecurrentLocale: string
- current page localecurrentRoot: string
- current page rootcurrentRule: SchemaRule | undefined
- containing current rulekey
,href
and optionallyalias
currentMeta: SchemaMeta | undefined
- containing current page meta data
Example usage:
import { useRoots } from 'next-roots/context'
// router path = /en/auth/signup-p1.htm
const roots = useRoots()
roots.locales // ['en', 'cs', ...]
roots.defaultLocale // 'cs'
roots.currentLocale // 'en'
roots.currentRoot // 'auth/signup'
roots.currentRule // { key: 'en:auth/signup', href: '/en/auth/signup' }
roots.currentMeta // { key: 'en:auth/signup', data: { title: 'Signup', background: 'grey' } }
Provides API to create localized links based on context rules.
Example usage:
import { useRootLink } from 'next-roots/link'
const link = useRootLink()
// 1. generates href with current locale using root name (currentLocale = en)
link.href('auth/signup')
// result: /en/auth/signup-p1.htm`
// 2. generates href with custom locale using root name and explicit locale option
link.href('auth/signup', { locale: 'cs' })
// result: `/cs/overeni/registrace-p1.htm`
// 3. generate href with custom locale using rule key and empty locale option
link.href('cs:auth/signup', { locale: '' })
// result: '/cs/overeni/registrace-p1.htm'
// 4. generate href with custom locale using current rule key and explicit locale option (currentRule = cs:auth/signup)
link.href('cs:auth/signup', { locale: 'en' })
// result: '/en/auth-signup-p1.htm'
// 5. generate href for dynamic page
link.href('dynamic', { locale: 'en' })
// result: '/en/[...slug]'
// 6. generate href for home page using shortcut
link.href('/', { locale: 'en' })
// result: '/en'
NOTE: There is a predefined home page shortcut
/
in roots package. So you do not need to usehome
if you don't want to.
The same options work for link alias. Plus dynamic params can be explicitly pushed:
// same options as for link.href() plus:
const link = useRootLink()
// 1. generate alias for dynamic page
link.as('dynamic', { locale: 'en', params: { slug: 'some-slug' } })
// result: '/en/some-slug'
NOTE: It is not practical to have all possible rules injected into every page because it makes page bundle size bigger in large applications. Therefore if your current page is
cs:auth/signup
you have access only tocs
rules and toauth/signup
rules with a different locale.
Provides API to read static metadata attached current page or its mutations.
Example usage:
// example values when current root is 'dynamic'
const meta = useRootMeta()
// 1. read all meta data for current router path (data is merged with general meta data - schema.meta.key === '*')
meta.data()
// result: { title: 'Next Roots', background: 'magenta' }
// 2. cherry pick meta data using custom selector for current router path
meta.data('background')
// result: 'magenta'
// 3. read all meta data for explicit rule key
meta.data('*', 'cs:auth/signup')
// result: { title: 'Next Roots', background: 'grey' }
NOTE: It is not practical to have all possible metadata injected into every page because it makes page bundle size bigger in large applications. Therefore if your current page is
cs:auth/signup
you have access only to its current metadata and metadata with a different locale but the same root.
The Next-roots package provides ready-to-use components with injected roots context.
Main roots context component which holds current values according to the current page.
This component is required to use in your app so that other components can consume the context of the current root.
Recommended usage:
// in your _app.tsx
import { RootsContext, detectRoots } from 'next-roots/context'
import { AppProps } from 'next/app'
import schemaRoots from 'roots.schema'
function MyApp(appProps: AppProps) {
const { Component, pageProps } = appProps
// detect roots context from page component
// - current values will be obtained from Component.getRoots
// - second argument holds default values
const roots = detectRoots(appProps, {
defaultLocale: schemaRoots.defaultLocale,
locales: schemaRoots.locales,
})
return (
<RootsContext.Provider value={roots}>
<Component {...pageProps} />
</RootsContext.Provider>
)
}
export default MyApp
Extends native link/next
with roots context. Generates links based on roots schema when a rule is found otherwise link generation is handled by native link component.
Works similar to useRootLink
as this hook is used under the hood of the RootLink
component.
Example usage:
import RootLink from 'next-roots/link';
// 1. Using with current locale and root name (currentLocale = en)
<RootLink href="auth/signup">
<a>...</a>
</RootLink>
// result <a href="/en/auth/signup-p1.htm">...</a>
// 2. Using with the custom locale and root name
<RootLink href="auth/signup" locale="cs">
<a>...</a>
</RootLink>
// result <a href="/cs/overeni/registrace-p1.htm">...</a>
// 3. Using with rule key and empty locale option
<RootLink href="cs:auth/signup" locale="">
<a>...</a>
</RootLink>
// result <a href="/cs/overeni/registrace-p1.htm">...</a>
// 4. Using with rule key and custom locale
<RootLink href="cs:auth/signup" locale="en">
<a>...</a>
</RootLink>
// result <a href="/en/auth/signup-p1.htm">...</a>
// 5. Using with dynamic root (e.g. page = [...slug])
<RootLink href="dynamic" locale="en" params={{slug: 'some-slug'}}>
<a>...</a>
</RootLink>
// result <a href="/en/auth/some-slug">...</a>
// 6. Using home page shortcut (currentLocale = en)
<RootLink href="/">
<a>...</a>
</RootLink>
// result <a href="/en">...</a>
It is possible to use component to debug whole Roots context values.
import { RootsConsole } from 'next-roots/console'
//
function MyLayout() {
return (
<div>
// ... custom logic
<RootsConsole />
</div>
)
}
Once this component is mounted to the DOM following indicator will appear in the bottom left corner of the window:
The console will open once that indicator is clicked:
All current context values like currenLocale
, currentRoot
or currentMeta
can be found in this console. Also, links to current page mutations (locales) can be found in a tab called links
.
Note that
RootsConsole
component should be used only in dev and should be removed before deploying to production
Next.js provides us with special page methods like getServerSideProps
, getStaticProps
, getStaticPaths
and getInitialProps (will be deprecated)
.
All these methods are parsed during build time and forwarded from generated pages. The current page locale
is also pushed to the context of some of these methods.
For example if you have root
named dynamic
which contains two of mentioned methods (getStaticProps, getStaticPaths) the result will look like:
import { GetStaticPaths, GetStaticProps } from 'next'
import DynamicRoot, * as __root from 'roots/dynamic'
// ... custom logic
// @ts-ignore
export const getStaticProps: GetStaticProps = async (context) =>
__root.getStaticProps({ ...context, __locale: 'cs' })
export const getStaticPaths: GetStaticPaths = async () =>
__root.getStaticPaths()
export default DynamicPage
Then your are able to read page locale directly inside your root's special methods:
export const getStaticProps: GetStaticProps = async (context) => {
const { __locale, ...ctxOthers } = context
// ... custom logic
}
Example usage with lightweight schema can be found in the example folder
cd example
yarn install
yarn dev
Refactor has been done to keep page bundles size as small as possible. Therefore including roots.schema.js
file in your _app
(or anywhere else) is not required anymore.
The only possible case for using
roots.schema.js
is handling tokenized page redirects like/en/p1.htm
>>>/en/auth/signup-p1.htm
in your [catchAllRoute] page
The parsePathname
util has been replaced with detectRoots
and is no longer dependent on the router pathname. Context values are obtained from the PageComponent.getRoots
which is generated during build time.
Rules and metadata are now directly injected to page file during build time BUT to keep page size bundle small only rules and meta with same locale or root are injected.
Migrating to 2.x requires two steps:
// BEFORE 2.0.0
import RootsContext, { parsePathname } from 'next-roots/context'
import schemaRoots from 'roots.schema'
import { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
// parse current roots values from router pathname
const { locale, root, rule } = parsePathname(router.pathname, schema)
return (
<RootsContext.Provider
value={{
currentRule: rule,
currentRoot: root,
currentLocale: locale || schema.defaultLocale,
defaultLocale: schema.defaultLocale,
locales: schema.locales,
rules: schema.rules,
meta: schema.meta,
}}
>
<Component {...pageProps} />
</RootsContext.Provider>
)
}
export default MyApp
// AFTER 2.0.0
import { RootsContext, detectRoots } from 'next-roots/context'
import { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
// detect roots context from page component
const roots = detectRoots(Component, {
defaultLocale: 'en',
locales: ['en', 'cs', 'es'],
})
return (
<RootsContext.Provider value={roots}>
<Component {...pageProps} />
</RootsContext.Provider>
)
}
export default MyApp
// BEFORE 2.0.0
{
root: 'some-root-name',
pages: [
{
locale: 'en',
metaData: { background: 'blue' },
// ... other page config
}
],
metaData: { title: 'Next Roots', background: 'grey' },
},
// AFTER 2.0.0
{
root: 'some-root-name',
pages: [
{
locale: 'en',
// ... other page config
}
],
metaData: [
{ locale: '*', data: { title: 'Next Roots', background: 'grey' } }
{ locale: 'en', data: { background: 'blue' } }
],
},
Following options are experimental and should not be used in production yet.
To generate localized pages directly inside pages
directory and ignore its enclosing locale folder the following configuration is required:
// roots.config.js
{
shallowLocale: 'en'
// ...other options
}