diff --git a/.eslintrc.js b/.eslintrc.js index bf8001b..a32ab9e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,9 @@ module.exports = { - extends: 'eslint-config-ns-ts', + extends: [ + 'eslint-config-ns-ts', + // @see https://nextjs.org/docs/basic-features/eslint + 'plugin:@next/next/recommended' + ], rules: { /** * This rule was disabled because of NextJS' Link API. diff --git a/.storybook/i18n.js b/.storybook/i18n.js new file mode 100644 index 0000000..d7c5533 --- /dev/null +++ b/.storybook/i18n.js @@ -0,0 +1,42 @@ +import { initReactI18next } from 'react-i18next' +import i18n from 'i18next'; + +import Backend from 'i18next-xhr-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + + +/** + * Example + * - @see https://github.com/i18next/react-i18next/blob/7cfab9746b3ccc6f833cd9c892e7c3c804944a5e/example/react-typescript4.1/namespaces/src/i18n/config.ts#L13 + */ +i18n +.use(Backend) +.use(LanguageDetector) +.use(initReactI18next) +.init({ + lng: 'en', + fallbackLng: 'en', + // have a common namespace used around the full app + ns: ['common'], + defaultNS: 'common', + debug: true, + interpolation: { + escapeValue: false, // not needed for react!! + }, + // resources: { + // en: { + // 'common': { + // "home": "Home GERMAN", + // "home_EN": "Home English" + // } + // }, + // de: { + // 'common': { + // "home": "Home DEUTSCH", + // "home_EN": "Home ENGLISCH" + // } + // } + // }, +}) + +export default i18n \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js index 37266f1..3ad3336 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -13,6 +13,8 @@ module.exports = { addons: [ '@storybook/addon-links', '@storybook/addon-essentials', + 'storybook-addon-next-router', + 'storybook-addon-i18next/register', { /** * NOTE: fix Storybook issue with PostCSS@8 @@ -44,6 +46,24 @@ module.exports = { ...config.resolve?.alias, '@': [path.resolve(__dirname, '../src/'), path.resolve(__dirname, '../')], } + + /** + * Fixes issue with `next-i18next` and is ready for webpack@5 + * @see https://github.com/isaachinman/next-i18next/issues/1012#issuecomment-792697008 + * @see https://github.com/storybookjs/storybook/issues/4082#issuecomment-758272734 + * @see https://webpack.js.org/migrate/5/ + * + * source: https://github.com/isaachinman/next-i18next/issues/1012#issuecomment-818042184 + */ + config.resolve.fallback = { + ...config.resolve?.fallback, + fs: false, + // tls: false, + // net: false, + // module: false, + // path: require.resolve('path-browserify'), + } + return config }, } diff --git a/.storybook/preview.js b/.storybook/preview.js index 452da4c..5906c32 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,16 +1,11 @@ -import { withNextRouter } from 'storybook-addon-next-router' +import React, { Suspense } from 'react'; +import { RouterContext } from "next/dist/next-server/lib/router-context"; +// import { I18nextProvider } from 'react-i18next'; +import { withI18next } from 'storybook-addon-i18next'; import '../src/styles/index.scss' -// @see https://www.npmjs.com/package/storybook-addon-next-router -export const decorators = [ - withNextRouter({ - path: '/', // defaults to `/` - asPath: '/', // defaults to `/` - query: {}, // defaults to `{}` - push() {}, // defaults to using addon actions integration, can override any method in the router - }), -] +import i18n from './i18n' export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, @@ -20,4 +15,18 @@ export const parameters = { date: /Date$/, }, }, + // @see https://www.npmjs.com/package/storybook-addon-next-router + nextRouter: { + Provider: RouterContext.Provider, + path: '/', // defaults to `/` + asPath: '/', // defaults to `/` + query: {}, // defaults to `{}` + push() {}, // defaults to using addon actions integration, can override any method in the router + } } + +export const decorators = [ + // Story => , + withI18next({ i18n, languages: { en: 'English', de: 'Deutsch' }}), + Story => +] diff --git a/__mocks__/react-i18next.js b/__mocks__/react-i18next.js new file mode 100644 index 0000000..6180d3f --- /dev/null +++ b/__mocks__/react-i18next.js @@ -0,0 +1,80 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable react/display-name */ +/* eslint-disable no-confusing-arrow */ + +/** + * Setup i18n in tests + * + * Docs + * @see https://react.i18next.com/misc/testing + * + * Inspired by + * @see https://github.com/i18next/react-i18next/blob/552ed79036c28f282afe7c6ccb525b82b76e71d5/example/test-jest/src/__mocks__/react-i18next.js + * + * Alternative examples using `i18n.use()...` + * @see https://github.com/i18next/react-i18next/blob/552ed79036c28f282afe7c6ccb525b82b76e71d5/example/test-jest/src/setupTests.js#L4-L23 + * @see https://github.com/i18next/react-i18next/blob/552ed79036c28f282afe7c6ccb525b82b76e71d5/test/i18n.js + * @see https://github.com/isaachinman/next-i18next/issues/377#issuecomment-700516821 + */ + +const React = require('react') +const reactI18next = require('react-i18next') + +const hasChildren = node => + node && (node.children || (node.props && node.props.children)) + +const getChildren = node => + node && node.children ? node.children : node.props && node.props.children + +const renderNodes = reactNodes => { + if (typeof reactNodes === 'string') { + return reactNodes + } + + return Object.keys(reactNodes).map((key, i) => { + const child = reactNodes[key] + const isElement = React.isValidElement(child) + + if (typeof child === 'string') { + return child + } + if (hasChildren(child)) { + const inner = renderNodes(getChildren(child)) + return React.cloneElement(child, { ...child.props, key: i }, inner) + } + if (typeof child === 'object' && !isElement) { + return Object.keys(child).reduce( + (str, childKey) => `${str}${child[childKey]}`, + '', + ) + } + + return child + }) +} + +/** + * @type any + */ +const useMock = [k => k, {}] +useMock.t = k => k +useMock.i18n = {} + +module.exports = { + // this mock makes sure any components using the translate HoC receive the t function as a prop + withTranslation: () => Component => props => ( + k} {...props} /> + ), + Trans: ({ children }) => + Array.isArray(children) ? renderNodes(children) : renderNodes([children]), + Translation: ({ children }) => children(k => k, { i18n: {} }), + useTranslation: () => useMock, + + // mock if needed + I18nextProvider: reactI18next.I18nextProvider, + initReactI18next: reactI18next.initReactI18next, + setDefaults: reactI18next.setDefaults, + getDefaults: reactI18next.getDefaults, + setI18n: reactI18next.setI18n, + getI18n: reactI18next.getI18n, +} diff --git a/cypress.json b/cypress.json index 17ef242..ba9bc7b 100644 --- a/cypress.json +++ b/cypress.json @@ -1,3 +1,4 @@ { - "baseUrl": "http://localhost:3000" + "baseUrl": "http://localhost:3000", + "video": false } diff --git a/jest.setup.ts b/jest.setup.ts index b854473..7f17313 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,11 +1,8 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable import/no-extraneous-dependencies */ import 'jest-preset-ns/presets/react/jest-setup.js' - import { setConfig } from 'next/config' import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' -import i18n from 'i18next' -import { initReactI18next } from 'react-i18next' import { NextRouter } from 'next/router' // @ts-ignore @@ -22,29 +19,6 @@ const { publicRuntimeConfig, serverRuntimeConfig } = NextConfig( */ setConfig({ publicRuntimeConfig, serverRuntimeConfig }) -/** - * Setup i18n in tests - * - * @see https://github.com/i18next/react-i18next/blob/552ed79036c28f282afe7c6ccb525b82b76e71d5/example/test-jest/src/setupTests.js#L4-L23 - * @see https://github.com/isaachinman/next-i18next/issues/377#issuecomment-700516821 - */ -i18n.use(initReactI18next).init({ - lng: 'en', - fallbackLng: 'en', - - // have a common namespace used around the full app - ns: ['translations'], - defaultNS: 'translations', - - // debug: true, - - interpolation: { - escapeValue: false, // not needed for react!! - }, - - resources: { en: { translations: {} } }, -}) - /** * mockRouter mocks the initial router state of Next.js * @see https://github.com/vercel/next.js/issues/7479#issuecomment-752418517 diff --git a/next-env.d.ts b/next-env.d.ts index 3b89435..c6643fd 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,18 +1,3 @@ /// /// - -type NextConfig = { - publicRuntimeConfig: { - NODE_ENV: string - /** - * version of the app - */ - VERSION: string - } - serverRuntimeConfig: Record -} - -declare module 'next/config' { - export declare function setConfig(configValue: any): void - export default function getConfig(): NextConfig -} +/// diff --git a/next.config.js b/next.config.js index 7add2a9..01d11aa 100644 --- a/next.config.js +++ b/next.config.js @@ -13,15 +13,13 @@ const version = require('./version') * – It should be availabe already: https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/config-shared.ts#L12 */ const nextConfig = { - poweredByHeader: false, - /** - * Opt-in to webpack 5 support - * @see https://github.com/vercel/next.js/issues/21679#issuecomment-771941447 - */ - future: { - webpack5: true, + eslint: { + // We have manual checks in place to make sure we do not build dangerous + // code. + ignoreDuringBuilds: true, }, i18n, + poweredByHeader: false, publicRuntimeConfig: { NODE_ENV: process.env.NODE_ENV, VERSION: version, diff --git a/package.json b/package.json index dd3cd06..790dea3 100644 --- a/package.json +++ b/package.json @@ -22,46 +22,54 @@ "lint:css": "stylelint --cache './**/*.{ts,tsx,scss}'", "prepare": "node scripts/setup && is-ci || husky install", "start": "next start", - "storybook": "start-storybook -p 6006", + "storybook": "start-storybook -p 6006 -s ./public", "test": "jest", "test:cypress": "start-server-and-test dev 3000 cypress:open", - "test:cypress:ci": "start-server-and-test dev 3000 cypress:run" + "test:cypress:ci": "yarn build && start-server-and-test start 3000 cypress:run" }, "dependencies": { - "date-fns": "^2.21.1", - "next": "^10.1.3", + "date-fns": "^2.23.0", + "i18next": "^20.3.5", + "i18next-browser-languagedetector": "^6.1.2", + "i18next-xhr-backend": "^3.2.2", + "next": "^11.0.1", "next-compose-plugins": "^2.2.1", - "next-i18next": "^8.2.0", - "react": "^16.14.0", + "next-i18next": "^8.5.5", + "react": "^17.0.2", "react-cookie": "^4.0.3", - "react-dom": "^16.14.0" + "react-dom": "^17.0.2", + "react-i18next": "^11.11.4" }, "devDependencies": { - "@babel/core": "^7.13.16", - "@commitlint/cli": "^12.1.1", - "@commitlint/config-conventional": "^12.1.1", + "@babel/core": "^7.14.8", + "@commitlint/cli": "^13.1.0", + "@commitlint/config-conventional": "^13.1.0", "@natterstefan/scripts": "^1.2.0", + "@next/eslint-plugin-next": "^11.0.1", "@semantic-release/changelog": "^5.0.1", "@semantic-release/commit-analyzer": "^8.0.1", "@semantic-release/git": "^9.0.0", - "@semantic-release/github": "^7.2.1", - "@semantic-release/release-notes-generator": "^9.0.2", - "@storybook/addon-actions": "^6.2.9", - "@storybook/addon-essentials": "^6.2.9", - "@storybook/addon-links": "^6.2.9", + "@semantic-release/github": "^7.2.3", + "@semantic-release/release-notes-generator": "^9.0.3", + "@storybook/addon-actions": "^6.3.6", + "@storybook/addon-essentials": "^6.3.6", + "@storybook/addon-links": "^6.3.6", "@storybook/addon-postcss": "^2.0.0", - "@storybook/addons": "^6.2.9", - "@storybook/builder-webpack5": "^6.2.9", - "@storybook/react": "^6.2.9", - "@types/enzyme": "^3.10.8", + "@storybook/addons": "^6.3.6", + "@storybook/builder-webpack5": "^6.3.6", + "@storybook/manager-webpack5": "^6.3.6", + "@storybook/react": "^6.3.6", + "@types/enzyme": "^3.10.9", + "@types/jest": "^26.0.24", "@types/node": "^14.14.41", - "@types/react": "^17.0.3", - "@types/react-dom": "^17.0.3", + "@types/react": "^17.0.15", + "@types/react-dom": "^17.0.9", + "@wojtekmaj/enzyme-adapter-react-17": "^0.6.3", "all-contributors-cli": "^6.20.0", - "autoprefixer": "^10.2.5", + "autoprefixer": "^10.3.1", "babel-loader": "^8.2.2", - "css-loader": "^5.2.4", - "cypress": "^6.9.1", + "css-loader": "^6.2.0", + "cypress": "^8.1.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", "eslint-config-airbnb": "^18.2.1", @@ -69,31 +77,33 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^24.3.5", + "eslint-plugin-jest": "^24.4.0", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-react": "^7.23.2", "eslint-plugin-react-hooks": "^4.2.0", - "husky": "^6.0.0", + "husky": "^7.0.1", "is-ci": "^3.0.0", - "jest": "^26.6.3", - "jest-preset-ns": "^0.2.0", - "postcss": "^8.2.12", + "jest": "^27.0.6", + "jest-date-mock": "^1.0.8", + "jest-preset-ns": "^1.0.0", + "postcss": "^8.3.6", "postcss-flexbugs-fixes": "^5.0.2", - "postcss-import": "^14.0.1", - "postcss-loader": "^5.2.0", + "postcss-import": "^14.0.2", + "postcss-loader": "^6.1.1", "postcss-preset-env": "^6.7.0", "prettier": "^2.2.1", - "sass": "^1.32.11", - "sass-loader": "^11.0.1", - "semantic-release": "^17.4.2", - "start-server-and-test": "^1.12.1", - "storybook-addon-next-router": "^2.0.4", - "style-loader": "^2.0.0", - "stylelint": "^13.13.0", + "sass": "^1.37.2", + "sass-loader": "^12.1.0", + "semantic-release": "^17.4.4", + "start-server-and-test": "^1.13.1", + "storybook-addon-i18next": "^1.3.0", + "storybook-addon-next-router": "^3.0.5", + "style-loader": "^3.2.1", + "stylelint": "^13.13.1", "stylelint-config-recommended": "^5.0.0", - "tailwindcss": "^2.1.2", - "typescript": "^4.2.4", - "webpack": "^5.35.1" + "tailwindcss": "^2.2.7", + "typescript": "^4.3.5", + "webpack": "^5.48.0" } } diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx index 4159ae7..f7df587 100644 --- a/src/components/navigation/Navigation.tsx +++ b/src/components/navigation/Navigation.tsx @@ -12,7 +12,7 @@ const links = [ ] export const Navigation = () => { - const { t } = useTranslation() + const { t } = useTranslation('common') return (