mirror of
				https://github.com/Mastermindzh/react-starter-kit.git
				synced 2025-11-04 02:38:47 +01:00 
			
		
		
		
	- Added translations
- Added pluralization example - Added formatter example (with Luxon) - Used HTTP loader - Added a suspense fallback page for app loading - Added cypress eslint rules
This commit is contained in:
		
							
								
								
									
										12
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -4,6 +4,8 @@
 | 
			
		||||
    "camelcase",
 | 
			
		||||
    "flexbugs",
 | 
			
		||||
    "Immer",
 | 
			
		||||
    "languagedetector",
 | 
			
		||||
    "luxon",
 | 
			
		||||
    "pmmmwh",
 | 
			
		||||
    "reduxjs",
 | 
			
		||||
    "SVGR",
 | 
			
		||||
@@ -12,6 +14,16 @@
 | 
			
		||||
    "typeahead",
 | 
			
		||||
    "uncompiled"
 | 
			
		||||
  ],
 | 
			
		||||
  "cSpell.ignorePaths": [
 | 
			
		||||
    "package-lock.json",
 | 
			
		||||
    "node_modules",
 | 
			
		||||
    "vscode-extension",
 | 
			
		||||
    ".git/objects",
 | 
			
		||||
    ".vscode",
 | 
			
		||||
    ".vscode-insiders",
 | 
			
		||||
    "public/i18n/*",
 | 
			
		||||
    "!public/i18n/en.json"
 | 
			
		||||
  ],
 | 
			
		||||
  "files.exclude": {
 | 
			
		||||
    "**/.git": true,
 | 
			
		||||
    "coverage": true,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
 | 
			
		||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 | 
			
		||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 | 
			
		||||
 | 
			
		||||
## [0.3.0] - 2022-07-07
 | 
			
		||||
 | 
			
		||||
- Added translations
 | 
			
		||||
  - Added pluralization example
 | 
			
		||||
  - Added formatter example (with Luxon)
 | 
			
		||||
  - Used HTTP loader
 | 
			
		||||
- Added a suspense fallback page for app loading
 | 
			
		||||
- Added cypress eslint rules
 | 
			
		||||
 | 
			
		||||
## [0.2.0] - 2022-06-27
 | 
			
		||||
 | 
			
		||||
- Added [cypress.io](https://www.cypress.io/)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,10 @@ const config = {
 | 
			
		||||
  collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
 | 
			
		||||
  setupFiles: ["react-app-polyfill/jsdom"],
 | 
			
		||||
  setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
 | 
			
		||||
  testMatch: ["<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"],
 | 
			
		||||
  testMatch: [
 | 
			
		||||
    "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
 | 
			
		||||
    "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}",
 | 
			
		||||
  ],
 | 
			
		||||
  testEnvironment: "jsdom",
 | 
			
		||||
  transform: {
 | 
			
		||||
    "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
 | 
			
		||||
@@ -21,7 +24,18 @@ const config = {
 | 
			
		||||
    "^react-native$": "react-native-web",
 | 
			
		||||
    "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
 | 
			
		||||
  },
 | 
			
		||||
  moduleFileExtensions: ["web.js", "js", "web.ts", "ts", "web.tsx", "tsx", "json", "web.jsx", "jsx", "node"],
 | 
			
		||||
  moduleFileExtensions: [
 | 
			
		||||
    "web.js",
 | 
			
		||||
    "js",
 | 
			
		||||
    "web.ts",
 | 
			
		||||
    "ts",
 | 
			
		||||
    "web.tsx",
 | 
			
		||||
    "tsx",
 | 
			
		||||
    "json",
 | 
			
		||||
    "web.jsx",
 | 
			
		||||
    "jsx",
 | 
			
		||||
    "node",
 | 
			
		||||
  ],
 | 
			
		||||
  watchPlugins: ["jest-watch-typeahead/filename", "jest-watch-typeahead/testname"],
 | 
			
		||||
  resetMocks: true,
 | 
			
		||||
  coveragePathIgnorePatterns: [
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										245
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										245
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -11,6 +11,8 @@
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@reduxjs/toolkit": "^1.8.2",
 | 
			
		||||
        "i18next-http-backend": "^1.4.1",
 | 
			
		||||
        "luxon": "^2.4.0",
 | 
			
		||||
        "react": "^18.2.0",
 | 
			
		||||
        "react-app-polyfill": "^3.0.0",
 | 
			
		||||
        "react-dev-utils": "^12.0.1",
 | 
			
		||||
@@ -28,6 +30,7 @@
 | 
			
		||||
        "@testing-library/react": "^13.3.0",
 | 
			
		||||
        "@testing-library/user-event": "^14.2.1",
 | 
			
		||||
        "@types/jest": "^27.5.2",
 | 
			
		||||
        "@types/luxon": "^2.3.2",
 | 
			
		||||
        "@types/node": "^17.0.45",
 | 
			
		||||
        "@types/react": "^18.0.14",
 | 
			
		||||
        "@types/react-dom": "^18.0.5",
 | 
			
		||||
@@ -57,6 +60,8 @@
 | 
			
		||||
        "fs-extra": "^10.0.0",
 | 
			
		||||
        "html-webpack-plugin": "^5.5.0",
 | 
			
		||||
        "husky": "^8.0.1",
 | 
			
		||||
        "i18next": "^21.8.13",
 | 
			
		||||
        "i18next-browser-languagedetector": "^6.1.4",
 | 
			
		||||
        "identity-obj-proxy": "^3.0.0",
 | 
			
		||||
        "immer": "^9.0.15",
 | 
			
		||||
        "jest": "^27.4.3",
 | 
			
		||||
@@ -72,6 +77,7 @@
 | 
			
		||||
        "prettier": "^2.7.1",
 | 
			
		||||
        "pretty-quick": "^3.1.3",
 | 
			
		||||
        "prompts": "^2.4.2",
 | 
			
		||||
        "react-i18next": "^11.18.0",
 | 
			
		||||
        "react-refresh": "^0.11.0",
 | 
			
		||||
        "resolve": "^1.20.0",
 | 
			
		||||
        "resolve-url-loader": "^4.0.0",
 | 
			
		||||
@@ -4156,6 +4162,12 @@
 | 
			
		||||
      "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/luxon": {
 | 
			
		||||
      "version": "2.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz",
 | 
			
		||||
      "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/mime": {
 | 
			
		||||
      "version": "1.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
 | 
			
		||||
@@ -6608,6 +6620,14 @@
 | 
			
		||||
        "node": ">=10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cross-fetch": {
 | 
			
		||||
      "version": "3.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
 | 
			
		||||
      "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "node-fetch": "2.6.7"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/cross-spawn": {
 | 
			
		||||
      "version": "7.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
 | 
			
		||||
@@ -10162,6 +10182,15 @@
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/html-parse-stringify": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "void-elements": "3.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/html-webpack-plugin": {
 | 
			
		||||
      "version": "5.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
 | 
			
		||||
@@ -10335,6 +10364,46 @@
 | 
			
		||||
        "url": "https://github.com/sponsors/typicode"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/i18next": {
 | 
			
		||||
      "version": "21.8.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.13.tgz",
 | 
			
		||||
      "integrity": "sha512-DpzwrJq7Y8tjUHxx6ByOkUIjrGYdQI5Mfv4XEI7q2RWdknQ7TaO9bKi8hS/LqYD6pBV5YGxJLReyLkOCxIpouA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "individual",
 | 
			
		||||
          "url": "https://locize.com"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "type": "individual",
 | 
			
		||||
          "url": "https://locize.com/i18next.html"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "type": "individual",
 | 
			
		||||
          "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.17.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/i18next-browser-languagedetector": {
 | 
			
		||||
      "version": "6.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.4.tgz",
 | 
			
		||||
      "integrity": "sha512-wukWnFeU7rKIWT66VU5i8I+3Zc4wReGcuDK2+kuFhtoxBRGWGdvYI9UQmqNL/yQH1KogWwh+xGEaIPH8V/i2Zg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.14.6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/i18next-http-backend": {
 | 
			
		||||
      "version": "1.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.1.tgz",
 | 
			
		||||
      "integrity": "sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "cross-fetch": "3.1.5"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/iconv-lite": {
 | 
			
		||||
      "version": "0.6.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
 | 
			
		||||
@@ -14041,6 +14110,14 @@
 | 
			
		||||
        "node": ">=10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/luxon": {
 | 
			
		||||
      "version": "2.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz",
 | 
			
		||||
      "integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lz-string": {
 | 
			
		||||
      "version": "1.4.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
 | 
			
		||||
@@ -14398,6 +14475,44 @@
 | 
			
		||||
        "tslib": "^2.0.3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/node-fetch": {
 | 
			
		||||
      "version": "2.6.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
 | 
			
		||||
      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "whatwg-url": "^5.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "4.x || >=6.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "encoding": "^0.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependenciesMeta": {
 | 
			
		||||
        "encoding": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/node-fetch/node_modules/tr46": {
 | 
			
		||||
      "version": "0.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/node-fetch/node_modules/webidl-conversions": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/node-fetch/node_modules/whatwg-url": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tr46": "~0.0.3",
 | 
			
		||||
        "webidl-conversions": "^3.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/node-forge": {
 | 
			
		||||
      "version": "1.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
 | 
			
		||||
@@ -16891,6 +17006,28 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
 | 
			
		||||
      "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-i18next": {
 | 
			
		||||
      "version": "11.18.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.0.tgz",
 | 
			
		||||
      "integrity": "sha512-coJujU20xJ5Wa5rHjTyB5LFKZb1yfXo2A+40RRSyAF0FlZRHyy+3C1Mr92x1JPfS7W7v2TWNn8mRhpDFGJwXVg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.14.5",
 | 
			
		||||
        "html-parse-stringify": "^3.0.1"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "i18next": ">= 19.0.0",
 | 
			
		||||
        "react": ">= 16.8.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependenciesMeta": {
 | 
			
		||||
        "react-dom": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        },
 | 
			
		||||
        "react-native": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-is": {
 | 
			
		||||
      "version": "17.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
 | 
			
		||||
@@ -19158,6 +19295,15 @@
 | 
			
		||||
      "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/void-elements": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/w3c-hr-time": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
 | 
			
		||||
@@ -23105,6 +23251,12 @@
 | 
			
		||||
      "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "@types/luxon": {
 | 
			
		||||
      "version": "2.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz",
 | 
			
		||||
      "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "@types/mime": {
 | 
			
		||||
      "version": "1.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
 | 
			
		||||
@@ -24966,6 +25118,14 @@
 | 
			
		||||
        "yaml": "^1.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "cross-fetch": {
 | 
			
		||||
      "version": "3.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
 | 
			
		||||
      "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "node-fetch": "2.6.7"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "cross-spawn": {
 | 
			
		||||
      "version": "7.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
 | 
			
		||||
@@ -27605,6 +27765,15 @@
 | 
			
		||||
        "terser": "^5.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "html-parse-stringify": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "void-elements": "3.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "html-webpack-plugin": {
 | 
			
		||||
      "version": "5.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
 | 
			
		||||
@@ -27723,6 +27892,32 @@
 | 
			
		||||
      "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "i18next": {
 | 
			
		||||
      "version": "21.8.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.13.tgz",
 | 
			
		||||
      "integrity": "sha512-DpzwrJq7Y8tjUHxx6ByOkUIjrGYdQI5Mfv4XEI7q2RWdknQ7TaO9bKi8hS/LqYD6pBV5YGxJLReyLkOCxIpouA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@babel/runtime": "^7.17.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "i18next-browser-languagedetector": {
 | 
			
		||||
      "version": "6.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.4.tgz",
 | 
			
		||||
      "integrity": "sha512-wukWnFeU7rKIWT66VU5i8I+3Zc4wReGcuDK2+kuFhtoxBRGWGdvYI9UQmqNL/yQH1KogWwh+xGEaIPH8V/i2Zg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@babel/runtime": "^7.14.6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "i18next-http-backend": {
 | 
			
		||||
      "version": "1.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.1.tgz",
 | 
			
		||||
      "integrity": "sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "cross-fetch": "3.1.5"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "iconv-lite": {
 | 
			
		||||
      "version": "0.6.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
 | 
			
		||||
@@ -30453,6 +30648,11 @@
 | 
			
		||||
        "yallist": "^4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "luxon": {
 | 
			
		||||
      "version": "2.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz",
 | 
			
		||||
      "integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA=="
 | 
			
		||||
    },
 | 
			
		||||
    "lz-string": {
 | 
			
		||||
      "version": "1.4.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
 | 
			
		||||
@@ -30721,6 +30921,35 @@
 | 
			
		||||
        "tslib": "^2.0.3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node-fetch": {
 | 
			
		||||
      "version": "2.6.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
 | 
			
		||||
      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "whatwg-url": "^5.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tr46": {
 | 
			
		||||
          "version": "0.0.3",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
 | 
			
		||||
          "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
 | 
			
		||||
        },
 | 
			
		||||
        "webidl-conversions": {
 | 
			
		||||
          "version": "3.0.1",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
 | 
			
		||||
          "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
 | 
			
		||||
        },
 | 
			
		||||
        "whatwg-url": {
 | 
			
		||||
          "version": "5.0.0",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 | 
			
		||||
          "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "tr46": "~0.0.3",
 | 
			
		||||
            "webidl-conversions": "^3.0.0"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node-forge": {
 | 
			
		||||
      "version": "1.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
 | 
			
		||||
@@ -32375,6 +32604,16 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
 | 
			
		||||
      "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
 | 
			
		||||
    },
 | 
			
		||||
    "react-i18next": {
 | 
			
		||||
      "version": "11.18.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.0.tgz",
 | 
			
		||||
      "integrity": "sha512-coJujU20xJ5Wa5rHjTyB5LFKZb1yfXo2A+40RRSyAF0FlZRHyy+3C1Mr92x1JPfS7W7v2TWNn8mRhpDFGJwXVg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@babel/runtime": "^7.14.5",
 | 
			
		||||
        "html-parse-stringify": "^3.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "react-is": {
 | 
			
		||||
      "version": "17.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
 | 
			
		||||
@@ -34076,6 +34315,12 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "void-elements": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "w3c-hr-time": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,8 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@reduxjs/toolkit": "^1.8.2",
 | 
			
		||||
    "i18next-http-backend": "^1.4.1",
 | 
			
		||||
    "luxon": "^2.4.0",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-app-polyfill": "^3.0.0",
 | 
			
		||||
    "react-dev-utils": "^12.0.1",
 | 
			
		||||
@@ -60,6 +62,7 @@
 | 
			
		||||
    "@testing-library/react": "^13.3.0",
 | 
			
		||||
    "@testing-library/user-event": "^14.2.1",
 | 
			
		||||
    "@types/jest": "^27.5.2",
 | 
			
		||||
    "@types/luxon": "^2.3.2",
 | 
			
		||||
    "@types/node": "^17.0.45",
 | 
			
		||||
    "@types/react": "^18.0.14",
 | 
			
		||||
    "@types/react-dom": "^18.0.5",
 | 
			
		||||
@@ -89,6 +92,8 @@
 | 
			
		||||
    "fs-extra": "^10.0.0",
 | 
			
		||||
    "html-webpack-plugin": "^5.5.0",
 | 
			
		||||
    "husky": "^8.0.1",
 | 
			
		||||
    "i18next": "^21.8.13",
 | 
			
		||||
    "i18next-browser-languagedetector": "^6.1.4",
 | 
			
		||||
    "identity-obj-proxy": "^3.0.0",
 | 
			
		||||
    "immer": "^9.0.15",
 | 
			
		||||
    "jest": "^27.4.3",
 | 
			
		||||
@@ -104,6 +109,7 @@
 | 
			
		||||
    "prettier": "^2.7.1",
 | 
			
		||||
    "pretty-quick": "^3.1.3",
 | 
			
		||||
    "prompts": "^2.4.2",
 | 
			
		||||
    "react-i18next": "^11.18.0",
 | 
			
		||||
    "react-refresh": "^0.11.0",
 | 
			
		||||
    "resolve": "^1.20.0",
 | 
			
		||||
    "resolve-url-loader": "^4.0.0",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								public/i18n/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								public/i18n/en.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
{
 | 
			
		||||
  "common": {
 | 
			
		||||
    "welcome": "Welcome!"
 | 
			
		||||
  },
 | 
			
		||||
  "navBar": {
 | 
			
		||||
    "intro": "Our fancy header with navigation.",
 | 
			
		||||
    "version": "App version:",
 | 
			
		||||
    "currentDate": "Today's date: {{date, formattedDate}}"
 | 
			
		||||
  },
 | 
			
		||||
  "nav": {
 | 
			
		||||
    "home": "home",
 | 
			
		||||
    "about": "about",
 | 
			
		||||
    "counter": "counter"
 | 
			
		||||
  },
 | 
			
		||||
  "about": {
 | 
			
		||||
    "title": "About"
 | 
			
		||||
  },
 | 
			
		||||
  "counter": {
 | 
			
		||||
    "status": "Working status: {{status}}",
 | 
			
		||||
    "add_zero": "Please enter a value",
 | 
			
		||||
    "add_one": "Add one",
 | 
			
		||||
    "add_other": "Add {{count}}"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								public/i18n/nl.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								public/i18n/nl.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
{
 | 
			
		||||
  "common": {
 | 
			
		||||
    "welcome": "Welkom!"
 | 
			
		||||
  },
 | 
			
		||||
  "navBar": {
 | 
			
		||||
    "intro": "Een fancy header met navigatie",
 | 
			
		||||
    "version": "Aplicatie versie:",
 | 
			
		||||
    "currentDate": "De datum van vandaag: {{date, formattedDate}}"
 | 
			
		||||
  },
 | 
			
		||||
  "nav": {
 | 
			
		||||
    "home": "home",
 | 
			
		||||
    "about": "over ons",
 | 
			
		||||
    "counter": "teller"
 | 
			
		||||
  },
 | 
			
		||||
  "about": {
 | 
			
		||||
    "title": "Over ons"
 | 
			
		||||
  },
 | 
			
		||||
  "counter": {
 | 
			
		||||
    "status": "Staat van werking: {{status}}",
 | 
			
		||||
    "add_zero": "Vul aub een waarde in",
 | 
			
		||||
    "add_one": "+1",
 | 
			
		||||
    "add_other": "Voeg {{count}} toe"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/app/tests/mocks/i18n/WithTestTranslations.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/app/tests/mocks/i18n/WithTestTranslations.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import { FunctionComponent, ReactNode } from "react";
 | 
			
		||||
import { I18nextProvider, useTranslation } from "react-i18next";
 | 
			
		||||
import i18n from "./i18n";
 | 
			
		||||
 | 
			
		||||
type Props = { children?: ReactNode; keysOnly?: boolean };
 | 
			
		||||
 | 
			
		||||
const ProvidedComponent: FunctionComponent<Props> = ({ children, keysOnly }) => {
 | 
			
		||||
  const [_translate, i18nSettings] = useTranslation();
 | 
			
		||||
  if (keysOnly) {
 | 
			
		||||
    i18nSettings.changeLanguage("noLang");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <>{children}</>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const WithTestTranslations: FunctionComponent<Props> = ({ children, keysOnly = false }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <I18nextProvider i18n={i18n}>
 | 
			
		||||
      <ProvidedComponent keysOnly={keysOnly}>{children}</ProvidedComponent>
 | 
			
		||||
    </I18nextProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										33
									
								
								src/app/tests/mocks/i18n/i18n.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/app/tests/mocks/i18n/i18n.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import i18n from "i18next";
 | 
			
		||||
import LanguageDetector from "i18next-browser-languagedetector";
 | 
			
		||||
import { initReactI18next } from "react-i18next";
 | 
			
		||||
import { i18nSettings } from "../../../../infrastructure/i18n/init";
 | 
			
		||||
 | 
			
		||||
import en from "app/../../public/i18n/en.json";
 | 
			
		||||
import nl from "app/../../public/i18n/nl.json";
 | 
			
		||||
 | 
			
		||||
i18n
 | 
			
		||||
  // detect user language
 | 
			
		||||
  .use(LanguageDetector)
 | 
			
		||||
  .use(initReactI18next)
 | 
			
		||||
  .init({
 | 
			
		||||
    ...i18nSettings,
 | 
			
		||||
    ...{
 | 
			
		||||
      backend: undefined,
 | 
			
		||||
      fallbackLng: "noLang",
 | 
			
		||||
      debug: false,
 | 
			
		||||
      resources: {
 | 
			
		||||
        en: {
 | 
			
		||||
          translation: en,
 | 
			
		||||
        },
 | 
			
		||||
        nl: {
 | 
			
		||||
          translation: nl,
 | 
			
		||||
        },
 | 
			
		||||
        noLang: {
 | 
			
		||||
          translation: {},
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export default i18n;
 | 
			
		||||
@@ -1,10 +1,27 @@
 | 
			
		||||
import { render, screen } from "@testing-library/react";
 | 
			
		||||
import { WithTestTranslations } from "../../app/tests/mocks/i18n/WithTestTranslations";
 | 
			
		||||
 | 
			
		||||
import { AboutContainer } from "./About";
 | 
			
		||||
 | 
			
		||||
describe("About container", () => {
 | 
			
		||||
  it("renders welcome to the about page", () => {
 | 
			
		||||
    render(<AboutContainer />);
 | 
			
		||||
    render(
 | 
			
		||||
      <WithTestTranslations>
 | 
			
		||||
        <AboutContainer />
 | 
			
		||||
      </WithTestTranslations>,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText(/Welcome to the about page/i)).toBeInTheDocument();
 | 
			
		||||
    expect(screen.getByText(/About/)).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("uses the about.title key for translation rendering", () => {
 | 
			
		||||
    // we can specify that we only want translations keys to be rendered so we can check for translation keys instead
 | 
			
		||||
    render(
 | 
			
		||||
      <WithTestTranslations keysOnly={true}>
 | 
			
		||||
        <AboutContainer />
 | 
			
		||||
      </WithTestTranslations>,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByText(/about.title/)).toBeInTheDocument();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import { FunctionComponent } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
type Props = {};
 | 
			
		||||
 | 
			
		||||
export const AboutContainer: FunctionComponent<Props> = () => {
 | 
			
		||||
  return <h1>Welcome to the about page :)</h1>;
 | 
			
		||||
  const [translate] = useTranslation();
 | 
			
		||||
  return <h1>{translate("about.title")}</h1>;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
 | 
			
		||||
import styles from "./Counter.module.css";
 | 
			
		||||
import { incrementAsync } from "./state/actions/incrementAsync";
 | 
			
		||||
@@ -15,12 +16,13 @@ export function CounterContainer() {
 | 
			
		||||
  const { value, status } = useAppSelector(selectCountAndStatus);
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
  const [incrementAmount, setIncrementAmount] = useState("2");
 | 
			
		||||
  const [translate] = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const incrementValue = Number(incrementAmount) || 0;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      Status: {status}
 | 
			
		||||
      {translate("counter.status", { status })}
 | 
			
		||||
      <div className={styles.row}>
 | 
			
		||||
        <button
 | 
			
		||||
          className={styles.button}
 | 
			
		||||
@@ -49,7 +51,10 @@ export function CounterContainer() {
 | 
			
		||||
          className={styles.button}
 | 
			
		||||
          onClick={() => dispatch(incrementByAmount(incrementValue))}
 | 
			
		||||
        >
 | 
			
		||||
          Add Amount
 | 
			
		||||
          {/* Setting count allows you to pluralize / display different text based on the count
 | 
			
		||||
              See: https://www.i18next.com/translation-function/plurals
 | 
			
		||||
          */}
 | 
			
		||||
          {translate("counter.add", { count: incrementValue })}
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
          className={styles.asyncButton}
 | 
			
		||||
@@ -57,10 +62,7 @@ export function CounterContainer() {
 | 
			
		||||
        >
 | 
			
		||||
          Add Async
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
          className={styles.button}
 | 
			
		||||
          onClick={() => dispatch(incrementIfOdd(incrementValue))}
 | 
			
		||||
        >
 | 
			
		||||
        <button className={styles.button} onClick={() => dispatch(incrementIfOdd(incrementValue))}>
 | 
			
		||||
          Add If Odd
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ import { Provider } from "react-redux";
 | 
			
		||||
import App from "./App";
 | 
			
		||||
import { store } from "./app/store";
 | 
			
		||||
import "./index.css";
 | 
			
		||||
import "./infrastructure/i18n/init";
 | 
			
		||||
import { Loader } from "./infrastructure/wrappers/WithPageSuspense";
 | 
			
		||||
import reportWebVitals from "./reportWebVitals";
 | 
			
		||||
 | 
			
		||||
const container = document.getElementById("root")!;
 | 
			
		||||
@@ -12,7 +14,9 @@ const root = createRoot(container);
 | 
			
		||||
root.render(
 | 
			
		||||
  <React.StrictMode>
 | 
			
		||||
    <Provider store={store}>
 | 
			
		||||
      <App />
 | 
			
		||||
      <Loader>
 | 
			
		||||
        <App />
 | 
			
		||||
      </Loader>
 | 
			
		||||
    </Provider>
 | 
			
		||||
  </React.StrictMode>,
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								src/infrastructure/i18n/formatters/formattedDate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/infrastructure/i18n/formatters/formattedDate.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import { DateTime } from "luxon";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Luxon based date formatter for a specific language
 | 
			
		||||
 * @param value date to format
 | 
			
		||||
 * @param language language of the current user
 | 
			
		||||
 * @returns formatted date
 | 
			
		||||
 */
 | 
			
		||||
export const FormattedDate = (value: Date, language: string | undefined) => {
 | 
			
		||||
  if (!language) {
 | 
			
		||||
    language = "en";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let format;
 | 
			
		||||
 | 
			
		||||
  switch (language) {
 | 
			
		||||
    case "nl":
 | 
			
		||||
      format = "dd/MM/yyyy";
 | 
			
		||||
      break;
 | 
			
		||||
    case "en":
 | 
			
		||||
      format = "yyyy-MM-dd";
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      format = "yyyy-MM-dd";
 | 
			
		||||
  }
 | 
			
		||||
  return DateTime.fromJSDate(value).setLocale(language).toFormat(format);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										32
									
								
								src/infrastructure/i18n/init.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/infrastructure/i18n/init.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import i18n, { InitOptions } from "i18next";
 | 
			
		||||
import LanguageDetector from "i18next-browser-languagedetector";
 | 
			
		||||
import Backend from "i18next-http-backend";
 | 
			
		||||
import { initReactI18next } from "react-i18next";
 | 
			
		||||
import { FormattedDate } from "./formatters/formattedDate";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Initial i18n settings
 | 
			
		||||
 * https://www.i18next.com/overview/configuration-options
 | 
			
		||||
 */
 | 
			
		||||
export const i18nSettings: InitOptions = {
 | 
			
		||||
  debug: false,
 | 
			
		||||
  fallbackLng: "en",
 | 
			
		||||
  lng: "en",
 | 
			
		||||
  interpolation: {
 | 
			
		||||
    escapeValue: false, // not needed for react as it escapes by default
 | 
			
		||||
  },
 | 
			
		||||
  // i18next-http-back-end options, for all options read: https://github.com/i18next/i18next-http-backend
 | 
			
		||||
  backend: { loadPath: "/i18n/{{lng}}.json" },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
i18n
 | 
			
		||||
  // detect user language
 | 
			
		||||
  .use(Backend)
 | 
			
		||||
  .use(LanguageDetector)
 | 
			
		||||
  .use(initReactI18next)
 | 
			
		||||
  .init(i18nSettings);
 | 
			
		||||
 | 
			
		||||
// format date based on language
 | 
			
		||||
i18n.services.formatter?.add("formattedDate", FormattedDate);
 | 
			
		||||
 | 
			
		||||
export default i18n;
 | 
			
		||||
							
								
								
									
										7
									
								
								src/infrastructure/loader/appLoader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/infrastructure/loader/appLoader.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import { FunctionComponent } from "react";
 | 
			
		||||
 | 
			
		||||
type Props = {};
 | 
			
		||||
 | 
			
		||||
export const AppLoader: FunctionComponent<Props> = () => {
 | 
			
		||||
  return <h1>Loading app...</h1>;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,13 +1,16 @@
 | 
			
		||||
import { render, screen } from "@testing-library/react";
 | 
			
		||||
import { WithTestTranslations } from "../../app/tests/mocks/i18n/WithTestTranslations";
 | 
			
		||||
import { WithRouter } from "../wrappers/WithRouter";
 | 
			
		||||
import { Navbar } from "./Navbar";
 | 
			
		||||
 | 
			
		||||
describe("Navbar container", () => {
 | 
			
		||||
  it("renders a navigation section identified by the nav test-id", () => {
 | 
			
		||||
  it.only("renders a navigation section identified by the nav test-id", () => {
 | 
			
		||||
    render(
 | 
			
		||||
      <WithRouter>
 | 
			
		||||
        <Navbar />
 | 
			
		||||
      </WithRouter>,
 | 
			
		||||
      <WithTestTranslations>
 | 
			
		||||
        <WithRouter>
 | 
			
		||||
          <Navbar />
 | 
			
		||||
        </WithRouter>
 | 
			
		||||
      </WithTestTranslations>,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(screen.getAllByTestId("nav")?.length).toBeGreaterThan(0);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,36 @@
 | 
			
		||||
import { DateTime } from "luxon";
 | 
			
		||||
import { FunctionComponent } from "react";
 | 
			
		||||
import { Trans, useTranslation } from "react-i18next";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
import { Config } from "../config";
 | 
			
		||||
import "./Navbar.css";
 | 
			
		||||
type Props = {};
 | 
			
		||||
 | 
			
		||||
export const Navbar: FunctionComponent<Props> = () => {
 | 
			
		||||
  const [translate, i18n] = useTranslation();
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <h1>Our fancy header with navigation.</h1>
 | 
			
		||||
      <p>App version: {JSON.stringify(Config.version)}</p>
 | 
			
		||||
      <h1>{translate("navBar.intro")}</h1>
 | 
			
		||||
      <p>
 | 
			
		||||
        {/* trans can also be used to translate */}
 | 
			
		||||
        <Trans i18nKey="navBar.version">App version:</Trans>
 | 
			
		||||
        {JSON.stringify(Config.version)}
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
      {/* This translation uses a formatter in the translation files  */}
 | 
			
		||||
      <p>{translate("navBar.currentDate", { date: DateTime.now().toJSDate() })}</p>
 | 
			
		||||
      <nav data-testid="nav">
 | 
			
		||||
        <Link to="/" data-testid="nav.home">
 | 
			
		||||
          Home
 | 
			
		||||
          {translate("nav.home")}
 | 
			
		||||
        </Link>
 | 
			
		||||
        <Link to="/about" data-testid="nav.about">
 | 
			
		||||
          About
 | 
			
		||||
          {translate("nav.about")}
 | 
			
		||||
        </Link>
 | 
			
		||||
        <Link to="/counter" data-testid="nav.counter">
 | 
			
		||||
          Counter
 | 
			
		||||
          {translate("nav.counter")}
 | 
			
		||||
        </Link>
 | 
			
		||||
        <button onClick={() => i18n.changeLanguage("en")}>en</button>
 | 
			
		||||
        <button onClick={() => i18n.changeLanguage("nl")}>nl</button>
 | 
			
		||||
        <hr />
 | 
			
		||||
      </nav>
 | 
			
		||||
    </>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								src/infrastructure/wrappers/WithPageSuspense.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/infrastructure/wrappers/WithPageSuspense.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
import { FunctionComponent, ReactNode, Suspense } from "react";
 | 
			
		||||
import { AppLoader } from "../loader/appLoader";
 | 
			
		||||
 | 
			
		||||
type Props = { children?: ReactNode };
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component which wraps children in a fallback loader
 | 
			
		||||
 */
 | 
			
		||||
export const Loader: FunctionComponent<Props> = ({ children }) => {
 | 
			
		||||
  return <Suspense fallback={<AppLoader />}>{children}</Suspense>;
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user