diff --git a/package-lock.json b/package-lock.json index 72c749b..9d2129d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -801,6 +801,12 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/faker": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/faker/-/faker-4.1.12.tgz", + "integrity": "sha512-0MEyzJrLLs1WaOCx9ULK6FzdCSj2EuxdSP9kvuxxdBEGujZYUOZ4vkPXdgu3dhyg/pOdn7VCatelYX7k0YShlA==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", @@ -1046,13 +1052,15 @@ } }, "andculturecode-javascript-core": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/andculturecode-javascript-core/-/andculturecode-javascript-core-0.0.7.tgz", - "integrity": "sha512-AfBiuB2MlInHcCb4BGK/WKew0gYmo/hhXbdKoN9DFC/OCPDRLh0qBLSFHifgmVfV16T3ryLzXh5f2KIvcdWlhg==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/andculturecode-javascript-core/-/andculturecode-javascript-core-0.1.0.tgz", + "integrity": "sha512-/wGg8Z+m3CA9y9kRUkGYOTxXWXgsb18UardXkvi7UEClYj1BgdiMh6PuIWXGoE/agU8m8qBEXBJxExaSX4awPg==", "requires": { "axios": "0.19.2", + "i18next": "19.4.5", "immutable": "4.0.0-rc.12", - "lodash": "4.17.15" + "lodash": "4.17.15", + "query-string": "6.13.1" } }, "andculturecode-javascript-testing": { @@ -1740,8 +1748,7 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "deep-is": { "version": "0.1.3", @@ -2089,6 +2096,12 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, + "faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", + "dev": true + }, "fast-deep-equal": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", @@ -2408,6 +2421,15 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html-parse-stringify2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz", + "integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=", + "dev": true, + "requires": { + "void-elements": "^2.0.1" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -2425,6 +2447,14 @@ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true }, + "i18next": { + "version": "19.4.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.4.5.tgz", + "integrity": "sha512-aLvSsURoupi3x9IndmV6+m3IGhzLzhYv7Gw+//K3ovdliyGcFRV0I1MuddI0Bk/zR7BG1U+kJOjeHFUcUIdEgg==", + "requires": { + "@babel/runtime": "^7.3.1" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4375,6 +4405,16 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "query-string": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.1.tgz", + "integrity": "sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==", + "requires": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, "react": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", @@ -4396,6 +4436,16 @@ "scheduler": "^0.19.1" } }, + "react-i18next": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.6.0.tgz", + "integrity": "sha512-koyvoDgmY7y7vlbUOVWyoHahbBABfBse9X1vgYFw/WI+CfZwjumZ2/zQGYqLoMx6lEa0c9Lxr9GNP9L3HAJYUg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "html-parse-stringify2": "2.0.1" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5122,6 +5172,11 @@ "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", "dev": true }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -5187,6 +5242,11 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + }, "string-length": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", @@ -5748,6 +5808,12 @@ "extsprintf": "^1.2.0" } }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index 6f84051..82668c0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "https://github.com/AndcultureCode/AndcultureCode.JavaScript.React/issues" }, "dependencies": { - "andculturecode-javascript-core": "0.0.7", + "andculturecode-javascript-core": "0.1.0", "axios": "0.19.2", "immutable": "4.0.0-rc.12", "react": "16.13.1", @@ -19,6 +19,7 @@ "devDependencies": { "@testing-library/jest-dom": "5.5.0", "@testing-library/react": "10.0.4", + "@types/faker": "4.1.12", "@types/jest": "25.1.5", "@types/node": "13.11.0", "@types/react": "16.9.26", @@ -26,17 +27,20 @@ "@types/react-router-dom": "5.1.5", "@types/rosie": "0.0.37", "andculturecode-javascript-testing": "0.0.2", + "faker": "4.1.0", + "i18next": "19.4.5", "jest": "25.5.4", "jest-extended": "0.11.5", "jest-fetch-mock": "3.0.3", "prettier": "1.19.1", + "react-i18next": "11.6.0", "rimraf": "2.6.2", "rosie": "2.0.1", "ts-jest": "25.5.1", "tslint": "6.1.2", "tslint-config-prettier": "1.18.0", - "typedoc": "^0.17.6", - "typedoc-plugin-markdown": "^2.2.17", + "typedoc": "0.17.6", + "typedoc-plugin-markdown": "2.2.17", "typescript": "3.8.3" }, "files": [ @@ -69,6 +73,7 @@ "prepublishOnly": "npm run build", "test": "jest ./src", "watch": "npm run build -- --watch", + "watch:coverage": "jest ./src --coverage --watch", "watch:test": "jest ./src --watch" }, "types": "dist/index", diff --git a/src/hooks/use-localization.test.tsx b/src/hooks/use-localization.test.tsx new file mode 100644 index 0000000..19e4675 --- /dev/null +++ b/src/hooks/use-localization.test.tsx @@ -0,0 +1,80 @@ +import React, { useEffect } from "react"; +import "jest-extended"; +import faker from "faker"; +import { + BaseEnglishUnitedStates, + Culture, + LocalizationUtils, +} from "andculturecode-javascript-core"; +import { useLocalization } from "./use-localization"; +import { render, wait, waitFor } from "@testing-library/react"; +import { initReactI18next } from "react-i18next"; + +describe("useLocalization", () => { + test("when invalid key, returns key", async () => { + // Arrange + const expectedKey = faker.random.word(); + const culture: Partial> = { resources: {} }; + const EnglishUnitedStates = LocalizationUtils.cultureFactory( + BaseEnglishUnitedStates, + culture + ); + + const TestComponent = () => { + const { t } = useLocalization(); + + return

{t(expectedKey)}

; + }; + + const TestApp = () => { + LocalizationUtils.initialize(initReactI18next, [ + EnglishUnitedStates, + ]); + + return ; + }; + + // Act + const { getByText } = render(); + + // Assert + await waitFor(() => { + expect(getByText(expectedKey)).toBeInTheDocument(); + }); + }); + + test("when valid key, returns translation", async () => { + // Arrange + const key = "testkey"; + const expectedValue = faker.random.words(); + const culture: Partial> = { resources: {} }; + culture.resources[key] = expectedValue; + + const EnglishUnitedStates = LocalizationUtils.cultureFactory( + BaseEnglishUnitedStates, + culture + ); + + const TestComponent = () => { + const { t } = useLocalization(); + + return

{t(key)}

; + }; + + const TestApp = () => { + LocalizationUtils.initialize(initReactI18next, [ + EnglishUnitedStates, + ]); + + return ; + }; + + // Act + const { getByText } = render(); + + // Assert + await waitFor(() => { + expect(getByText(expectedValue)).toBeInTheDocument(); + }); + }); +}); diff --git a/src/hooks/use-localization.ts b/src/hooks/use-localization.ts new file mode 100644 index 0000000..0d24288 --- /dev/null +++ b/src/hooks/use-localization.ts @@ -0,0 +1,18 @@ +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; + +/** + * Typed wrapper of i18n `useTranslation` hook + */ +const useLocalization = () => { + const { t } = useTranslation(); + + const translate = (key: K, options?: object) => + t(key as string, options); + + return { + t: useCallback(translate, [t]), + }; +}; + +export { useLocalization }; diff --git a/src/index.ts b/src/index.ts index a91797f..b22f32c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,16 @@ export { } from "./components/routing/nested-routes-by-property"; export { Redirects, RedirectsProps } from "./components/routing/redirects"; -//#endregion Components +// #endregion Components + +// ----------------------------------------------------------------------------------------- +// #region Hooks +// ----------------------------------------------------------------------------------------- + +export { useCancellablePromise } from "./hooks/use-cancellable-promise"; +export { useLocalization } from "./hooks/use-localization"; + +// #endregion // ----------------------------------------------------------------------------------------- // #region Interfaces @@ -30,7 +39,7 @@ export { RedirectDefinition } from "./interfaces/redirect-definition"; export { RouteDefinition } from "./interfaces/route-definition"; export { RouteMap } from "./interfaces/route-map"; -//#endregion Interfaces +// #endregion Interfaces // ----------------------------------------------------------------------------------------- // #region Services @@ -38,7 +47,7 @@ export { RouteMap } from "./interfaces/route-map"; export { ServiceFactory } from "./services/service-factory"; -//#endregion Services +// #endregion Services // ----------------------------------------------------------------------------------------- // #region Utilities @@ -46,7 +55,7 @@ export { ServiceFactory } from "./services/service-factory"; export { RouteUtils } from "./utilities/route-utils"; -//#endregion Utilities +// #endregion Utilities // ----------------------------------------------------------------------------------------- // #region Vendor @@ -56,8 +65,10 @@ export { RouteUtils } from "./utilities/route-utils"; // specific component or function for their own implemention alongside our library. export { generatePath, - Prompt, + match, + matchPath, MemoryRouter, + Prompt, RedirectProps, Redirect, RouteChildrenProps, @@ -65,17 +76,15 @@ export { RouteProps, Route, Router, + RouterChildContext, StaticRouter, - SwitchProps, Switch, - match, - matchPath, - withRouter, - RouterChildContext, + SwitchProps, useHistory, useLocation, useParams, useRouteMatch, + withRouter, } from "react-router-dom"; -//#endregion Vendor +// #endregion Vendor