├── .nvmrc
├── i18n
├── locales
│ ├── zh-CN
│ │ ├── home.json
│ │ ├── about.json
│ │ ├── common.json
│ │ ├── newsletter.json
│ │ └── built-in-demo.json
│ ├── en
│ │ ├── home.json
│ │ ├── about.json
│ │ ├── common.json
│ │ ├── newsletter.json
│ │ └── built-in-demo.json
│ └── sv
│ │ ├── home.json
│ │ ├── about.json
│ │ ├── common.json
│ │ ├── newsletter.json
│ │ └── built-in-demo.json
├── settings.ts
├── server.ts
└── client.ts
├── postcss.config.js
├── .eslintrc
├── tailwind.config.js
├── renovate.json
├── next-env.d.ts
├── app
├── [locale]
│ ├── layout.tsx
│ ├── about
│ │ └── page.tsx
│ └── page.tsx
└── layout.tsx
├── styles
└── tailwind.css
├── .prettierrc
├── .gitignore
├── README.md
├── components
├── SubscribeForm.tsx
├── ChangeLocale.tsx
├── Header.tsx
└── BuiltInFormatDemo.tsx
├── tsconfig.json
├── package.json
└── middleware.ts
/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.12.1
2 |
--------------------------------------------------------------------------------
/i18n/locales/zh-CN/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "greeting": "世界您好"
3 | }
4 |
--------------------------------------------------------------------------------
/i18n/locales/en/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "greeting": "Hello world!"
3 | }
4 |
--------------------------------------------------------------------------------
/i18n/locales/sv/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "greeting": "Hej världen!"
3 | }
4 |
--------------------------------------------------------------------------------
/i18n/locales/zh-CN/about.json:
--------------------------------------------------------------------------------
1 | {
2 | "aboutThisPage": "这是关于页面"
3 | }
4 |
--------------------------------------------------------------------------------
/i18n/locales/sv/about.json:
--------------------------------------------------------------------------------
1 | {
2 | "aboutThisPage": "Det här är sidan Om"
3 | }
4 |
--------------------------------------------------------------------------------
/i18n/locales/sv/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "Hem",
3 | "about": "Om"
4 | }
5 |
--------------------------------------------------------------------------------
/i18n/locales/en/about.json:
--------------------------------------------------------------------------------
1 | {
2 | "aboutThisPage": "This is the About page"
3 | }
4 |
--------------------------------------------------------------------------------
/i18n/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "Home",
3 | "about": "About"
4 | }
5 |
--------------------------------------------------------------------------------
/i18n/locales/zh-CN/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "主页",
3 | "about": "关于页面"
4 | }
5 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "next/core-web-vitals", "prettier"],
3 | "rules": {
4 | "no-console": "error"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./**/*.{jsx,tsx,mdx}'],
3 | theme: {
4 | extend: {},
5 | },
6 | plugins: [],
7 | };
8 |
--------------------------------------------------------------------------------
/i18n/locales/zh-CN/newsletter.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "保持最新状态",
3 | "form": {
4 | "email": "电子邮箱",
5 | "action": {
6 | "cancel": "取消"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "labels": ["dependencies"],
4 | "packageRules": [
5 | {
6 | "matchDepTypes": ["devDependencies"],
7 | "automerge": true
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/app/[locale]/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from '../../components/Header';
3 |
4 | const Layout = ({children}) => {
5 | return (
6 | <>
7 |
8 | {children}
9 | >
10 | );
11 | };
12 |
13 | export default Layout;
14 |
--------------------------------------------------------------------------------
/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | h1 {
6 | @apply text-3xl;
7 | }
8 |
9 | h2 {
10 | @apply text-2xl;
11 | }
12 |
13 | h3 {
14 | @apply text-xl;
15 | }
16 |
17 | .form-field {
18 | @apply border mb-1 p-1 w-full;
19 | }
20 |
--------------------------------------------------------------------------------
/i18n/locales/en/newsletter.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Stay up to date",
3 | "subtitle": "Subscribe to my newsletter",
4 | "form": {
5 | "firstName": "First name",
6 | "email": "E-mail",
7 | "action": {
8 | "signUp": "Sign Up",
9 | "cancel": "Cancel"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/i18n/locales/zh-CN/built-in-demo.json:
--------------------------------------------------------------------------------
1 | {
2 | "number": "数: {{val, number}}",
3 | "currency": "货币: {{val, currency}}",
4 | "dateTime": "日期/时间: {{val, datetime}}",
5 | "relativeTime": "相对时间: {{val, relativetime}}",
6 | "list": "列表: {{val, list}}",
7 | "weekdays": ["星期一", "星期二", "星期三", "星期四", "星期五"]
8 | }
9 |
--------------------------------------------------------------------------------
/i18n/locales/sv/newsletter.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Håll dig uppdaterad",
3 | "subtitle": "Prenumerera på mitt nyhetsbrev",
4 | "form": {
5 | "firstName": "Förnamn",
6 | "email": "E-post",
7 | "action": {
8 | "signUp": "Registrera sig",
9 | "cancel": "Annullera"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import '../styles/tailwind.css';
2 |
3 | export const metadata = {
4 | title: 'Next.js i18n',
5 | };
6 |
7 | export default function RootLayout({children}: {children: React.ReactNode}) {
8 | return (
9 |
10 |
{children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/i18n/locales/sv/built-in-demo.json:
--------------------------------------------------------------------------------
1 | {
2 | "number": "Nummer: {{val, number}}",
3 | "currency": "Valuta: {{val, currency}}",
4 | "dateTime": "Datum/tid: {{val, datetime}}",
5 | "relativeTime": "Relativ tid: {{val, relativetime}}",
6 | "list": "Lista: {{val, list}}",
7 | "weekdays": ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag"]
8 | }
9 |
--------------------------------------------------------------------------------
/i18n/locales/en/built-in-demo.json:
--------------------------------------------------------------------------------
1 | {
2 | "number": "Number: {{val, number}}",
3 | "currency": "Currency: {{val, currency}}",
4 | "dateTime": "Date/Time: {{val, datetime}}",
5 | "relativeTime": "Relative Time: {{val, relativetime}}",
6 | "list": "List: {{val, list}}",
7 | "weekdays": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
8 | }
9 |
--------------------------------------------------------------------------------
/app/[locale]/about/page.tsx:
--------------------------------------------------------------------------------
1 | import {createTranslation} from '../../../i18n/server';
2 |
3 | const AboutPage = async ({params: {locale}}) => {
4 | // Make sure to use the correct namespace here.
5 | const {t} = await createTranslation(locale, 'about');
6 |
7 | return (
8 |
9 |
{t('aboutThisPage')}
10 |
11 | );
12 | };
13 |
14 | export default AboutPage;
15 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": false,
4 | "endOfLine": "lf",
5 | "htmlWhitespaceSensitivity": "css",
6 | "insertPragma": false,
7 | "jsxBracketSameLine": false,
8 | "jsxSingleQuote": false,
9 | "printWidth": 80,
10 | "proseWrap": "always",
11 | "quoteProps": "as-needed",
12 | "requirePragma": false,
13 | "singleQuote": true,
14 | "tabWidth": 2,
15 | "trailingComma": "all",
16 | "useTabs": false
17 | }
18 |
--------------------------------------------------------------------------------
/i18n/settings.ts:
--------------------------------------------------------------------------------
1 | import type {InitOptions} from 'i18next';
2 |
3 | export const fallbackLng = 'en';
4 | export const locales = [fallbackLng, 'zh-CN', 'sv'] as const;
5 | export type LocaleTypes = (typeof locales)[number];
6 | export const defaultNS = 'common';
7 |
8 | export function getOptions(lang = fallbackLng, ns = defaultNS): InitOptions {
9 | return {
10 | // debug: true, // Set to true to see console logs
11 | supportedLngs: locales,
12 | fallbackLng,
13 | lng: lang,
14 | fallbackNS: defaultNS,
15 | defaultNS,
16 | ns,
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 | /test-results
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 | /test-results/
37 | /playwright-report/
38 | /playwright/.cache/
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next.js, TypeScript, Tailwind Boilerplate
2 |
3 | A very lean NextJS boilerplate with TypeScript and Tailwind support. Plus all
4 | the goodness of ESLint and Prettier!
5 |
6 | ## Dependencies
7 |
8 | ### Install npm via nodejs using either
9 |
10 | - [NodeJS](https://nodejs.org/en/)
11 | - [NVM](https://github.com/nvm-sh/nvm)
12 |
13 | > You can either use `yarn` or `pnpm`; simply delete `package-lock.json` and run
14 | > with your package manager of choice
15 |
16 | ### Install dependencies
17 |
18 | ```sh
19 | npm install
20 | ```
21 |
22 | ### Running the application
23 |
24 | ```sh
25 | npm run dev
26 | ```
27 |
28 | ### Build the application
29 |
30 | ```sh
31 | npm run build
32 | ```
33 |
--------------------------------------------------------------------------------
/app/[locale]/page.tsx:
--------------------------------------------------------------------------------
1 | import BuiltInFormatsDemo from '../../components/BuiltInFormatDemo';
2 | import SubscribeForm from '../../components/SubscribeForm';
3 | import {createTranslation} from '../../i18n/server';
4 |
5 | // Make the page async cause we need to use await for createTranslation
6 | const IndexPage = async ({params: {locale}}) => {
7 | // Make sure to use the correct namespace here.
8 | const {t} = await createTranslation(locale, 'home');
9 |
10 | return (
11 |
12 |
{t('greeting')}
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default IndexPage;
22 |
--------------------------------------------------------------------------------
/components/SubscribeForm.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {createTranslation} from '../i18n/server';
3 |
4 | const SubscribeForm = async ({locale}) => {
5 | const {t} = await createTranslation(locale, 'newsletter');
6 |
7 | return (
8 |
9 | {t('title')}
10 | {t('subtitle')}
11 |
12 |
18 |
19 | );
20 | };
21 |
22 | export default SubscribeForm;
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "incremental": true,
15 | "esModuleInterop": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "jsx": "preserve",
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ]
26 | },
27 | "include": [
28 | "next-env.d.ts",
29 | "**/*.ts",
30 | "**/*.tsx",
31 | ".next/types/**/*.ts"
32 | ],
33 | "exclude": [
34 | "node_modules"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/i18n/server.ts:
--------------------------------------------------------------------------------
1 | import {createInstance} from 'i18next';
2 | import resourcesToBackend from 'i18next-resources-to-backend';
3 | import {initReactI18next} from 'react-i18next/initReactI18next';
4 | import {getOptions, LocaleTypes} from './settings';
5 |
6 | const initI18next = async (lang: LocaleTypes, ns: string) => {
7 | const i18nInstance = createInstance();
8 | await i18nInstance
9 | .use(initReactI18next)
10 | .use(
11 | resourcesToBackend(
12 | (language: string, namespace: typeof ns) =>
13 | import(`./locales/${language}/${namespace}.json`),
14 | ),
15 | )
16 | .init(getOptions(lang, ns));
17 |
18 | return i18nInstance;
19 | };
20 |
21 | export async function createTranslation(lang: LocaleTypes, ns: string) {
22 | const i18nextInstance = await initI18next(lang, ns);
23 |
24 | return {
25 | t: i18nextInstance.getFixedT(lang, Array.isArray(ns) ? ns[0] : ns),
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-typescript-tailwind-boilerplate",
3 | "author": {
4 | "name": "Carlo Gino Catapang",
5 | "url": "https://carlogino.com"
6 | },
7 | "scripts": {
8 | "dev": "next",
9 | "build": "next build",
10 | "start": "next start"
11 | },
12 | "dependencies": {
13 | "i18next": "23.7.3",
14 | "i18next-browser-languagedetector": "7.2.0",
15 | "i18next-resources-to-backend": "1.2.0",
16 | "next": "14.0.3",
17 | "react": "18.2.0",
18 | "react-dom": "18.2.0",
19 | "react-i18next": "13.5.0"
20 | },
21 | "devDependencies": {
22 | "@types/node": "20.4.4",
23 | "@types/react": "18.2.42",
24 | "@types/react-dom": "18.2.17",
25 | "autoprefixer": "10.4.16",
26 | "eslint": "8.55.0",
27 | "eslint-config-next": "14.0.3",
28 | "eslint-config-prettier": "9.1.0",
29 | "postcss": "8.4.32",
30 | "prettier": "3.1.0",
31 | "tailwindcss": "3.3.6",
32 | "typescript": "5.3.3"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/components/ChangeLocale.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import {useRouter, useParams, useSelectedLayoutSegments} from 'next/navigation';
4 |
5 | const ChangeLocale = () => {
6 | const router = useRouter();
7 | const params = useParams();
8 | const urlSegments = useSelectedLayoutSegments();
9 |
10 | const handleLocaleChange = event => {
11 | const newLocale = event.target.value;
12 |
13 | // This is used by the Header component which is used in `app/[locale]/layout.tsx` file,
14 | // urlSegments will contain the segments after the locale.
15 | // We replace the URL with the new locale and the rest of the segments.
16 | router.push(`/${newLocale}/${urlSegments.join('/')}`);
17 | };
18 |
19 | return (
20 |
21 |
26 |
27 | );
28 | };
29 |
30 | export default ChangeLocale;
31 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import {usePathname, useParams} from 'next/navigation';
3 | import Link from 'next/link';
4 | import ChangeLocale from './ChangeLocale';
5 | import {useTranslation} from '../i18n/client';
6 | import type {LocaleTypes} from '../i18n/settings';
7 |
8 | const Header = () => {
9 | const pathName = usePathname();
10 | const locale = useParams()?.locale as LocaleTypes;
11 | const {t} = useTranslation(locale, 'common');
12 |
13 | return (
14 |
15 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default Header;
41 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import {NextResponse, NextRequest} from 'next/server';
2 | import {fallbackLng, locales} from './i18n/settings';
3 |
4 | export function middleware(request: NextRequest) {
5 | // Check if there is any supported locale in the pathname
6 | const pathname = request.nextUrl.pathname;
7 |
8 | // Check if the default locale is in the pathname
9 | if (
10 | pathname.startsWith(`/${fallbackLng}/`) ||
11 | pathname === `/${fallbackLng}`
12 | ) {
13 | // e.g. incoming request is /en/about
14 | // The new URL is now /about
15 | return NextResponse.redirect(
16 | new URL(
17 | pathname.replace(
18 | `/${fallbackLng}`,
19 | pathname === `/${fallbackLng}` ? '/' : '',
20 | ),
21 | request.url,
22 | ),
23 | );
24 | }
25 |
26 | const pathnameIsMissingLocale = locales.every(
27 | locale => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
28 | );
29 |
30 | if (pathnameIsMissingLocale) {
31 | // We are on the default locale
32 | // Rewrite so Next.js understands
33 |
34 | // e.g. incoming request is /about
35 | // Tell Next.js it should pretend it's /en/about
36 | return NextResponse.rewrite(
37 | new URL(`/${fallbackLng}${pathname}`, request.url),
38 | );
39 | }
40 | }
41 |
42 | export const config = {
43 | // Do not run the middleware on the following paths
44 | matcher:
45 | '/((?!api|_next/static|_next/image|manifest.json|assets|favicon.ico).*)',
46 | };
47 |
--------------------------------------------------------------------------------
/i18n/client.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {useEffect} from 'react';
4 | import i18next, {i18n} from 'i18next';
5 | import {initReactI18next, useTranslation as useTransAlias} from 'react-i18next';
6 | import resourcesToBackend from 'i18next-resources-to-backend';
7 | import LanguageDetector from 'i18next-browser-languagedetector';
8 | import {type LocaleTypes, getOptions, locales} from './settings';
9 |
10 | const runsOnServerSide = typeof window === 'undefined';
11 |
12 | // Initialize i18next for the client side
13 | i18next
14 | .use(initReactI18next)
15 | .use(LanguageDetector)
16 | .use(
17 | resourcesToBackend(
18 | (language: LocaleTypes, namespace: string) =>
19 | import(`./locales/${language}/${namespace}.json`),
20 | ),
21 | )
22 | .init({
23 | ...getOptions(),
24 | lng: undefined, // detect the language on the client
25 | detection: {
26 | order: ['path'],
27 | },
28 | preload: runsOnServerSide ? locales : [],
29 | });
30 |
31 | export function useTranslation(lng: LocaleTypes, ns: string) {
32 | const translator = useTransAlias(ns);
33 | const {i18n} = translator;
34 |
35 | // Run content is being rendered on server side
36 | if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
37 | i18n.changeLanguage(lng);
38 | } else {
39 | // Use our custom implementation when running on client side
40 | // eslint-disable-next-line react-hooks/rules-of-hooks
41 | useCustomTranslationImplem(i18n, lng);
42 | }
43 | return translator;
44 | }
45 |
46 | function useCustomTranslationImplem(i18n: i18n, lng: LocaleTypes) {
47 | // This effect changes the language of the application when the lng prop changes.
48 | useEffect(() => {
49 | if (!lng || i18n.resolvedLanguage === lng) return;
50 | i18n.changeLanguage(lng);
51 | }, [lng, i18n]);
52 | }
53 |
--------------------------------------------------------------------------------
/components/BuiltInFormatDemo.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import {useTranslation} from '../i18n/client';
4 | import type {LocaleTypes} from '../i18n/settings';
5 | import {useParams} from 'next/navigation';
6 |
7 | const BuiltInFormatsDemo = () => {
8 | let locale = useParams()?.locale as LocaleTypes;
9 |
10 | const {t} = useTranslation(locale, 'built-in-demo');
11 |
12 | return (
13 |
14 |
15 | {/* "number": "Number: {{val, number}}", */}
16 | {t('number', {
17 | val: 123456789.0123,
18 | })}
19 |
20 |
21 | {/* "currency": "Currency: {{val, currency}}", */}
22 | {t('currency', {
23 | val: 123456789.0123,
24 | style: 'currency',
25 | currency: 'USD',
26 | })}
27 |
28 |
29 |
30 | {/* "dateTime": "Date/Time: {{val, datetime}}", */}
31 | {t('dateTime', {
32 | val: new Date(1234567890123),
33 | formatParams: {
34 | val: {
35 | weekday: 'long',
36 | year: 'numeric',
37 | month: 'long',
38 | day: 'numeric',
39 | },
40 | },
41 | })}
42 |
43 |
44 |
45 | {/* "relativeTime": "Relative Time: {{val, relativetime}}", */}
46 | {t('relativeTime', {
47 | val: 12,
48 | style: 'long',
49 | })}
50 |
51 |
52 |
53 | {/* "list": "List: {{val, list}}", */}
54 | {t('list', {
55 | // https://www.i18next.com/translation-function/objects-and-arrays#objects
56 | // Check the link for more details on `returnObjects`
57 | val: t('weekdays', {returnObjects: true}),
58 | })}
59 |
60 |
61 | );
62 | };
63 |
64 | export default BuiltInFormatsDemo;
65 |
--------------------------------------------------------------------------------