- 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:
Rick van Lieshout 2022-07-08 15:05:05 +02:00
parent 7d9c7037bc
commit 0003df5fab
19 changed files with 527 additions and 21 deletions

12
.vscode/settings.json vendored
View File

@ -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,

View File

@ -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/)

View File

@ -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
View File

@ -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",

View File

@ -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
View 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
View 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"
}
}

View 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>
);
};

View 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;

View File

@ -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();
});
});

View File

@ -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>;
};

View File

@ -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>

View File

@ -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>,
);

View 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);
};

View 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;

View File

@ -0,0 +1,7 @@
import { FunctionComponent } from "react";
type Props = {};
export const AppLoader: FunctionComponent<Props> = () => {
return <h1>Loading app...</h1>;
};

View File

@ -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);

View File

@ -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>
</>

View 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>;
};