mirror of
https://github.com/Mastermindzh/react-starter-kit.git
synced 2025-01-20 18:41:41 +01:00
Merge pull request #1 from Inforitnl/master
i18n, more test config, etc
This commit is contained in:
commit
5856a761cd
1
.gitignore
vendored
1
.gitignore
vendored
@ -69,3 +69,4 @@ bundle.zip
|
|||||||
|
|
||||||
cypress/videos
|
cypress/videos
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
dist-tests/**
|
||||||
|
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@ -4,6 +4,8 @@
|
|||||||
"camelcase",
|
"camelcase",
|
||||||
"flexbugs",
|
"flexbugs",
|
||||||
"Immer",
|
"Immer",
|
||||||
|
"languagedetector",
|
||||||
|
"luxon",
|
||||||
"pmmmwh",
|
"pmmmwh",
|
||||||
"reduxjs",
|
"reduxjs",
|
||||||
"SVGR",
|
"SVGR",
|
||||||
@ -12,6 +14,16 @@
|
|||||||
"typeahead",
|
"typeahead",
|
||||||
"uncompiled"
|
"uncompiled"
|
||||||
],
|
],
|
||||||
|
"cSpell.ignorePaths": [
|
||||||
|
"package-lock.json",
|
||||||
|
"node_modules",
|
||||||
|
"vscode-extension",
|
||||||
|
".git/objects",
|
||||||
|
".vscode",
|
||||||
|
".vscode-insiders",
|
||||||
|
"public/i18n/*",
|
||||||
|
"!public/i18n/en.json"
|
||||||
|
],
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/.git": true,
|
"**/.git": true,
|
||||||
"coverage": true,
|
"coverage": true,
|
||||||
|
16
CHANGELOG.md
16
CHANGELOG.md
@ -4,6 +4,22 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.3.1] - 2022-07-19
|
||||||
|
|
||||||
|
- Added npm run commands to support inforitnl/front-end-build
|
||||||
|
- Fixed e2e test which targeted the wrong word.
|
||||||
|
- Updated minor/patch versions of npm packages
|
||||||
|
- Added junit output for test runners
|
||||||
|
|
||||||
|
## [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
|
## [0.2.0] - 2022-06-27
|
||||||
|
|
||||||
- Added [cypress.io](https://www.cypress.io/)
|
- Added [cypress.io](https://www.cypress.io/)
|
||||||
|
@ -7,4 +7,10 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
video: false,
|
video: false,
|
||||||
|
reporter: "mocha-junit-reporter",
|
||||||
|
reporterOptions: {
|
||||||
|
testsuitesTitle: true,
|
||||||
|
mochaFile: "dist-tests/test-results/cypress/[hash].xml",
|
||||||
|
outputs: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,6 @@ describe("Application navigation", () => {
|
|||||||
|
|
||||||
it("Should navigate to about when clicking on About", () => {
|
it("Should navigate to about when clicking on About", () => {
|
||||||
cy.get('[data-testid="nav.about"]').click();
|
cy.get('[data-testid="nav.about"]').click();
|
||||||
cy.contains("about");
|
cy.contains("About");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,9 +3,24 @@
|
|||||||
const config = {
|
const config = {
|
||||||
roots: ["<rootDir>/src"],
|
roots: ["<rootDir>/src"],
|
||||||
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
|
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
|
||||||
|
reporters: [
|
||||||
|
[
|
||||||
|
"jest-junit",
|
||||||
|
{
|
||||||
|
outputDirectory: "dist-tests/test-results/jest",
|
||||||
|
outputName: "jest.xml",
|
||||||
|
includeShortConsoleOutput: true,
|
||||||
|
classNameTemplate: "{classname}-{title}",
|
||||||
|
titleTemplate: "{classname}-{title}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
setupFiles: ["react-app-polyfill/jsdom"],
|
setupFiles: ["react-app-polyfill/jsdom"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
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",
|
testEnvironment: "jsdom",
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
|
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
|
||||||
@ -21,7 +36,18 @@ const config = {
|
|||||||
"^react-native$": "react-native-web",
|
"^react-native$": "react-native-web",
|
||||||
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
|
"^.+\\.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"],
|
watchPlugins: ["jest-watch-typeahead/filename", "jest-watch-typeahead/testname"],
|
||||||
resetMocks: true,
|
resetMocks: true,
|
||||||
coveragePathIgnorePatterns: [
|
coveragePathIgnorePatterns: [
|
||||||
|
7174
package-lock.json
generated
7174
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@ -25,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build.js",
|
"build": "node scripts/build.js",
|
||||||
|
"build:prod": "npm run build",
|
||||||
"e2e": "cypress open -d --e2e",
|
"e2e": "cypress open -d --e2e",
|
||||||
"e2e-ci": "cypress run",
|
"e2e-ci": "cypress run",
|
||||||
"postinstall": "husky install",
|
"postinstall": "husky install",
|
||||||
@ -34,47 +35,51 @@
|
|||||||
"pretty-quick": "pretty-quick --staged",
|
"pretty-quick": "pretty-quick --staged",
|
||||||
"start": "node scripts/start.js",
|
"start": "node scripts/start.js",
|
||||||
"test": "node scripts/test.js",
|
"test": "node scripts/test.js",
|
||||||
"test-ci": "node scripts/test.js --ci",
|
"test-ci": "node scripts/test.js --ci --coverage",
|
||||||
"test-live-coverage": " concurrently --kill-others \"npm run test-with-coverage\" \"npx http-server -c-1 coverage/lcov-report\"",
|
"test-live-coverage": " concurrently --kill-others \"npm run test-with-coverage\" \"npx http-server -c-1 coverage/lcov-report\"",
|
||||||
"test-with-coverage": "node scripts/test.js --coverage"
|
"test-with-coverage": "node scripts/test.js --coverage",
|
||||||
|
"test:prod": "npm run test-ci && npm run e2e-ci"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,js,jsx,tsx,css,scss,json,md}": "prettier --write"
|
"*.{ts,js,jsx,tsx,css,scss,json,md}": "prettier --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.2",
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
|
"i18next-http-backend": "^1.4.1",
|
||||||
|
"luxon": "^2.4.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-app-polyfill": "^3.0.0",
|
"react-app-polyfill": "^3.0.0",
|
||||||
"react-dev-utils": "^12.0.1",
|
"react-dev-utils": "^12.0.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"tailwindcss": "^3.0.2"
|
"tailwindcss": "^3.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.0",
|
"@babel/core": "^7.18.6",
|
||||||
"@mastermindzh/prettier-config": "^1.0.0",
|
"@mastermindzh/prettier-config": "^1.0.0",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
||||||
"@svgr/webpack": "^5.5.0",
|
"@svgr/webpack": "^5.5.0",
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^13.3.0",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/user-event": "^14.2.1",
|
"@testing-library/user-event": "^14.2.5",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^28.1.6",
|
||||||
"@types/node": "^17.0.45",
|
"@types/luxon": "^2.3.2",
|
||||||
"@types/react": "^18.0.14",
|
"@types/node": "^18.0.6",
|
||||||
"@types/react-dom": "^18.0.5",
|
"@types/react": "^18.0.15",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
"babel-jest": "^27.4.2",
|
"babel-jest": "^27.4.2",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.5",
|
||||||
"babel-plugin-named-asset-import": "^0.3.8",
|
"babel-plugin-named-asset-import": "^0.3.8",
|
||||||
"babel-preset-react-app": "^10.0.1",
|
"babel-preset-react-app": "^10.0.1",
|
||||||
"bfj": "^7.0.2",
|
"bfj": "^7.0.2",
|
||||||
"browserslist": "^4.18.1",
|
"browserslist": "^4.21.2",
|
||||||
"camelcase": "^6.2.1",
|
"camelcase": "^6.2.1",
|
||||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||||
"concurrently": "^7.2.2",
|
"concurrently": "^7.2.2",
|
||||||
"css-loader": "^6.5.1",
|
"css-loader": "^6.7.1",
|
||||||
"css-minimizer-webpack-plugin": "^3.2.0",
|
"css-minimizer-webpack-plugin": "^3.2.0",
|
||||||
"cypress": "^10.2.0",
|
"cypress": "^10.3.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"dotenv-expand": "^5.1.0",
|
"dotenv-expand": "^5.1.0",
|
||||||
"eslint": "^8.3.0",
|
"eslint": "^8.3.0",
|
||||||
@ -84,39 +89,44 @@
|
|||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-react": "^7.30.1",
|
"eslint-plugin-react": "^7.30.1",
|
||||||
"eslint-watch": "^8.0.0",
|
"eslint-watch": "^8.0.0",
|
||||||
"eslint-webpack-plugin": "^3.1.1",
|
"eslint-webpack-plugin": "^3.2.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.1.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"husky": "^8.0.1",
|
"husky": "^8.0.1",
|
||||||
|
"i18next": "^21.8.14",
|
||||||
|
"i18next-browser-languagedetector": "^6.1.4",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
"jest": "^27.4.3",
|
"jest": "^27.4.3",
|
||||||
|
"jest-junit": "^14.0.0",
|
||||||
"jest-resolve": "^27.4.2",
|
"jest-resolve": "^27.4.2",
|
||||||
"jest-watch-typeahead": "^1.0.0",
|
"jest-watch-typeahead": "^1.0.0",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"mini-css-extract-plugin": "^2.4.5",
|
"mini-css-extract-plugin": "^2.6.1",
|
||||||
"postcss": "^8.4.4",
|
"mocha-junit-reporter": "^2.0.2",
|
||||||
|
"postcss": "^8.4.14",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
"postcss-loader": "^6.2.1",
|
"postcss-loader": "^6.2.1",
|
||||||
"postcss-normalize": "^10.0.1",
|
"postcss-normalize": "^10.0.1",
|
||||||
"postcss-preset-env": "^7.0.1",
|
"postcss-preset-env": "^7.7.2",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"pretty-quick": "^3.1.3",
|
"pretty-quick": "^3.1.3",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
"react-refresh": "^0.11.0",
|
"react-i18next": "^11.18.1",
|
||||||
"resolve": "^1.20.0",
|
"react-refresh": "^0.14.0",
|
||||||
|
"resolve": "^1.22.1",
|
||||||
"resolve-url-loader": "^4.0.0",
|
"resolve-url-loader": "^4.0.0",
|
||||||
"sass-loader": "^12.3.0",
|
"sass-loader": "^12.3.0",
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.7",
|
||||||
"source-map-loader": "^3.0.0",
|
"source-map-loader": "^3.0.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"terser-webpack-plugin": "^5.2.5",
|
"terser-webpack-plugin": "^5.3.3",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"webpack": "^5.64.4",
|
"webpack": "^5.73.0",
|
||||||
"webpack-dev-server": "^4.6.0",
|
"webpack-dev-server": "^4.9.3",
|
||||||
"webpack-manifest-plugin": "^4.0.2",
|
"webpack-manifest-plugin": "^4.0.2",
|
||||||
"workbox-webpack-plugin": "^6.4.1"
|
"workbox-webpack-plugin": "^6.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 { render, screen } from "@testing-library/react";
|
||||||
|
import { WithTestTranslations } from "../../app/tests/mocks/i18n/WithTestTranslations";
|
||||||
|
|
||||||
import { AboutContainer } from "./About";
|
import { AboutContainer } from "./About";
|
||||||
|
|
||||||
describe("About container", () => {
|
describe("About container", () => {
|
||||||
it("renders welcome to the about page", () => {
|
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 { FunctionComponent } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Props = {};
|
type Props = {};
|
||||||
|
|
||||||
export const AboutContainer: FunctionComponent<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 { useState } from "react";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import styles from "./Counter.module.css";
|
import styles from "./Counter.module.css";
|
||||||
import { incrementAsync } from "./state/actions/incrementAsync";
|
import { incrementAsync } from "./state/actions/incrementAsync";
|
||||||
@ -15,12 +16,13 @@ export function CounterContainer() {
|
|||||||
const { value, status } = useAppSelector(selectCountAndStatus);
|
const { value, status } = useAppSelector(selectCountAndStatus);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [incrementAmount, setIncrementAmount] = useState("2");
|
const [incrementAmount, setIncrementAmount] = useState("2");
|
||||||
|
const [translate] = useTranslation();
|
||||||
|
|
||||||
const incrementValue = Number(incrementAmount) || 0;
|
const incrementValue = Number(incrementAmount) || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Status: {status}
|
{translate("counter.status", { status })}
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
<button
|
<button
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
@ -49,7 +51,10 @@ export function CounterContainer() {
|
|||||||
className={styles.button}
|
className={styles.button}
|
||||||
onClick={() => dispatch(incrementByAmount(incrementValue))}
|
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>
|
||||||
<button
|
<button
|
||||||
className={styles.asyncButton}
|
className={styles.asyncButton}
|
||||||
@ -57,10 +62,7 @@ export function CounterContainer() {
|
|||||||
>
|
>
|
||||||
Add Async
|
Add Async
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className={styles.button} onClick={() => dispatch(incrementIfOdd(incrementValue))}>
|
||||||
className={styles.button}
|
|
||||||
onClick={() => dispatch(incrementIfOdd(incrementValue))}
|
|
||||||
>
|
|
||||||
Add If Odd
|
Add If Odd
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,8 @@ import { Provider } from "react-redux";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { store } from "./app/store";
|
import { store } from "./app/store";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import "./infrastructure/i18n/init";
|
||||||
|
import { Loader } from "./infrastructure/wrappers/WithPageSuspense";
|
||||||
import reportWebVitals from "./reportWebVitals";
|
import reportWebVitals from "./reportWebVitals";
|
||||||
|
|
||||||
const container = document.getElementById("root")!;
|
const container = document.getElementById("root")!;
|
||||||
@ -12,7 +14,9 @@ const root = createRoot(container);
|
|||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<Loader>
|
||||||
<App />
|
<App />
|
||||||
|
</Loader>
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>,
|
</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 { render, screen } from "@testing-library/react";
|
||||||
|
import { WithTestTranslations } from "../../app/tests/mocks/i18n/WithTestTranslations";
|
||||||
import { WithRouter } from "../wrappers/WithRouter";
|
import { WithRouter } from "../wrappers/WithRouter";
|
||||||
import { Navbar } from "./Navbar";
|
import { Navbar } from "./Navbar";
|
||||||
|
|
||||||
describe("Navbar container", () => {
|
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(
|
render(
|
||||||
|
<WithTestTranslations>
|
||||||
<WithRouter>
|
<WithRouter>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</WithRouter>,
|
</WithRouter>
|
||||||
|
</WithTestTranslations>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getAllByTestId("nav")?.length).toBeGreaterThan(0);
|
expect(screen.getAllByTestId("nav")?.length).toBeGreaterThan(0);
|
||||||
|
@ -1,24 +1,36 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Config } from "../config";
|
import { Config } from "../config";
|
||||||
import "./Navbar.css";
|
import "./Navbar.css";
|
||||||
type Props = {};
|
type Props = {};
|
||||||
|
|
||||||
export const Navbar: FunctionComponent<Props> = () => {
|
export const Navbar: FunctionComponent<Props> = () => {
|
||||||
|
const [translate, i18n] = useTranslation();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Our fancy header with navigation.</h1>
|
<h1>{translate("navBar.intro")}</h1>
|
||||||
<p>App version: {JSON.stringify(Config.version)}</p>
|
<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">
|
<nav data-testid="nav">
|
||||||
<Link to="/" data-testid="nav.home">
|
<Link to="/" data-testid="nav.home">
|
||||||
Home
|
{translate("nav.home")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/about" data-testid="nav.about">
|
<Link to="/about" data-testid="nav.about">
|
||||||
About
|
{translate("nav.about")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/counter" data-testid="nav.counter">
|
<Link to="/counter" data-testid="nav.counter">
|
||||||
Counter
|
{translate("nav.counter")}
|
||||||
</Link>
|
</Link>
|
||||||
|
<button onClick={() => i18n.changeLanguage("en")}>en</button>
|
||||||
|
<button onClick={() => i18n.changeLanguage("nl")}>nl</button>
|
||||||
<hr />
|
<hr />
|
||||||
</nav>
|
</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>;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user