diff --git a/.nvmrc b/.nvmrc index 93a75dd..238155b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.14.0 \ No newline at end of file +v20.12.2 \ No newline at end of file diff --git a/README.md b/README.md index f6c6a77..ec81249 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ This project's API integration uses the simulated REST endpoints made available When running the application, you may sign in with any of the JSON Placeholder [Users](https://jsonplaceholder.typicode.com/users). Simply enter the _Username_ value from any user in the API and use any value for the _Password_. For example, try username `Bret` and password `abc123`. -### Diagnostics +### Easter Eggs + +#### Diagnostics Many applications, particularly mobile applications, have a hidden page which displays content useful for troubleshooting and support. To access the diagnostics page, go to the _Account_ page. Locate the _About_ section and click or tap the _Version_ item 7 times. @@ -38,9 +40,11 @@ The technology stack includes: - Yup - validation - Lodash - utility functions - DayJS - date utility functions -- Testing Library React - tests -- Vitest - tests +- i18next - internationalization framework +- Testing Library React - unit tests +- Vitest - unit tests - MSW - API mocking +- Cypress - end-to-end tests - TypeScript ### Repository diff --git a/package-lock.json b/package-lock.json index 1b61721..e2f8f2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,15 +19,18 @@ "@fortawesome/react-fontawesome": "0.2.2", "@ionic/react": "8.3.2", "@ionic/react-router": "8.3.2", - "@tanstack/react-query": "5.59.0", - "@tanstack/react-query-devtools": "5.59.0", + "@tanstack/react-query": "5.59.9", + "@tanstack/react-query-devtools": "5.59.9", "axios": "1.7.7", "classnames": "2.5.1", "dayjs": "1.11.13", "formik": "2.4.6", + "i18next": "23.15.2", + "i18next-browser-languagedetector": "8.0.0", "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", + "react-i18next": "15.0.2", "react-router": "5.3.4", "react-router-dom": "5.3.4", "uuid": "10.0.0", @@ -41,23 +44,23 @@ "@testing-library/user-event": "14.5.2", "@types/lodash": "4.17.10", "@types/react": "18.3.11", - "@types/react-dom": "18.3.0", + "@types/react-dom": "18.3.1", "@types/react-router": "5.1.20", "@types/react-router-dom": "5.3.3", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.8.0", - "@typescript-eslint/parser": "8.8.0", + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", "@vitejs/plugin-legacy": "5.4.2", "@vitejs/plugin-react": "4.3.2", "@vitest/coverage-v8": "2.1.2", "cypress": "13.15.0", "eslint": "8.57.0", "eslint-plugin-react": "7.37.1", - "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-hooks": "5.0.0", "eslint-plugin-react-refresh": "0.4.12", "jsdom": "25.0.1", - "msw": "2.4.9", - "sass": "1.79.4", + "msw": "2.4.10", + "sass": "1.79.5", "terser": "5.34.1", "typescript": "5.5.4", "vite": "5.4.8", @@ -1784,9 +1787,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3215,6 +3218,279 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3446,9 +3722,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.0.tgz", - "integrity": "sha512-WGD8uIhX6/deH/tkZqPNcRyAhDUqs729bWKoByYHSogcshXfFbppOdTER5+qY7mFvu8KEFJwT0nxr8RfPTVh0Q==", + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.9.tgz", + "integrity": "sha512-vFGnblfJOKlOPyTR5M0ohWKb/03eGubh5KuGyzsDfc7VQ6F0nsB75kQIoLpwp3Wfj6fKv0wGoTUX8BsIfhxDfw==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -3464,11 +3740,11 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.0.tgz", - "integrity": "sha512-YDXp3OORbYR+8HNQx+lf4F73NoiCmCcSvZvgxE29OifmQFk0sBlO26NWLHpcNERo92tVk3w+JQ53/vkcRUY1hA==", + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.9.tgz", + "integrity": "sha512-g2cbiw/ZIIrnUaQqhGtarTAsuLdKDNLtY5HNfRHVWY9kHDj96M4qs4ygJxHc119tPQpzZe4i9W7d2Gc2Gvng2A==", "dependencies": { - "@tanstack/query-core": "5.59.0" + "@tanstack/query-core": "5.59.9" }, "funding": { "type": "github", @@ -3479,9 +3755,9 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.59.0.tgz", - "integrity": "sha512-Kz7577FQGU8qmJxROIT/aOwmkTcxfBqgTP6r1AIvuJxVMVHPkp8eQxWQ7BnfBsy/KTJHiV9vMtRVo1+R1tB3vg==", + "version": "5.59.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.59.9.tgz", + "integrity": "sha512-Vfr8JPgx4GxopQOqdQTC7MAUbX1vuEqeexCIX0RiwjUmNCoHKUg2Mh3rTZPsx8Y7wscc7eWkBjiz03Dt/YM3oQ==", "dependencies": { "@tanstack/query-devtools": "5.58.0" }, @@ -3490,7 +3766,7 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.59.0", + "@tanstack/react-query": "^5.59.9", "react": "^18 || ^19" } }, @@ -3714,9 +3990,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, "dependencies": { "@types/react": "*" @@ -3796,16 +4072,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", - "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", + "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/type-utils": "8.8.0", - "@typescript-eslint/utils": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/type-utils": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3829,15 +4105,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", - "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", + "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4" }, "engines": { @@ -3857,13 +4133,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", - "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0" + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3874,13 +4150,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", - "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", + "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/utils": "8.8.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3898,9 +4174,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", - "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3911,13 +4187,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", - "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3975,15 +4251,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", - "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0" + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3997,12 +4273,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", - "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/types": "8.8.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -5471,6 +5747,18 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -5877,15 +6165,15 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz", + "integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==", "dev": true, "engines": { "node": ">=10" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react-refresh": { @@ -6809,6 +7097,14 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "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==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -6858,6 +7154,36 @@ "node": ">=8.12.0" } }, + "node_modules/i18next": { + "version": "23.15.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.15.2.tgz", + "integrity": "sha512-zcPSWzCvw6uKnuYHIqs4W7hTuB9e3AFcSdZgvCWoPXIZsBjBd4djN2/2uOHIB+1DFFkQnMBXvhNg7J3WyCuywQ==", + "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.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -8125,9 +8451,9 @@ "dev": true }, "node_modules/msw": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.4.9.tgz", - "integrity": "sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==", + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.4.10.tgz", + "integrity": "sha512-bDQh9b25JK4IKMs5hnamwAkcNZ9RwA4mR/4YcgWkzwHOxj7UICbVJfmChJvY1UCAAMraPpvjHdxjoUDpc3F+Qw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -8252,6 +8578,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -8886,6 +9218,27 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" }, + "node_modules/react-i18next": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.2.tgz", + "integrity": "sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "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", @@ -9332,11 +9685,12 @@ "dev": true }, "node_modules/sass": { - "version": "1.79.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz", - "integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "dev": true, "dependencies": { + "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" @@ -10542,6 +10896,14 @@ } } }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index 8e3fec1..740cf95 100644 --- a/package.json +++ b/package.json @@ -29,15 +29,18 @@ "@fortawesome/react-fontawesome": "0.2.2", "@ionic/react": "8.3.2", "@ionic/react-router": "8.3.2", - "@tanstack/react-query": "5.59.0", - "@tanstack/react-query-devtools": "5.59.0", + "@tanstack/react-query": "5.59.9", + "@tanstack/react-query-devtools": "5.59.9", "axios": "1.7.7", "classnames": "2.5.1", "dayjs": "1.11.13", "formik": "2.4.6", + "i18next": "23.15.2", + "i18next-browser-languagedetector": "8.0.0", "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", + "react-i18next": "15.0.2", "react-router": "5.3.4", "react-router-dom": "5.3.4", "uuid": "10.0.0", @@ -51,23 +54,23 @@ "@testing-library/user-event": "14.5.2", "@types/lodash": "4.17.10", "@types/react": "18.3.11", - "@types/react-dom": "18.3.0", + "@types/react-dom": "18.3.1", "@types/react-router": "5.1.20", "@types/react-router-dom": "5.3.3", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.8.0", - "@typescript-eslint/parser": "8.8.0", + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", "@vitejs/plugin-legacy": "5.4.2", "@vitejs/plugin-react": "4.3.2", "@vitest/coverage-v8": "2.1.2", "cypress": "13.15.0", "eslint": "8.57.0", "eslint-plugin-react": "7.37.1", - "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-hooks": "5.0.0", "eslint-plugin-react-refresh": "0.4.12", "jsdom": "25.0.1", - "msw": "2.4.9", - "sass": "1.79.4", + "msw": "2.4.10", + "sass": "1.79.5", "terser": "5.34.1", "typescript": "5.5.4", "vite": "5.4.8", diff --git a/src/common/components/Card/EmptyCard.tsx b/src/common/components/Card/EmptyCard.tsx index c58efc5..e320f24 100644 --- a/src/common/components/Card/EmptyCard.tsx +++ b/src/common/components/Card/EmptyCard.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from 'react-i18next'; + import MessageCard, { MessageCardProps } from './MessageCard'; import { IconName } from '../Icon/Icon'; @@ -17,9 +19,12 @@ interface EmptyCardProps extends MessageCardProps {} const EmptyCard = ({ icon = IconName.CircleInfo, testid = 'card-empty', - title = 'No data', + title, ...cardProps }: EmptyCardProps): JSX.Element => { + const { t } = useTranslation(); + title ??= t('error-no-data'); + return ; }; diff --git a/src/common/components/Card/ErrorCard.tsx b/src/common/components/Card/ErrorCard.tsx index 4811cbc..75ff4d2 100644 --- a/src/common/components/Card/ErrorCard.tsx +++ b/src/common/components/Card/ErrorCard.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from 'react-i18next'; + import MessageCard, { MessageCardProps } from './MessageCard'; import { IconName } from '../Icon/Icon'; @@ -17,9 +19,12 @@ const ErrorCard = ({ color = 'danger', icon = IconName.TriangleExclamation, testid = 'card-error', - title = 'Uh oh', + title, ...cardProps }: ErrorCardProps): JSX.Element => { + const { t } = useTranslation(); + title ??= t('error-generic'); + return ; }; diff --git a/src/common/components/Header/Header.tsx b/src/common/components/Header/Header.tsx index 004313f..718f1a2 100644 --- a/src/common/components/Header/Header.tsx +++ b/src/common/components/Header/Header.tsx @@ -9,6 +9,7 @@ import { IonTitle, IonToolbar, } from '@ionic/react'; +import { useTranslation } from 'react-i18next'; import './Header.scss'; import { PropsWithTestId } from '../types'; @@ -50,6 +51,7 @@ const Header = ({ }: HeaderProps): JSX.Element => { const { isAuthenticated } = useAuth(); const { isActive: isActiveProgressBar, progressBar } = useProgress(); + const { t } = useTranslation(); return ( @@ -75,8 +77,8 @@ const Header = ({ > {isAuthenticated && ( <> - Home - Users + {t('navigation.home')} + {t('navigation.users')} )} diff --git a/src/common/components/Menu/AppMenu.tsx b/src/common/components/Menu/AppMenu.tsx index ebec716..79d6f20 100644 --- a/src/common/components/Menu/AppMenu.tsx +++ b/src/common/components/Menu/AppMenu.tsx @@ -10,6 +10,7 @@ import { IonToolbar, } from '@ionic/react'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import './AppMenu.scss'; import { BaseComponentProps } from '../types'; @@ -33,6 +34,7 @@ interface AppMenuProps extends BaseComponentProps {} const AppMenu = ({ className, testid = 'menu-app' }: AppMenuProps): JSX.Element => { const { isAuthenticated } = useAuth(); const { data: currentUser } = useGetCurrentUser(); + const { t } = useTranslation(); const showUserHeader = isAuthenticated && !!currentUser; @@ -64,13 +66,13 @@ const AppMenu = ({ className, testid = 'menu-app' }: AppMenuProps): JSX.Element - Home + {t('navigation.home')} - Users + {t('navigation.users')} @@ -80,7 +82,7 @@ const AppMenu = ({ className, testid = 'menu-app' }: AppMenuProps): JSX.Element data-testid={`${testid}-item-account`} > - Account + {t('navigation.account')} @@ -90,7 +92,7 @@ const AppMenu = ({ className, testid = 'menu-app' }: AppMenuProps): JSX.Element data-testid={`${testid}-item-signout`} > - Sign Out + {t('navigation.signout')} diff --git a/src/common/components/Router/TabNavigation.tsx b/src/common/components/Router/TabNavigation.tsx index c5480f0..0669350 100644 --- a/src/common/components/Router/TabNavigation.tsx +++ b/src/common/components/Router/TabNavigation.tsx @@ -1,5 +1,6 @@ import { IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; import { Redirect, Route } from 'react-router'; +import { useTranslation } from 'react-i18next'; import './TabNavigation.scss'; import AppMenu from '../Menu/AppMenu'; @@ -28,6 +29,8 @@ import DiagnosticsPage from 'pages/Account/components/Diagnostics/DiagnosticsPag * @see {@link AppMenu} */ const TabNavigation = (): JSX.Element => { + const { t } = useTranslation(); + return ( <> @@ -69,7 +72,7 @@ const TabNavigation = (): JSX.Element => { size="xl" fixedWidth /> - Home + {t('navigation.home')} { size="xl" fixedWidth /> - Users + {t('navigation.users')} { size="xl" fixedWidth /> - Account + {t('navigation.account')} diff --git a/src/common/components/Toast/Toast.tsx b/src/common/components/Toast/Toast.tsx index 556e1a2..07dd80d 100644 --- a/src/common/components/Toast/Toast.tsx +++ b/src/common/components/Toast/Toast.tsx @@ -1,6 +1,7 @@ import { IonToast, ToastButton } from '@ionic/react'; import { useState } from 'react'; import classNames from 'classnames'; +import { t } from 'i18next'; import { BaseComponentProps } from '../types'; import { ToastData } from 'common/providers/ToastProvider'; @@ -17,12 +18,13 @@ interface ToastProps extends BaseComponentProps { } /** - * A `ToastButton` to dismiss a toast. + * Creates a standardized `ToastButton` to dismiss a `Toast`. + * @returns {ToastButton} A `ToastButton`. */ -export const DismissButton: ToastButton = { +export const DismissButton = (): ToastButton => ({ role: 'cancel', - text: 'Dismiss', -}; + text: t('label.dismiss'), +}); /** * The `Toast` component renders an `IonToast` using the supplied `toast` diff --git a/src/common/utils/constants.ts b/src/common/utils/constants.ts index c2c6a68..4b7b29f 100644 --- a/src/common/utils/constants.ts +++ b/src/common/utils/constants.ts @@ -15,6 +15,7 @@ export enum QueryKey { * Local storage keys. */ export enum StorageKey { + Language = 'ionic-playground.language', RememberMe = 'ionic-playground.remember-me', Settings = 'ionic-playground.settings', UserProfile = 'ionic-playground.user-profile', diff --git a/src/common/utils/i18n/index.ts b/src/common/utils/i18n/index.ts new file mode 100644 index 0000000..3ad3527 --- /dev/null +++ b/src/common/utils/i18n/index.ts @@ -0,0 +1,37 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import { StorageKey } from '../constants'; + +// translation resources +import en from './resources/en'; +import es from './resources/es'; +import fr from './resources/fr'; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + // logging + debug: import.meta.env.DEV, + + // languages, namespaces, and resources + supportedLngs: ['en', 'es', 'fr'], + fallbackLng: 'en', + ns: ['account', 'auth', 'common', 'home', 'user'], + defaultNS: 'common', + resources: { en, es, fr }, + + // translation defaults + interpolation: { + escapeValue: false, + }, + + // plugin - language detector + detection: { + lookupLocalStorage: StorageKey.Language, + }, + }); + +export default i18n; diff --git a/src/common/utils/i18n/resources/en/account.json b/src/common/utils/i18n/resources/en/account.json new file mode 100644 index 0000000..41a1390 --- /dev/null +++ b/src/common/utils/i18n/resources/en/account.json @@ -0,0 +1,59 @@ +{ + "about": "About", + "diagnostics": { + "app": "App", + "build": "Build", + "diagnostics": "Diagnostics", + "label": { + "build": "Build", + "environment": "Environment", + "id": "ID", + "name": "Name", + "native": "Native", + "platforms": "Platforms", + "sha": "SHA", + "time": "Time", + "version": "Version", + "workflow": "Workflow" + }, + "platform": "Platform", + "platform-not-native": "Information available on mobile devices." + }, + "legal": "Legal", + "my-account": "My Account", + "privacy-policy": "Privacy policy", + "profile": { + "label": { + "bio": "Bio", + "birthday": "Birthday", + "email": "Email", + "name": "Name" + }, + "profile": "Profile", + "unable-to-process": "We are experiencing problems processing your request.", + "unable-to-retrieve": "We are unable to retrieve your profile details at this time." + }, + "settings": { + "font-size": { + "default": "Default", + "larger": "Larger", + "smaller": "Smaller" + }, + "label": { + "brightness": "Brightness", + "font-size": "Font Size", + "language": "Language", + "notifications": "Notifications" + }, + "language": { + "en": "English", + "es": "Spanish", + "fr": "French" + }, + "settings": "Settings", + "unable-to-update": "Unable to update settings.", + "update-success": "Settings updated." + }, + "terms-and-conditions": "Terms and conditions", + "version": "Version" +} diff --git a/src/common/utils/i18n/resources/en/auth.json b/src/common/utils/i18n/resources/en/auth.json new file mode 100644 index 0000000..91fcb08 --- /dev/null +++ b/src/common/utils/i18n/resources/en/auth.json @@ -0,0 +1,18 @@ +{ + "error": { + "unable-to-verify": "We were unable to verify your credentials. Please try again." + }, + "info-username": { + "part1": "This example application uses ", + "part2": "JSONPlaceholder data", + "part3": "Try a username like ", + "part4": " or ", + "part5": "You may use any value as the password." + }, + "label": { + "password": "Password", + "remember-me": "Remember me", + "username": "Username" + }, + "signin": "Sign In" +} diff --git a/src/common/utils/i18n/resources/en/common.json b/src/common/utils/i18n/resources/en/common.json new file mode 100644 index 0000000..567f842 --- /dev/null +++ b/src/common/utils/i18n/resources/en/common.json @@ -0,0 +1,33 @@ +{ + "confirm-prompt": "Are you sure?", + "created": "created", + "deleted": "deleted", + "error-no-data": "No data", + "error-generic": "Uh oh", + "ionic-playground": "Ionic Playground", + "label": { + "cancel": "Cancel", + "delete": "Delete", + "dismiss": "Dismiss", + "save": "Save" + }, + "navigation": { + "account": "Account", + "home": "Home", + "menu": "Menu", + "signout": "Sign Out", + "users": "Users" + }, + "validation": { + "email": "Must be an email address. ", + "max": "Must be at most {{max}} characters. ", + "min": "Must be at least {{min}} characters. ", + "oneOf": "Must be one of: {{values}} ", + "required": "Required. ", + "url": "Must be a URL. " + }, + "no": "no", + "updated": "updated", + "welcome": "Welcome", + "yes": "yes" +} diff --git a/src/common/utils/i18n/resources/en/home.json b/src/common/utils/i18n/resources/en/home.json new file mode 100644 index 0000000..f24a769 --- /dev/null +++ b/src/common/utils/i18n/resources/en/home.json @@ -0,0 +1,10 @@ +{ + "welcome": { + "sentence1": "Welcome to the Ionic playground project.", + "sentence2": { + "1": "This project demonstrates how to create a cross-platform application using the", + "2": "framework and", + "3": "components" + } + } +} diff --git a/src/common/utils/i18n/resources/en/index.ts b/src/common/utils/i18n/resources/en/index.ts new file mode 100644 index 0000000..45cb7e7 --- /dev/null +++ b/src/common/utils/i18n/resources/en/index.ts @@ -0,0 +1,7 @@ +import account from './account.json'; +import auth from './auth.json'; +import common from './common.json'; +import home from './home.json'; +import user from './user.json'; + +export default { account, auth, common, home, user }; diff --git a/src/common/utils/i18n/resources/en/user.json b/src/common/utils/i18n/resources/en/user.json new file mode 100644 index 0000000..c6aa73c --- /dev/null +++ b/src/common/utils/i18n/resources/en/user.json @@ -0,0 +1,26 @@ +{ + "address": "Address", + "add-user": "Add user", + "browse-and-search": "Browse and search all the users. View user profiles and read their posts.", + "company": "Company", + "contact": "Contact", + "delete": { + "deleting": "Deleting...", + "in-progress": "Deleting {{name}} in progress.", + "warning": "Deleting {{name}} is permanent." + }, + "delete-user": "Delete user", + "edit-user": "Edit user", + "label": { + "email": "Email", + "name": "Name", + "phone": "Phone", + "username": "Username", + "website": "Website" + }, + "loading-users": "Loading users", + "tap-to-view": "Tap to view all users.", + "unable-to-find": "We are unable to find information matching your request.", + "unable-to-process": "We are experiencing problems processing your request.", + "unable-to-retrieve": "We are unable to retrieve the requested information at this time." +} diff --git a/src/common/utils/i18n/resources/es/account.json b/src/common/utils/i18n/resources/es/account.json new file mode 100644 index 0000000..9301ec2 --- /dev/null +++ b/src/common/utils/i18n/resources/es/account.json @@ -0,0 +1,59 @@ +{ + "about": "Sobre", + "diagnostics": { + "app": "App", + "build": "Build", + "diagnostics": "Diagnósticos", + "label": { + "build": "Compilación", + "environment": "Ambiente", + "id": "ID", + "name": "Nombre", + "native": "Nativo", + "platforms": "Plataformas", + "sha": "SHA", + "time": "Tiempo", + "version": "Versión", + "workflow": "Flujo de trabajo" + }, + "platform": "Plataforma", + "platform-not-native": "Información disponible en dispositivos móviles." + }, + "legal": "Legal", + "my-account": "Mi Cuenta", + "privacy-policy": "Política de privacidad", + "profile": { + "label": { + "bio": "Biografía", + "birthday": "Cumpleaños", + "email": "Correo electrónico", + "name": "Nombre" + }, + "profile": "Perfil", + "unable-to-process": "Estamos experimentando problemas al procesar su solicitud.", + "unable-to-retrieve": "No podemos recuperar los detalles de su perfil en este momento." + }, + "settings": { + "font-size": { + "default": "Valor predeterminado", + "larger": "Más grande", + "smaller": "Mas pequeño" + }, + "label": { + "brightness": "Brillo", + "font-size": "Tamaño de fuente", + "language": "Idioma", + "notifications": "Notificaciones" + }, + "language": { + "en": "Inglés", + "es": "Español", + "fr": "Francés" + }, + "settings": "Configuraciónes", + "unable-to-update": "No se puede actualizar la configuración.", + "update-success": "Configuración actualizada." + }, + "terms-and-conditions": "Términos y condiciones", + "version": "Versión" +} diff --git a/src/common/utils/i18n/resources/es/auth.json b/src/common/utils/i18n/resources/es/auth.json new file mode 100644 index 0000000..6312698 --- /dev/null +++ b/src/common/utils/i18n/resources/es/auth.json @@ -0,0 +1,18 @@ +{ + "error": { + "unable-to-verify": "No pudimos verificar tus credenciales. Inténtalo nuevamente." + }, + "info-username": { + "part1": "Esta aplicación de ejemplo utiliza ", + "part2": "datos de JSONPlaceholder", + "part3": "Prueba un nombre de usuario como ", + "part4": " o ", + "part5": "Puede utilizar cualquier valor como contraseña." + }, + "label": { + "password": "Contraseña", + "remember-me": "Acuérdate de mí", + "username": "Nombre de usuario" + }, + "signin": "Iniciar sesión" +} diff --git a/src/common/utils/i18n/resources/es/common.json b/src/common/utils/i18n/resources/es/common.json new file mode 100644 index 0000000..68a1300 --- /dev/null +++ b/src/common/utils/i18n/resources/es/common.json @@ -0,0 +1,33 @@ +{ + "confirm-prompt": "Estas seguro?", + "created": "creado", + "deleted": "eliminado", + "error-no-data": "Sin datos", + "error-generic": "Un problema", + "ionic-playground": "Proyecto Ionic", + "label": { + "cancel": "Cancelar", + "delete": "Borrar", + "dismiss": "Despedir", + "save": "Guardar" + }, + "navigation": { + "account": "Cuenta", + "home": "Inicio", + "menu": "Menú", + "signout": "Desconecta", + "users": "Usuarios" + }, + "validation": { + "email": "Debe ser una dirección de correo electrónico. ", + "max": "Debe tener como máximo {{max}} caracteres. ", + "min": "Debe tener al menos {{min}} caracteres. ", + "oneOf": "Debe ser uno de: {{values}} ", + "required": "Requerido. ", + "url": "Debe ser una URL. " + }, + "no": "no", + "updated": "actualizado", + "welcome": "Bienvenido", + "yes": "sí" +} diff --git a/src/common/utils/i18n/resources/es/home.json b/src/common/utils/i18n/resources/es/home.json new file mode 100644 index 0000000..cb1e317 --- /dev/null +++ b/src/common/utils/i18n/resources/es/home.json @@ -0,0 +1,10 @@ +{ + "welcome": { + "sentence1": "Bienvenido al proyecto del experimento Ionic.", + "sentence2": { + "1": "Este proyecto demuestra cómo crear una aplicación multiplataforma utilizando el", + "2": "marco lógico y", + "3": "componentes" + } + } +} diff --git a/src/common/utils/i18n/resources/es/index.ts b/src/common/utils/i18n/resources/es/index.ts new file mode 100644 index 0000000..45cb7e7 --- /dev/null +++ b/src/common/utils/i18n/resources/es/index.ts @@ -0,0 +1,7 @@ +import account from './account.json'; +import auth from './auth.json'; +import common from './common.json'; +import home from './home.json'; +import user from './user.json'; + +export default { account, auth, common, home, user }; diff --git a/src/common/utils/i18n/resources/es/user.json b/src/common/utils/i18n/resources/es/user.json new file mode 100644 index 0000000..0488406 --- /dev/null +++ b/src/common/utils/i18n/resources/es/user.json @@ -0,0 +1,26 @@ +{ + "address": "Dirección", + "add-user": "Agregar usuario", + "browse-and-search": "Explora y busca entre todos los usuarios. Ve los perfiles de los usuarios y lee sus publicaciones.", + "company": "Compañía", + "contact": "Contacto", + "delete": { + "deleting": "Borrando...", + "in-progress": "Eliminando {{name}} en proceso.", + "warning": "Eliminar {{name}} es permanente." + }, + "delete-user": "Eliminar usuario", + "edit-user": "Editar usuario", + "label": { + "email": "Dirección de correo electrónico", + "name": "Nombre", + "phone": "Número de teléfono", + "username": "Nombre de usuario", + "website": "Sitio web" + }, + "loading-users": "Cargando usuarios", + "tap-to-view": "Toque para ver todos los usuarios.", + "unable-to-find": "No podemos encontrar información que coincida con su solicitud.", + "unable-to-process": "Estamos experimentando problemas al procesar su solicitud.", + "unable-to-retrieve": "No podemos recuperar la información solicitada en este momento." +} diff --git a/src/common/utils/i18n/resources/fr/account.json b/src/common/utils/i18n/resources/fr/account.json new file mode 100644 index 0000000..3a54b41 --- /dev/null +++ b/src/common/utils/i18n/resources/fr/account.json @@ -0,0 +1,59 @@ +{ + "about": "À propos", + "diagnostics": { + "app": "App", + "build": "Construire", + "diagnostics": "Diagnostics", + "label": { + "build": "Compilation", + "environment": "Environnement", + "id": "ID", + "name": "Nom", + "native": "Natal", + "platforms": "Plateformes", + "sha": "SHA", + "time": "Temps", + "version": "Version", + "workflow": "Flux de travail" + }, + "platform": "Plate-forme", + "platform-not-native": "Informations disponibles sur les appareils mobiles." + }, + "legal": "Légal", + "my-account": "Mon Compte", + "privacy-policy": "Politique de confidentialité", + "profile": { + "label": { + "bio": "Biographie", + "birthday": "Anniversaire", + "email": "E-mail", + "name": "Nom" + }, + "profile": "Profil", + "unable-to-process": "Nous rencontrons des problèmes lors du traitement de votre demande.", + "unable-to-retrieve": "Nous ne sommes pas en mesure de récupérer les détails de votre profil pour le moment." + }, + "settings": { + "font-size": { + "default": "Valeur par défaut", + "larger": "Plus grand", + "smaller": "Plus petit" + }, + "label": { + "brightness": "Luminosité", + "font-size": "Taille de la police", + "language": "Langue", + "notifications": "Notifications" + }, + "language": { + "en": "Anglais", + "es": "Espagnol", + "fr": "Français" + }, + "settings": "Paramètres", + "unable-to-update": "Impossible de mettre à jour les paramètres.", + "update-success": "Paramètres mis à jour." + }, + "terms-and-conditions": "Terms and conditions", + "version": "Version" +} diff --git a/src/common/utils/i18n/resources/fr/auth.json b/src/common/utils/i18n/resources/fr/auth.json new file mode 100644 index 0000000..0051d28 --- /dev/null +++ b/src/common/utils/i18n/resources/fr/auth.json @@ -0,0 +1,18 @@ +{ + "error": { + "unable-to-verify": "Nous n'avons pas pu vérifier vos informations d'identification. Veuillez réessayer." + }, + "info-username": { + "part1": "Cet exemple d'application utilise ", + "part2": "données JSONPlaceholder", + "part3": "Essayez un nom d'utilisateur comme ", + "part4": " ou ", + "part5": "Vous pouvez utiliser n’importe quelle valeur comme mot de passe." + }, + "label": { + "password": "Mot de passe", + "remember-me": "Souviens-toi de moi", + "username": "Nom d'utilisateur" + }, + "signin": "Se connecter" +} diff --git a/src/common/utils/i18n/resources/fr/common.json b/src/common/utils/i18n/resources/fr/common.json new file mode 100644 index 0000000..f8a68f2 --- /dev/null +++ b/src/common/utils/i18n/resources/fr/common.json @@ -0,0 +1,33 @@ +{ + "confirm-prompt": "Es-tu sûr??", + "created": "créé", + "deleted": "supprimé", + "error-no-data": "Aucune donnée", + "error-generic": "Un problème", + "ionic-playground": "Projet Ionic", + "label": { + "cancel": "Annuler", + "delete": "Supprimer", + "dismiss": "Rejeter", + "save": "Sauvegarder" + }, + "navigation": { + "account": "Compte", + "home": "Maison", + "menu": "Menu", + "signout": "Déconnecter", + "users": "Utilisateurs" + }, + "validation": { + "email": "Doit être une adresse e-mail. ", + "max": "Doit contenir au maximum {{max}} caractères. ", + "min": "Doit contenir au moins {{min}} caractères. ", + "oneOf": "Doit être l'un des: {{values}} ", + "required": "Requis. ", + "url": "Doit être une URL. " + }, + "no": "non", + "updated": "mis à jour", + "welcome": "Bienvenu", + "yes": "oui" +} diff --git a/src/common/utils/i18n/resources/fr/home.json b/src/common/utils/i18n/resources/fr/home.json new file mode 100644 index 0000000..1bfa024 --- /dev/null +++ b/src/common/utils/i18n/resources/fr/home.json @@ -0,0 +1,10 @@ +{ + "welcome": { + "sentence1": "Bienvenue au projet d'expérimentation Ionic.", + "sentence2": { + "1": "Ce projet montre comment créer une application multiplateforme à l'aide de", + "2": "cadre et", + "3": "composants" + } + } +} diff --git a/src/common/utils/i18n/resources/fr/index.ts b/src/common/utils/i18n/resources/fr/index.ts new file mode 100644 index 0000000..45cb7e7 --- /dev/null +++ b/src/common/utils/i18n/resources/fr/index.ts @@ -0,0 +1,7 @@ +import account from './account.json'; +import auth from './auth.json'; +import common from './common.json'; +import home from './home.json'; +import user from './user.json'; + +export default { account, auth, common, home, user }; diff --git a/src/common/utils/i18n/resources/fr/user.json b/src/common/utils/i18n/resources/fr/user.json new file mode 100644 index 0000000..521e171 --- /dev/null +++ b/src/common/utils/i18n/resources/fr/user.json @@ -0,0 +1,26 @@ +{ + "address": "Adresse", + "add-user": "Ajouter un utilisateur", + "browse-and-search": "Parcourez et recherchez tous les utilisateurs. Affichez les profils des utilisateurs et lisez leurs publications.", + "company": "Entreprise", + "contact": "Contact", + "delete": { + "deleting": "Suppression...", + "in-progress": "Suppression {{name}} en cours.", + "warning": "La suppression de {{name}} est permanente." + }, + "delete-user": "Supprimer l'utilisateur", + "edit-user": "Modifier l'utilisateur", + "label": { + "email": "Adresse email", + "name": "Nom", + "phone": "Numéro de téléphone", + "username": "Nom d'utilisateur", + "website": "Site web" + }, + "loading-users": "Chargement des utilisateurs", + "tap-to-view": "Appuyez pour afficher tous les utilisateurs.", + "unable-to-find": "Nous ne parvenons pas à trouver d'informations correspondant à votre demande.", + "unable-to-process": "Nous rencontrons des problèmes lors du traitement de votre demande.", + "unable-to-retrieve": "Nous ne sommes pas en mesure de récupérer les informations demandées pour le moment." +} diff --git a/src/main.tsx b/src/main.tsx index 79053b5..e7cd979 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; + +import 'common/utils/i18n'; import App from './App'; const container = document.getElementById('root'); diff --git a/src/pages/Account/AccountPage.tsx b/src/pages/Account/AccountPage.tsx index 508435d..d3c8b17 100644 --- a/src/pages/Account/AccountPage.tsx +++ b/src/pages/Account/AccountPage.tsx @@ -11,6 +11,7 @@ import { useIonRouter, } from '@ionic/react'; import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import { PropsWithTestId } from 'common/components/types'; @@ -30,6 +31,7 @@ const AccountPage = ({ testid = 'page-account' }: PropsWithTestId): JSX.Element const [diagnosticsCount, setDiagnosticsCount] = useState(0); const config = useConfig(); const router = useIonRouter(); + const { t } = useTranslation(); const versionTs = dayjs(config.VITE_BUILD_TS).format('YY.MM.DD.hhmm'); const sha = config.VITE_BUILD_COMMIT_SHA.substring(0, 7); @@ -47,7 +49,7 @@ const AccountPage = ({ testid = 'page-account' }: PropsWithTestId): JSX.Element return ( -
+
@@ -55,14 +57,18 @@ const AccountPage = ({ testid = 'page-account' }: PropsWithTestId): JSX.Element - Account + {t('navigation.account')} - Profile + + {t('profile.profile', { ns: 'account' })} + - Sign Out + + {t('navigation.signout')} + @@ -74,14 +80,18 @@ const AccountPage = ({ testid = 'page-account' }: PropsWithTestId): JSX.Element - Legal + {t('legal', { ns: 'account' })} - Privacy policy + + {t('privacy-policy', { ns: 'account' })} + - Terms and conditions + + {t('terms-and-conditions', { ns: 'account' })} + @@ -89,11 +99,13 @@ const AccountPage = ({ testid = 'page-account' }: PropsWithTestId): JSX.Element - About + {t('about', { ns: 'account' })} onDiagnosticsClick()}> - Version + + {t('version', { ns: 'account' })} + {version} diff --git a/src/pages/Account/components/Diagnostics/AppDiagnostics.tsx b/src/pages/Account/components/Diagnostics/AppDiagnostics.tsx index 5168855..cc0a6e2 100644 --- a/src/pages/Account/components/Diagnostics/AppDiagnostics.tsx +++ b/src/pages/Account/components/Diagnostics/AppDiagnostics.tsx @@ -1,5 +1,6 @@ import { IonItem, IonLabel, IonListHeader } from '@ionic/react'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import { BaseComponentProps } from 'common/components/types'; import { usePlatform } from 'common/hooks/usePlatform'; @@ -21,12 +22,13 @@ const AppDiagnostics = ({ }: BaseComponentProps): JSX.Element => { const { isNativePlatform } = usePlatform(); const { data: appInfo, isLoading } = useGetAppInfo(); + const { t } = useTranslation(); if (isNativePlatform) { return ( - App + {t('diagnostics.app', { ns: 'account' })} {isLoading && ( @@ -36,25 +38,33 @@ const AppDiagnostics = ({ {appInfo && ( <> - Name + + {t('diagnostics.label.name', { ns: 'account' })} + {appInfo.name} - ID + + {t('diagnostics.label.id', { ns: 'account' })} + {appInfo.id} - Build + + {t('diagnostics.label.build', { ns: 'account' })} + {appInfo.build} - Version + + {t('diagnostics.label.version', { ns: 'account' })} + {appInfo.version} @@ -67,11 +77,11 @@ const AppDiagnostics = ({ return ( - App + {t('diagnostics.app', { ns: 'account' })} - Information available on mobile devices. + {t('diagnostics.platform-not-native', { ns: 'account' })} diff --git a/src/pages/Account/components/Diagnostics/BuildDiagnostics.tsx b/src/pages/Account/components/Diagnostics/BuildDiagnostics.tsx index 95b0157..be95f66 100644 --- a/src/pages/Account/components/Diagnostics/BuildDiagnostics.tsx +++ b/src/pages/Account/components/Diagnostics/BuildDiagnostics.tsx @@ -1,6 +1,7 @@ import { IonItem, IonLabel, IonListHeader, IonText } from '@ionic/react'; import classNames from 'classnames'; import dayjs from 'dayjs'; +import { useTranslation } from 'react-i18next'; import { BaseComponentProps } from 'common/components/types'; import List from 'common/components/List/List'; @@ -19,31 +20,40 @@ const BuildDiagnostics = ({ testid = 'diagnostics-build', }: BaseComponentProps): JSX.Element => { const config = useConfig(); + const { t } = useTranslation(); return ( - Build + {t('diagnostics.build', { ns: 'account' })} - Environment + + {t('diagnostics.label.environment', { ns: 'account' })} + {config.VITE_BUILD_ENV_CODE} - Time + + {t('diagnostics.label.time', { ns: 'account' })} + {dayjs(config.VITE_BUILD_TS).format('YYYY-MM-DD HH:mm:ss Z')} - SHA + + {t('diagnostics.label.sha', { ns: 'account' })} + {config.VITE_BUILD_COMMIT_SHA} - Workflow + + {t('diagnostics.label.workflow', { ns: 'account' })} + {config.VITE_BUILD_WORKFLOW_NAME} {config.VITE_BUILD_WORKFLOW_RUN_NUMBER}. {config.VITE_BUILD_WORKFLOW_RUN_ATTEMPT} diff --git a/src/pages/Account/components/Diagnostics/DiagnosticsPage.tsx b/src/pages/Account/components/Diagnostics/DiagnosticsPage.tsx index 1ba3ad6..0d103d9 100644 --- a/src/pages/Account/components/Diagnostics/DiagnosticsPage.tsx +++ b/src/pages/Account/components/Diagnostics/DiagnosticsPage.tsx @@ -1,4 +1,5 @@ import { IonCol, IonContent, IonGrid, IonPage, IonRow } from '@ionic/react'; +import { useTranslation } from 'react-i18next'; import { PropsWithTestId } from 'common/components/types'; import Header from 'common/components/Header/Header'; @@ -15,16 +16,22 @@ import BuildDiagnostics from './BuildDiagnostics'; * @returns {JSX.Element} JSX */ const DiagnosticsPage = ({ testid = 'page-diagnostics' }: PropsWithTestId): JSX.Element => { + const { t } = useTranslation(); + return ( -
+
- Diagnostics + {t('diagnostics.diagnostics', { ns: 'account' })} diff --git a/src/pages/Account/components/Diagnostics/PlatformDiagnostics.tsx b/src/pages/Account/components/Diagnostics/PlatformDiagnostics.tsx index 6314947..0e8a553 100644 --- a/src/pages/Account/components/Diagnostics/PlatformDiagnostics.tsx +++ b/src/pages/Account/components/Diagnostics/PlatformDiagnostics.tsx @@ -5,6 +5,7 @@ import { BaseComponentProps } from 'common/components/types'; import { usePlatform } from 'common/hooks/usePlatform'; import List from 'common/components/List/List'; import Badges from 'common/components/Badge/Badges'; +import { useTranslation } from 'react-i18next'; /** * The `PlatformDiagnostics` component displays application diagnostic information @@ -19,24 +20,31 @@ const PlatformDiagnostics = ({ testid = 'diagnostics-platform', }: BaseComponentProps): JSX.Element => { const { isNativePlatform, platforms } = usePlatform(); + const { t } = useTranslation(); return ( - Platform + {t('diagnostics.platform', { ns: 'account' })} - Native + + {t('diagnostics.label.native', { ns: 'account' })} + {isNativePlatform ? ( - YES + + {t('yes')} + ) : ( - - NO + + {t('no')} )} - Platforms + + {t('diagnostics.label.platforms', { ns: 'account' })} + {platforms.map((platform) => ( diff --git a/src/pages/Account/components/Profile/ProfileForm.tsx b/src/pages/Account/components/Profile/ProfileForm.tsx index fe94da7..584edf4 100644 --- a/src/pages/Account/components/Profile/ProfileForm.tsx +++ b/src/pages/Account/components/Profile/ProfileForm.tsx @@ -3,6 +3,7 @@ import { useRef, useState } from 'react'; import { Form, Formik } from 'formik'; import { date, object, string } from 'yup'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import './ProfileForm.scss'; import { BaseComponentProps } from 'common/components/types'; @@ -32,16 +33,6 @@ interface ProfileFormProps extends BaseComponentProps { profile: Profile; } -/** - * Profile form validation schema. - */ -const validationSchema = object({ - name: string().required('Required. '), - email: string().required('Required. ').email('Must be an email address. '), - bio: string().max(500, 'Must be 500 characters or less. '), - dateOfBirth: date().required('Required. '), -}); - /** * The `ProfileForm` component renders a Formik form to edit a user profile. * @param {ProfileFormProps} props - Component propeties. @@ -58,6 +49,17 @@ const ProfileForm = ({ const router = useIonRouter(); const { setProgress } = useProgress(); const { createToast } = useToasts(); + const { t } = useTranslation(); + + /** + * Profile form validation schema. + */ + const validationSchema = object({ + name: string().required(t('validation.required')), + email: string().required(t('validation.required')).email(t('validation.email')), + bio: string().max(500, ({ max }) => t('validation.max', { max })), + dateOfBirth: date().required(t('validation.required')), + }); useIonViewDidEnter(() => { focusInput.current?.setFocus(); @@ -71,7 +73,7 @@ const ProfileForm = ({
{error && ( @@ -95,7 +97,7 @@ const ProfileForm = ({ createToast({ message: 'Updated profile', duration: 5000, - buttons: [DismissButton], + buttons: [DismissButton()], }); router.goBack(); }, @@ -115,7 +117,7 @@ const ProfileForm = ({
- Cancel + {t('label.cancel')} - Save + {t('label.save')}
diff --git a/src/pages/Account/components/Profile/ProfilePage.tsx b/src/pages/Account/components/Profile/ProfilePage.tsx index b202eb1..af8aafc 100644 --- a/src/pages/Account/components/Profile/ProfilePage.tsx +++ b/src/pages/Account/components/Profile/ProfilePage.tsx @@ -1,4 +1,5 @@ import { IonCol, IonContent, IonGrid, IonPage, IonRow } from '@ionic/react'; +import { useTranslation } from 'react-i18next'; import { PropsWithTestId } from 'common/components/types'; import { useGetProfile } from 'pages/Account/api/useGetProfile'; @@ -20,11 +21,16 @@ import CardRow from 'common/components/Card/CardRow'; */ const ProfilePage = ({ testid = 'page-profile' }: PropsWithTestId): JSX.Element => { const { data: profile, isError, isLoading } = useGetProfile(); + const { t } = useTranslation(); return ( -
+
@@ -38,7 +44,7 @@ const ProfilePage = ({ testid = 'page-profile' }: PropsWithTestId): JSX.Element {isError && ( @@ -48,7 +54,7 @@ const ProfilePage = ({ testid = 'page-profile' }: PropsWithTestId): JSX.Element {profile && ( <> -
Profile
+
{t('profile.profile', { ns: 'account' })}
diff --git a/src/pages/Account/components/Settings/SettingsForm.tsx b/src/pages/Account/components/Settings/SettingsForm.tsx index 9d89b92..e07ee20 100644 --- a/src/pages/Account/components/Settings/SettingsForm.tsx +++ b/src/pages/Account/components/Settings/SettingsForm.tsx @@ -4,10 +4,12 @@ import { Form, Formik } from 'formik'; import { boolean, number, object, string } from 'yup'; import orderBy from 'lodash/orderBy'; import map from 'lodash/map'; +import { useTranslation } from 'react-i18next'; import './SettingsForm.scss'; +import storage from 'common/utils/storage'; import { BaseComponentProps } from 'common/components/types'; -import { LANGUAGES } from 'common/utils/constants'; +import { LANGUAGES, StorageKey } from 'common/utils/constants'; import { Settings } from 'common/models/settings'; import { useGetSettings } from 'common/api/useGetSettings'; import { useUpdateSettings } from 'common/api/useUpdateSettings'; @@ -31,18 +33,6 @@ type SettingsFormValues = Pick< 'allowNotifications' | 'brightness' | 'fontSize' | 'language' >; -/** - * Settings form validation schema. - */ -const validationSchema = object({ - allowNotifications: boolean(), - brightness: number().min(0).max(100), - fontSize: string() - .required('Required. ') - .oneOf(['smaller', 'default', 'larger'], 'Font size must be one of: ${values} '), - language: string().oneOf(map(LANGUAGES, 'code')), -}); - /** * The `SettingsForm` component renders a Formik form to edit user settings. * @param {BaseComponentProps} props - Component properties. @@ -56,13 +46,28 @@ const SettingsForm = ({ const { mutate: updateSettings } = useUpdateSettings(); const { setProgress } = useProgress(); const { createToast } = useToasts(); + const { i18n, t } = useTranslation(); + + /** + * Settings form validation schema. + */ + const validationSchema = object({ + allowNotifications: boolean(), + brightness: number().min(0).max(100), + fontSize: string() + .required(t('validation.required')) + .oneOf(['smaller', 'default', 'larger'], ({ values }) => t('validation.oneOf', { values })), + language: string().oneOf(map(LANGUAGES, 'code'), ({ values }) => + t('validation.oneOf', { values }), + ), + }); if (isLoading) { return (
- Settings + {t('settings.settings', { ns: 'account' })} @@ -104,17 +109,20 @@ const SettingsForm = ({ updateSettings( { settings: values }, { - onSuccess: () => { + onSuccess: (settings) => { createToast({ - message: 'Settings updated.', + message: t('settings.update-success', { ns: 'account' }), duration: 3000, - buttons: [DismissButton], + buttons: [DismissButton()], }); + // store the preferred language for i18n language detection + storage.setItem(StorageKey.Language, settings.language); + i18n.changeLanguage(settings.language); }, onError: () => { createToast({ - message: 'Unable to update settings.', - buttons: [DismissButton], + message: t('settings.unable-to-update', { ns: 'account' }), + buttons: [DismissButton()], color: 'danger', }); }, @@ -131,7 +139,7 @@ const SettingsForm = ({
- Settings + {t('settings.settings', { ns: 'account' })} @@ -141,14 +149,14 @@ const SettingsForm = ({ onIonChange={() => submitForm()} testid={`${testid}-field-allowNotifications`} > - Notifications + {t('settings.label.notifications', { ns: 'account' })} submitForm()} @@ -162,7 +170,7 @@ const SettingsForm = ({ submitForm()} @@ -170,14 +178,14 @@ const SettingsForm = ({ > {orderBy(LANGUAGES, ['value']).map((language) => ( - {language.value} + {t(`settings.language.${language.code}`, { ns: 'account' })} ))} - Font Size + {t('settings.label.font-size', { ns: 'account' })} @@ -191,21 +199,21 @@ const SettingsForm = ({ disabled={isSubmitting} value="smaller" > - Smaller + {t('settings.font-size.smaller', { ns: 'account' })} - Default + {t('settings.font-size.default', { ns: 'account' })} - Larger + {t('settings.font-size.larger', { ns: 'account' })} diff --git a/src/pages/Auth/SignIn/SignInPage.tsx b/src/pages/Auth/SignIn/SignInPage.tsx index 5f561f1..8395b50 100644 --- a/src/pages/Auth/SignIn/SignInPage.tsx +++ b/src/pages/Auth/SignIn/SignInPage.tsx @@ -1,4 +1,5 @@ import { IonContent, IonPage } from '@ionic/react'; +import { useTranslation } from 'react-i18next'; import './SignInPage.scss'; import { PropsWithTestId } from 'common/components/types'; @@ -18,10 +19,12 @@ interface SignInPageProps extends PropsWithTestId {} * @returns {JSX.Element} JSX */ const SignInPage = ({ testid = 'page-signin' }: SignInPageProps): JSX.Element => { + const { t } = useTranslation(); + return ( -
+
diff --git a/src/pages/Auth/SignIn/components/SignInForm.tsx b/src/pages/Auth/SignIn/components/SignInForm.tsx index 4a22c57..4c7674f 100644 --- a/src/pages/Auth/SignIn/components/SignInForm.tsx +++ b/src/pages/Auth/SignIn/components/SignInForm.tsx @@ -10,6 +10,7 @@ import { useRef, useState } from 'react'; import classNames from 'classnames'; import { Form, Formik } from 'formik'; import { boolean, object, string } from 'yup'; +import { useTranslation } from 'react-i18next'; import './SignInForm.scss'; import { BaseComponentProps } from 'common/components/types'; @@ -40,15 +41,6 @@ interface SignInFormValues { rememberMe: boolean; } -/** - * Sign in form validation schema. - */ -const validationSchema = object({ - username: string().required('Required. '), - password: string().required('Required. '), - rememberMe: boolean().default(false), -}); - /** * The `SignInForm` component renders a Formik form for user authentication. * @param {SignInFormProps} props - Component properties. @@ -60,6 +52,16 @@ const SignInForm = ({ className, testid = 'form-signin' }: SignInFormProps): JSX const { setIsActive: setShowProgress } = useProgress(); const router = useIonRouter(); const { mutate: signIn } = useSignIn(); + const { t } = useTranslation(); + + /** + * Sign in form validation schema. + */ + const validationSchema = object({ + username: string().required(t('validation.required')), + password: string().required(t('validation.required')), + rememberMe: boolean().default(false), + }); // remember me details const rememberMe = storage.getJsonItem(StorageKey.RememberMe); @@ -72,7 +74,7 @@ const SignInForm = ({ className, testid = 'form-signin' }: SignInFormProps): JSX
{error && ( @@ -113,13 +115,13 @@ const SignInForm = ({ className, testid = 'form-signin' }: SignInFormProps): JSX {({ dirty, isSubmitting }) => ( -
Sign In
+
{t('signin', { ns: 'auth' })}
- Remember me + {t('label.remember-me', { ns: 'auth' })} - Sign In + {t('signin', { ns: 'auth' })}

- This example application uses{' '} + {t('info-username.part1', { ns: 'auth' })} - JSONPlaceholder data + {t('info-username.part2', { ns: 'auth' })} - . Try a username like Bret or{' '} + . {t('info-username.part3', { ns: 'auth' })}{' '} + Bret{' '} + {t('info-username.part4', { ns: 'auth' })}{' '} Samantha.

-

You may use any value as the password.

+

{t('info-username.part5', { ns: 'auth' })}

diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx index e970b8b..2770308 100644 --- a/src/pages/Home/HomePage.tsx +++ b/src/pages/Home/HomePage.tsx @@ -9,6 +9,7 @@ import { RefresherEventDetail, } from '@ionic/react'; import { useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; import { QueryKey } from 'common/utils/constants'; import Header from 'common/components/Header/Header'; @@ -22,6 +23,7 @@ import WelcomeBlock from './components/WelcomeBlock/WelcomeBlock'; */ const HomePage = (): JSX.Element => { const queryClient = useQueryClient(); + const { t } = useTranslation(); const handleRefresh = async (event: CustomEvent) => { await queryClient.refetchQueries({ queryKey: [QueryKey.Users], exact: true }); @@ -30,7 +32,7 @@ const HomePage = (): JSX.Element => { return ( -
+
diff --git a/src/pages/Home/components/WelcomeBlock/WelcomeBlock.tsx b/src/pages/Home/components/WelcomeBlock/WelcomeBlock.tsx index afae9f1..dd1211d 100644 --- a/src/pages/Home/components/WelcomeBlock/WelcomeBlock.tsx +++ b/src/pages/Home/components/WelcomeBlock/WelcomeBlock.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from 'react-i18next'; + import Block from 'common/components/Block/Block'; import { BaseComponentProps } from 'common/components/types'; @@ -14,23 +16,24 @@ interface WelcomeBlockProps extends BaseComponentProps {} * @returns JSX */ const WelcomeBlock = ({ className, testid = 'block-welcome' }: WelcomeBlockProps): JSX.Element => { + const { t } = useTranslation(); + return ( - Welcome to the Ionic playground project. This project demonstrates how to create a - cross-platform application using the{' '} + {t('welcome.sentence1', { ns: 'home' })} {t('welcome.sentence2.1', { ns: 'home' })}{' '} Ionic {' '} - framework and{' '} + {t('welcome.sentence2.2', { ns: 'home' })}{' '} React {' '} - components. + {t('welcome.sentence2.3', { ns: 'home' })}.
} /> diff --git a/src/pages/Users/components/UserAdd/UserAddModal.tsx b/src/pages/Users/components/UserAdd/UserAddModal.tsx index 6e466e8..7b2d46f 100644 --- a/src/pages/Users/components/UserAdd/UserAddModal.tsx +++ b/src/pages/Users/components/UserAdd/UserAddModal.tsx @@ -12,6 +12,7 @@ import { useIonRouter, } from '@ionic/react'; import { ComponentPropsWithoutRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { PropsWithTestId } from 'common/components/types'; import { useCreateUser } from 'pages/Users/api/useCreateUser'; @@ -51,6 +52,7 @@ const UserAddModal = ({ const { isActive: isActiveProgressBar, progressBar, setProgress } = useProgress(); const { createToast } = useToasts(); const { mutate: createUser } = useCreateUser(); + const { t } = useTranslation(); const didDismiss = (e: IonModalCustomEvent) => { onIonModalDidDismiss?.(e); @@ -61,7 +63,7 @@ const UserAddModal = ({ - Add User + {t('add-user', { ns: 'user' })} setIsOpen(false)} data-testid={`${testid}-button-close`}> @@ -75,7 +77,7 @@ const UserAddModal = ({ {error && ( @@ -91,9 +93,9 @@ const UserAddModal = ({ setProgress(false); setSubmitting(false); createToast({ - buttons: [DismissButton], + buttons: [DismissButton()], duration: 5000, - message: `${user.name} created`, + message: `${user.name} ${t('created')}`, }); setIsOpen(false); router.push(`/tabs/users/${user.id}`); diff --git a/src/pages/Users/components/UserDelete/UserDeleteAlert.tsx b/src/pages/Users/components/UserDelete/UserDeleteAlert.tsx index 5c6e38a..0f6f9cd 100644 --- a/src/pages/Users/components/UserDelete/UserDeleteAlert.tsx +++ b/src/pages/Users/components/UserDelete/UserDeleteAlert.tsx @@ -1,6 +1,7 @@ import { IonAlert } from '@ionic/react'; import { ComponentPropsWithoutRef } from 'react'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import './UserDeleteAlert.scss'; import { BaseComponentProps } from 'common/components/types'; @@ -49,6 +50,7 @@ const UserDeleteAlert = ({ user, }: UserDeleteAlertProps): JSX.Element => { const { isPending: isDeleting, mutate: deleteUser } = useDeleteUser(); + const { t } = useTranslation(); const doDeleteUser = () => { isPending?.(true); @@ -81,7 +83,7 @@ const UserDeleteAlert = ({ disabled: isDeleting, 'data-testid': `${testid}-button-cancel`, }, - text: 'Cancel', + text: t('label.cancel'), }, { handler: () => { @@ -89,13 +91,15 @@ const UserDeleteAlert = ({ return false; }, htmlAttributes: { disabled: isDeleting, 'data-testid': `${testid}-button-delete` }, - text: 'Delete', + text: t('label.delete'), }, ]} - header={isDeleting ? 'Deleting...' : 'Are you sure?'} + header={isDeleting ? t('delete.deleting', { ns: 'user' }) : t('confirm-prompt')} isOpen={isOpen} message={ - isDeleting ? `Deleting ${user?.name} in progress.` : `Deleting ${user?.name} is permanent.` + isDeleting + ? t('delete.in-progress', { name: user?.name, ns: 'user' }) + : t('delete.warning', { name: user?.name, ns: 'user' }) } /> ); diff --git a/src/pages/Users/components/UserDetail/AddressDetail.tsx b/src/pages/Users/components/UserDetail/AddressDetail.tsx index 2a0a2ec..530765a 100644 --- a/src/pages/Users/components/UserDetail/AddressDetail.tsx +++ b/src/pages/Users/components/UserDetail/AddressDetail.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import { Address } from 'common/models/user'; import { BaseComponentProps } from 'common/components/types'; @@ -40,6 +41,8 @@ const AddressDetail = ({ isLoading = false, testid = 'address-detail', }: AddressDetailProps): JSX.Element | false => { + const { t } = useTranslation(); + if (isLoading) { // loading state return ( @@ -67,7 +70,7 @@ const AddressDetail = ({
-
Address
+
{t('address', { ns: 'user' })}
{address.street}
diff --git a/src/pages/Users/components/UserDetail/CompanyDetail.tsx b/src/pages/Users/components/UserDetail/CompanyDetail.tsx index a42984b..ce3cca1 100644 --- a/src/pages/Users/components/UserDetail/CompanyDetail.tsx +++ b/src/pages/Users/components/UserDetail/CompanyDetail.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import { BaseComponentProps } from 'common/components/types'; import { Company } from 'common/models/user'; @@ -40,6 +41,8 @@ const CompanyDetail = ({ isLoading = false, testid = 'company-detail', }: CompanyDetailProps): JSX.Element | false => { + const { t } = useTranslation(); + if (isLoading) { // loading state return ( @@ -66,7 +69,7 @@ const CompanyDetail = ({
-
Company
+
{t('company', { ns: 'user' })}
{company.name}
diff --git a/src/pages/Users/components/UserDetail/ContactInfo.tsx b/src/pages/Users/components/UserDetail/ContactInfo.tsx index bfeb053..7af3124 100644 --- a/src/pages/Users/components/UserDetail/ContactInfo.tsx +++ b/src/pages/Users/components/UserDetail/ContactInfo.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import './ContactInfo.scss'; import { BaseComponentProps } from 'common/components/types'; @@ -44,6 +45,8 @@ const ContactInfo = ({ testid = 'contact-info', user, }: ContactInfoProps): JSX.Element | false => { + const { t } = useTranslation(); + if (isLoading) { // loading state return ( @@ -73,7 +76,7 @@ const ContactInfo = ({ {showHeader && ( -
Contact Info
+
{t('contact', { ns: 'user' })}
)}
diff --git a/src/pages/Users/components/UserDetail/UserDetail.tsx b/src/pages/Users/components/UserDetail/UserDetail.tsx index c5db0b8..9e338bd 100644 --- a/src/pages/Users/components/UserDetail/UserDetail.tsx +++ b/src/pages/Users/components/UserDetail/UserDetail.tsx @@ -1,5 +1,6 @@ import { IonCol, IonGrid, IonRow } from '@ionic/react'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import './UserDetail.scss'; import { BaseComponentProps } from 'common/components/types'; @@ -36,6 +37,7 @@ const UserDetail = ({ testid = 'user-detail', userId, }: UserDetailProps): JSX.Element => { + const { t } = useTranslation(); const { data: user, isError, isLoading } = useGetUser({ userId }); const baseProps = { @@ -48,7 +50,7 @@ const UserDetail = ({ return (
- +
); diff --git a/src/pages/Users/components/UserDetail/UserDetailPage.tsx b/src/pages/Users/components/UserDetail/UserDetailPage.tsx index bf26086..6dd4c55 100644 --- a/src/pages/Users/components/UserDetail/UserDetailPage.tsx +++ b/src/pages/Users/components/UserDetail/UserDetailPage.tsx @@ -1,6 +1,7 @@ import { IonButton, IonButtons, IonContent, IonPage, IonText, useIonRouter } from '@ionic/react'; import { useState } from 'react'; import { useParams } from 'react-router'; +import { useTranslation } from 'react-i18next'; import { PropsWithTestId } from 'common/components/types'; import { useGetUser } from 'pages/Users/api/useGetUser'; @@ -37,6 +38,7 @@ interface UserDetailPageRouteParams { export const UserDetailPage = ({ testid = 'page-user-detail', }: UserDetailPageProps): JSX.Element => { + const { t } = useTranslation(); const router = useIonRouter(); const { createToast } = useToasts(); const [showConfirmDelete, setShowConfirmDelete] = useState(false); @@ -54,7 +56,7 @@ export const UserDetailPage = ({ user && ( <> setShowConfirmDelete(true)} @@ -86,7 +88,7 @@ export const UserDetailPage = ({ {user.name} setShowConfirmDelete(true)} data-testid={`${testid}-page-header-button-delete`} @@ -127,9 +129,9 @@ export const UserDetailPage = ({ onSuccess={() => { setShowConfirmDelete(false); createToast({ - buttons: [DismissButton], + buttons: [DismissButton()], duration: 5000, - message: `${user?.name} deleted`, + message: `${user?.name} ${t('deleted')}`, }); router.goBack(); }} diff --git a/src/pages/Users/components/UserEdit/UserEdit.tsx b/src/pages/Users/components/UserEdit/UserEdit.tsx index 6360a78..e9be022 100644 --- a/src/pages/Users/components/UserEdit/UserEdit.tsx +++ b/src/pages/Users/components/UserEdit/UserEdit.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { IonCol, IonGrid, IonRow, useIonRouter } from '@ionic/react'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import { BaseComponentProps } from 'common/components/types'; import { User } from 'common/models/user'; @@ -25,6 +26,7 @@ interface UserEditProps extends BaseComponentProps { * @returns {JSX.Element} JSX */ const UserEdit = ({ className, user, testid = 'user-edit' }: UserEditProps): JSX.Element => { + const { t } = useTranslation(); const router = useIonRouter(); const [error, setError] = useState(''); const { mutate: updateUser } = useUpdateUser(); @@ -38,7 +40,7 @@ const UserEdit = ({ className, user, testid = 'user-edit' }: UserEditProps): JSX {error && ( @@ -56,9 +58,9 @@ const UserEdit = ({ className, user, testid = 'user-edit' }: UserEditProps): JSX setProgress(false); setSubmitting(false); createToast({ - buttons: [DismissButton], + buttons: [DismissButton()], duration: 5000, - message: `${user.name} updated`, + message: `${user.name} ${t('updated')}`, }); if (router.canGoBack()) { router.goBack(); diff --git a/src/pages/Users/components/UserForm/UserForm.tsx b/src/pages/Users/components/UserForm/UserForm.tsx index c3102b4..648fbb8 100644 --- a/src/pages/Users/components/UserForm/UserForm.tsx +++ b/src/pages/Users/components/UserForm/UserForm.tsx @@ -3,6 +3,7 @@ import { IonButton } from '@ionic/react'; import { Form, Formik, FormikHelpers } from 'formik'; import { object, string } from 'yup'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import './UserForm.scss'; import { BaseComponentProps } from 'common/components/types'; @@ -25,20 +26,6 @@ interface UserFormProps extends BaseComponentProps { user?: User; } -/** - * User form validation schema. - */ -const validationSchema = object({ - name: string().required('Required. '), - username: string() - .required('Required. ') - .min(8, 'Must be at least 8 characters. ') - .max(30, 'Must be at most 30 characters. '), - email: string().required('Required. ').email('Must be an email address. '), - phone: string().required('Required. '), - website: string().url('Must be a URL. '), -}); - /** * The `UserForm` component renders a Formik form for creating or editing * a `User`. @@ -52,11 +39,26 @@ const UserForm = ({ testid = 'form-user', }: UserFormProps): JSX.Element => { const focusInput = useRef(null); + const { t } = useTranslation(); useEffect(() => { focusInput.current?.setFocus(); }, []); + /** + * User form validation schema. + */ + const validationSchema = object({ + name: string().required(t('validation.required')), + username: string() + .required(t('validation.required')) + .min(8, ({ min }) => t('validation.min', { min })) + .max(30, ({ max }) => t('validation.max', { max })), + email: string().required(t('validation.required')).email(t('validation.email')), + phone: string().required(t('validation.required')), + website: string().url(t('validation.url')), + }); + return (
@@ -75,7 +77,7 @@ const UserForm = ({
- Save + {t('label.save')}
)} diff --git a/src/pages/Users/components/UserList/UserGrid.tsx b/src/pages/Users/components/UserList/UserGrid.tsx index 2f7bee8..bdc58f6 100644 --- a/src/pages/Users/components/UserList/UserGrid.tsx +++ b/src/pages/Users/components/UserList/UserGrid.tsx @@ -1,6 +1,7 @@ import { IonCol, IonGrid, IonRow } from '@ionic/react'; import classNames from 'classnames'; import isEmpty from 'lodash/isEmpty'; +import { useTranslation } from 'react-i18next'; import './UserGrid.scss'; import { BaseComponentProps } from 'common/components/types'; @@ -29,6 +30,7 @@ interface UserGridProps extends BaseComponentProps { * @see {@link IonGrid} */ const UserGrid = ({ className, filterBy, testid = 'grid-user' }: UserGridProps): JSX.Element => { + const { t } = useTranslation(); const { data: users, isError, isLoading } = useGetUsers(); const baseProps = { @@ -43,7 +45,7 @@ const UserGrid = ({ className, filterBy, testid = 'grid-user' }: UserGridProps):
); @@ -54,7 +56,7 @@ const UserGrid = ({ className, filterBy, testid = 'grid-user' }: UserGridProps): return (
- +
); @@ -67,7 +69,7 @@ const UserGrid = ({ className, filterBy, testid = 'grid-user' }: UserGridProps): return (
- +
); diff --git a/src/pages/Users/components/UserList/UserList.tsx b/src/pages/Users/components/UserList/UserList.tsx index a016dab..d43135f 100644 --- a/src/pages/Users/components/UserList/UserList.tsx +++ b/src/pages/Users/components/UserList/UserList.tsx @@ -1,6 +1,7 @@ import { IonList, IonListHeader } from '@ionic/react'; import classNames from 'classnames'; import isEmpty from 'lodash/isEmpty'; +import { useTranslation } from 'react-i18next'; import './UserList.scss'; import { BaseComponentProps } from 'common/components/types'; @@ -22,7 +23,6 @@ import EmptyCard from 'common/components/Card/EmptyCard'; interface UserListProps extends BaseComponentProps { filterBy?: string; header?: string; - showHeader?: boolean; } /** @@ -34,10 +34,10 @@ interface UserListProps extends BaseComponentProps { const UserList = ({ className, filterBy, - header = 'Users', - showHeader = false, + header, testid = 'list-user', }: UserListProps): JSX.Element => { + const { t } = useTranslation(); const { data: users, isError, isLoading } = useGetUsers(); const baseProps = { @@ -52,7 +52,7 @@ const UserList = ({
); @@ -63,7 +63,7 @@ const UserList = ({ return (
- +
); @@ -76,7 +76,7 @@ const UserList = ({ return (
- +
); @@ -85,7 +85,7 @@ const UserList = ({ // Success state return ( - {showHeader && {header}} + {header && {header}} {filteredUsers && filteredUsers.map((user, index) => ( diff --git a/src/pages/Users/components/UserList/UserListItem.tsx b/src/pages/Users/components/UserList/UserListItem.tsx index 1026d7e..c803854 100644 --- a/src/pages/Users/components/UserList/UserListItem.tsx +++ b/src/pages/Users/components/UserList/UserListItem.tsx @@ -102,7 +102,7 @@ const UserListItem = ({ className, lines, testid, user }: UserListItemProps): JS onSuccess={() => { setShowConfirmDelete(false); createToast({ - buttons: [DismissButton], + buttons: [DismissButton()], duration: 5000, message: `${user?.name} deleted`, }); diff --git a/src/pages/Users/components/UserList/UserListPage.tsx b/src/pages/Users/components/UserList/UserListPage.tsx index f3cab18..13eb2f3 100644 --- a/src/pages/Users/components/UserList/UserListPage.tsx +++ b/src/pages/Users/components/UserList/UserListPage.tsx @@ -12,6 +12,7 @@ import { import { useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import './UserListPage.scss'; import { PropsWithTestId } from 'common/components/types'; @@ -35,6 +36,7 @@ import UserAddModal from '../UserAdd/UserAddModal'; export const UserListPage = ({ testid = 'page-user-list' }: PropsWithTestId): JSX.Element => { const [isOpenModal, setIsOpenModal] = useState(false); const [search, setSearch] = useState(''); + const { t } = useTranslation(); const queryClient = useQueryClient(); const { handleIonScroll, scrollDirection } = useScrollContext(); @@ -59,7 +61,7 @@ export const UserListPage = ({ testid = 'page-user-list' }: PropsWithTestId): JS
, @@ -77,11 +79,11 @@ export const UserListPage = ({ testid = 'page-user-list' }: PropsWithTestId): JS - Users + {t('navigation.users')} setIsOpenModal(true)} data-testid={`${testid}-page-header-button-create`} > diff --git a/src/pages/Users/components/UserSummaryCard/UserSummaryCard.tsx b/src/pages/Users/components/UserSummaryCard/UserSummaryCard.tsx index afc66ea..c090773 100644 --- a/src/pages/Users/components/UserSummaryCard/UserSummaryCard.tsx +++ b/src/pages/Users/components/UserSummaryCard/UserSummaryCard.tsx @@ -7,6 +7,7 @@ import { IonCardTitle, } from '@ionic/react'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import './UserSummaryCard.scss'; import { useGetUsers } from 'pages/Users/api/useGetUsers'; @@ -30,6 +31,7 @@ const UserSummaryCard = ({ testid = 'card-user-summary', }: UserSummaryCardProps): JSX.Element => { const { data: users } = useGetUsers(); + const { t } = useTranslation(); return ( - Users + {t('navigation.users')} {users && ( {users.length} )} - Tap to view all users. + {t('tap-to-view', { ns: 'user' })} - - Browse and search all the users. View user profiles and read their posts. - + {t('browse-and-search', { ns: 'user' })} ); }; diff --git a/src/test/test-utils.tsx b/src/test/test-utils.tsx index 071fae5..9194ea9 100644 --- a/src/test/test-utils.tsx +++ b/src/test/test-utils.tsx @@ -7,6 +7,7 @@ import { RenderOptions, } from '@testing-library/react'; +import 'common/utils/i18n'; import WithAllProviders from './wrappers/WithAllProviders'; const customRender = (ui: React.ReactElement, options?: RenderOptions, { route = '/' } = {}) => { diff --git a/src/theme/typography.scss b/src/theme/typography.scss index 28f5c2f..c35c0f7 100644 --- a/src/theme/typography.scss +++ b/src/theme/typography.scss @@ -77,4 +77,18 @@ .break-keep { word-break: keep-all; } + + // text transform + .capitalize { + text-transform: capitalize; + } + .lowercase { + text-transform: lowercase; + } + .uppercase { + text-transform: uppercase; + } + .normal-case { + text-transform: none; + } }