├── .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 |
13 | 14 | 15 | 16 | 17 |
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 | --------------------------------------------------------------------------------