From c0cbd7dd0b8b68a80d580ab697e1e3e9cad4bdd3 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Wed, 11 Dec 2024 12:52:10 -0500 Subject: [PATCH] Remove wonder-blocks-i18n --- .changeset/eleven-vans-chew.md | 2 + consistency-tests/tsconfig.json | 1 - .../__tests__/multi-select.test.tsx | 18 +- .../tsconfig-build.json | 1 - .../__tests__/field-heading.test.tsx | 46 - .../wonder-blocks-form/tsconfig-build.json | 1 - packages/wonder-blocks-i18n/.npmignore | 6 - packages/wonder-blocks-i18n/CHANGELOG.md | 137 --- packages/wonder-blocks-i18n/package.json | 26 - .../i18n-inline-markup.test.tsx.snap | 123 --- .../__tests__/i18n-inline-markup.test.tsx | 196 ---- .../__tests__/parse-simple-html.test.ts | 158 --- .../src/components/i18n-inline-markup.tsx | 200 ---- .../src/components/parse-simple-html.ts | 121 --- .../functions/__tests__/i18n-accents.test.ts | 115 --- .../functions/__tests__/i18n-boxes.test.ts | 66 -- .../__tests__/i18n-faketranslate.test.ts | 211 ---- .../functions/__tests__/i18n-store.test.ts | 147 --- .../src/functions/__tests__/i18n.test.ts | 913 ------------------ .../src/functions/__tests__/l10n.test.ts | 175 ---- .../src/functions/__tests__/locale.test.ts | 31 - .../functions/__tests__/plural-forms.test.ts | 161 --- .../src/functions/i18n-accents.ts | 105 -- .../src/functions/i18n-boxes.ts | 21 - .../src/functions/i18n-faketranslate.ts | 168 ---- .../src/functions/i18n-store.ts | 157 --- .../wonder-blocks-i18n/src/functions/i18n.ts | 262 ----- .../wonder-blocks-i18n/src/functions/l10n.ts | 43 - .../src/functions/locale.ts | 9 - .../src/functions/plural-forms.ts | 119 --- .../wonder-blocks-i18n/src/functions/types.ts | 21 - packages/wonder-blocks-i18n/src/index.ts | 16 - .../wonder-blocks-i18n/tsconfig-build.json | 9 - packages/wonder-blocks-i18n/types | 1 - tsconfig-build.json | 1 - 35 files changed, 14 insertions(+), 3773 deletions(-) create mode 100644 .changeset/eleven-vans-chew.md delete mode 100644 packages/wonder-blocks-i18n/.npmignore delete mode 100644 packages/wonder-blocks-i18n/CHANGELOG.md delete mode 100644 packages/wonder-blocks-i18n/package.json delete mode 100644 packages/wonder-blocks-i18n/src/components/__tests__/__snapshots__/i18n-inline-markup.test.tsx.snap delete mode 100644 packages/wonder-blocks-i18n/src/components/__tests__/i18n-inline-markup.test.tsx delete mode 100644 packages/wonder-blocks-i18n/src/components/__tests__/parse-simple-html.test.ts delete mode 100644 packages/wonder-blocks-i18n/src/components/i18n-inline-markup.tsx delete mode 100644 packages/wonder-blocks-i18n/src/components/parse-simple-html.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/__tests__/i18n-accents.test.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/__tests__/i18n-boxes.test.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/__tests__/i18n-faketranslate.test.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/__tests__/i18n-store.test.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/__tests__/i18n.test.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/__tests__/l10n.test.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/__tests__/locale.test.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/__tests__/plural-forms.test.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/i18n-accents.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/i18n-boxes.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/i18n-faketranslate.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/i18n-store.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/i18n.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/l10n.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/locale.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/plural-forms.ts delete mode 100644 packages/wonder-blocks-i18n/src/functions/types.ts delete mode 100644 packages/wonder-blocks-i18n/src/index.ts delete mode 100644 packages/wonder-blocks-i18n/tsconfig-build.json delete mode 120000 packages/wonder-blocks-i18n/types diff --git a/.changeset/eleven-vans-chew.md b/.changeset/eleven-vans-chew.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/eleven-vans-chew.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/consistency-tests/tsconfig.json b/consistency-tests/tsconfig.json index 9e67d7f288..ed58b5a0e1 100644 --- a/consistency-tests/tsconfig.json +++ b/consistency-tests/tsconfig.json @@ -12,7 +12,6 @@ {"path": "../packages/wonder-blocks-core/tsconfig-build.json"}, {"path": "../packages/wonder-blocks-dropdown/tsconfig-build.json"}, {"path": "../packages/wonder-blocks-form/tsconfig-build.json"}, - {"path": "../packages/wonder-blocks-i18n/tsconfig-build.json"}, {"path": "../packages/wonder-blocks-icon/tsconfig-build.json"}, {"path": "../packages/wonder-blocks-icon-button/tsconfig-build.json"}, {"path": "../packages/wonder-blocks-layout/tsconfig-build.json"}, diff --git a/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx b/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx index c02f2f0255..db5f735729 100644 --- a/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx +++ b/packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx @@ -14,8 +14,6 @@ import { UserEvent, } from "@testing-library/user-event"; -import {ngettext} from "@khanacademy/wonder-blocks-i18n"; - import {PropsFor} from "@khanacademy/wonder-blocks-core"; import OptionItem from "../option-item"; import MultiSelect from "../multi-select"; @@ -868,7 +866,9 @@ describe("MultiSelect", () => { const labels: Labels = { ...builtinLabels, someSelected: (numOptions: number): string => - ngettext("%(num)s planet", "%(num)s planets", numOptions), + numOptions <= 1 + ? `${numOptions} planet` + : `${numOptions} planets`, }; const {userEvent} = doRender( @@ -900,7 +900,9 @@ describe("MultiSelect", () => { const labels: Labels = { ...builtinLabels, someSelected: (numOptions: number): string => - ngettext("%(num)s planet", "%(num)s planets", numOptions), + numOptions <= 1 + ? `${numOptions} planet` + : `${numOptions} planets`, }; const {userEvent} = doRender( @@ -1512,7 +1514,9 @@ describe("MultiSelect", () => { const labels: Labels = { ...builtinLabels, someSelected: (numOptions: number): string => - ngettext("%(num)s school", "%(num)s schools", numOptions), + numOptions <= 1 + ? `${numOptions} school` + : `${numOptions} schools`, }; // Act @@ -1540,7 +1544,9 @@ describe("MultiSelect", () => { const labels: Labels = { ...builtinLabels, someSelected: (numOptions: number): string => - ngettext("%(num)s planet", "%(num)s planets", numOptions), + numOptions <= 1 + ? `${numOptions} planet` + : `${numOptions} planets`, }; const {container, userEvent} = doRender( diff --git a/packages/wonder-blocks-dropdown/tsconfig-build.json b/packages/wonder-blocks-dropdown/tsconfig-build.json index 7ccaec5d56..883d3dc0c3 100644 --- a/packages/wonder-blocks-dropdown/tsconfig-build.json +++ b/packages/wonder-blocks-dropdown/tsconfig-build.json @@ -9,7 +9,6 @@ {"path": "../wonder-blocks-cell/tsconfig-build.json"}, {"path": "../wonder-blocks-clickable/tsconfig-build.json"}, {"path": "../wonder-blocks-core/tsconfig-build.json"}, - {"path": "../wonder-blocks-i18n/tsconfig-build.json"}, {"path": "../wonder-blocks-icon/tsconfig-build.json"}, {"path": "../wonder-blocks-layout/tsconfig-build.json"}, {"path": "../wonder-blocks-modal/tsconfig-build.json"}, diff --git a/packages/wonder-blocks-form/src/components/__tests__/field-heading.test.tsx b/packages/wonder-blocks-form/src/components/__tests__/field-heading.test.tsx index 81804f0f77..34d201bd86 100644 --- a/packages/wonder-blocks-form/src/components/__tests__/field-heading.test.tsx +++ b/packages/wonder-blocks-form/src/components/__tests__/field-heading.test.tsx @@ -2,9 +2,6 @@ import * as React from "react"; import {render, screen} from "@testing-library/react"; import {StyleSheet} from "aphrodite"; -import {I18nInlineMarkup} from "@khanacademy/wonder-blocks-i18n"; -import {Body} from "@khanacademy/wonder-blocks-typography"; - import FieldHeading from "../field-heading"; import TextField from "../text-field"; @@ -179,47 +176,4 @@ describe("FieldHeading", () => { const fieldHeading = container.childNodes[0]; expect(fieldHeading).toHaveStyle("background: blue"); }); - - it("should render a LabelMedium when the 'label' prop is a I18nInlineMarkup", () => { - // Arrange - - // Act - render( - {}} />} - label={ - {s}}> - {"Test Hello, world!"} - - } - />, - ); - - // Assert - const label = screen.getByText("Hello, world!"); - // LabelMedium has a font-size of 16px - expect(label).toHaveStyle("font-size: 16px"); - }); - - it("should render a LabelSmall when the 'description' prop is a I18nInlineMarkup", () => { - // Arrange - - // Act - render( - {}} />} - label={Hello, world} - description={ - {s}}> - {"Test description"} - - } - />, - ); - - // Assert - const description = screen.getByText("description"); - // LabelSmall has a font-size of 16px - expect(description).toHaveStyle("font-size: 14px"); - }); }); diff --git a/packages/wonder-blocks-form/tsconfig-build.json b/packages/wonder-blocks-form/tsconfig-build.json index 214bf85fbc..56353969a3 100644 --- a/packages/wonder-blocks-form/tsconfig-build.json +++ b/packages/wonder-blocks-form/tsconfig-build.json @@ -9,7 +9,6 @@ {"path": "../wonder-blocks-button/tsconfig-build.json"}, {"path": "../wonder-blocks-clickable/tsconfig-build.json"}, {"path": "../wonder-blocks-core/tsconfig-build.json"}, - {"path": "../wonder-blocks-i18n/tsconfig-build.json"}, {"path": "../wonder-blocks-icon/tsconfig-build.json"}, {"path": "../wonder-blocks-layout/tsconfig-build.json"}, {"path": "../wonder-blocks-link/tsconfig-build.json"}, diff --git a/packages/wonder-blocks-i18n/.npmignore b/packages/wonder-blocks-i18n/.npmignore deleted file mode 100644 index 4d1100197c..0000000000 --- a/packages/wonder-blocks-i18n/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -*.tsbuildinfo -tsconfig-build.json -dist/**/__tests__ -dist/**/*.test.d.ts -types -src diff --git a/packages/wonder-blocks-i18n/CHANGELOG.md b/packages/wonder-blocks-i18n/CHANGELOG.md deleted file mode 100644 index 6893997b09..0000000000 --- a/packages/wonder-blocks-i18n/CHANGELOG.md +++ /dev/null @@ -1,137 +0,0 @@ -# @khanacademy/wonder-blocks-i18n - -## 4.0.0 - -### Major Changes - -- e6abdd17: Upgrade to React 18 - -## 3.1.4 - -### Patch Changes - -- 5af2e751: Fix a bug with pluralization for fallback untranslated languages. - -## 3.1.3 - -### Patch Changes - -- 5899cbe4: Fix ngettext not returning plural translations. - -## 3.1.2 - -### Patch Changes - -- d7330053: Fix plural forms configuration for Khmer locale - -## 3.1.1 - -### Patch Changes - -- 02a1b298: Make sure we don't package tsconfig and tsbuildinfo files - -## 3.1.0 - -### Minor Changes - -- fc7dc5e2: Add loadTranslations/clearTranslations methods to wonder-blocks-i18n. - -## 3.0.1 - -### Patch Changes - -- 559e82d5: Update to build tooling, generating smaller output - -## 3.0.0 - -### Major Changes - -- 215f9b8b: Custom num formatting for ngettext no longer supported. - -## 2.0.2 - -### Patch Changes - -- 31798cea: Update callback props to a ReactNode to be returned - -## 2.0.1 - -### Patch Changes - -- ccb6fe00: Miscellaneous TS type fixes -- d4c2b18c: Fix a variety of issues with Flow types generated by flowgen - -## 2.0.0 - -### Major Changes - -- 1ca4d7e3: Fix minor issue with generate Flow types (this is a major bump b/c I forgot to do one after doing the TS conversion) - -## 1.2.7 - -### Patch Changes - -- b5ba5568: Ensure that flow lib defs use React.ElementConfig<> isntead of JSX.LibraryManagedAttributes<> - -## 1.2.6 - -### Patch Changes - -- 92afa7d2: Fix export types for ngettext and \$\_ - -## 1.2.5 - -### Patch Changes - -- d816af08: Update build and test configs use TypeScript -- 3891f544: Update babel config to include plugins that Storybook needed -- 0d28bb1c: Configured TypeScript -- 3d05f764: Fix HOCs and other type errors -- c2ec4902: Update eslint configuration, fix lint -- 2983c05b: Include 'types' field in package.json -- 77ff6a66: Generate Flow types from TypeScript types -- ec8d4b7f: Fix miscellaneous TypeScript errors - -## 1.2.4 - -### Patch Changes - -- 91cb727c: Remove file extensions from imports -- 91cb727c: Include onError for all kinds of errors thrown in `I18nInlineMarkup` - -## 1.2.3 - -### Patch Changes - -- 49c041e5: Warn instead of throwing an execption if the use of I18nInlineMarkup isn't necessary - -## 1.2.2 - -### Patch Changes - -- 9a6641e2: Pass-through strings if the current locale is not a pseudo-lang - -## 1.2.1 - -### Patch Changes - -- 399394bb: Don't package 'react' in the dist bundles - -## 1.2.0 - -### Minor Changes - -- b2a6a248: Export I18nInlineMarkup from wonder-blocks-i18n package -- d05a3c4e: Add setLocale()/getLocale() methods so that mobile/webapp can set the current locale - -## 1.1.0 - -### Minor Changes - -- cb57833c: Export ngetpos so that it can be use by handlebars i18n functions in webapp - -## 1.0.0 - -### Major Changes - -- 66097df7: Create wonder-blocks-i18n package by copying i18n.js and deps from webapp diff --git a/packages/wonder-blocks-i18n/package.json b/packages/wonder-blocks-i18n/package.json deleted file mode 100644 index 30d3849504..0000000000 --- a/packages/wonder-blocks-i18n/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@khanacademy/wonder-blocks-i18n", - "version": "4.0.0", - "design": "v1", - "publishConfig": { - "access": "public" - }, - "description": "", - "main": "dist/index.js", - "module": "dist/es/index.js", - "types": "dist/index.d.ts", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.6" - }, - "peerDependencies": { - "react": "18.2.0" - }, - "devDependencies": { - "@khanacademy/wb-dev-build-settings": "^2.0.0" - } -} \ No newline at end of file diff --git a/packages/wonder-blocks-i18n/src/components/__tests__/__snapshots__/i18n-inline-markup.test.tsx.snap b/packages/wonder-blocks-i18n/src/components/__tests__/__snapshots__/i18n-inline-markup.test.tsx.snap deleted file mode 100644 index b8a00ca50c..0000000000 --- a/packages/wonder-blocks-i18n/src/components/__tests__/__snapshots__/i18n-inline-markup.test.tsx.snap +++ /dev/null @@ -1,123 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`I18nInlineMarkup ElementWrapper 1`] = ` -
- - -6°C, - - - - __ - - Sunny - - __ - - - - , Fells - - - - * - - like - - * - - - - : - - - - __ - - -12 - - __ - - - - , Wind: VR 5 km/h - -
-`; - -exports[`I18nInlineMarkup MultipleShallowSubstitution 1`] = ` -
- -6°C, - __ - - Sunny - - __ - , Fells - - * - - like - - * - - : - __ - - -12 - - __ - , Wind: VR 5 km/h -
-`; - -exports[`I18nInlineMarkup SingleShallowSubstitution 1`] = ` -
- -6°C, Sunny, Fells like: - [Underline: - - -12 - - ] - , Wind: VR 5 km/h -
-`; - -exports[`I18nInlineMarkup errors should call \`onError\` callback if set when an parse error is thrown 1`] = ` -
- Hello world! -
-`; - -exports[`I18nInlineMarkup errors should call \`onError\` callback if set when there's invalid markup 1`] = ` -
- This HTML is broken - An error occurred! - , but here is fine. -
-`; diff --git a/packages/wonder-blocks-i18n/src/components/__tests__/i18n-inline-markup.test.tsx b/packages/wonder-blocks-i18n/src/components/__tests__/i18n-inline-markup.test.tsx deleted file mode 100644 index 19f0d77658..0000000000 --- a/packages/wonder-blocks-i18n/src/components/__tests__/i18n-inline-markup.test.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import * as React from "react"; -import {render} from "@testing-library/react"; - -import * as ParseSimpleHTML from "../parse-simple-html"; -import {I18nInlineMarkup} from "../i18n-inline-markup"; -import * as i18n from "../../functions/i18n"; - -const SingleShallowSubstitution = (): React.ReactElement => ( - ( - - [Underline:{t}] - - )} - > - {i18n._("-6\u00b0C, Sunny, Fells like: -12, Wind: VR 5 km/h")} - -); - -const MultipleShallowSubstitution = (): React.ReactElement => ( - ( - - __{t}__ - - )} - i={(t: string) => ( - - *{t}* - - )} - > - {i18n._( - "-6\u00b0C, Sunny, Fells like: -12, Wind: VR 5 km/h", - )} - -); - -const ElementWrapper = (): React.ReactElement => ( - {t}} - u={(t: string) => ( - - __{t}__ - - )} - i={(t: string) => ( - - *{t}* - - )} - > - {i18n._( - "-6\u00b0C, Sunny, Fells like: -12, Wind: VR 5 km/h", - )} - -); - -describe("I18nInlineMarkup", () => { - test("SingleShallowSubstitution", () => { - const {container} = render(); - - expect(container).toMatchSnapshot(); - }); - - test("MultipleShallowSubstitution", () => { - const {container} = render(); - - expect(container).toMatchSnapshot(); - }); - - test("ElementWrapper", () => { - const {container} = render(); - - expect(container).toMatchSnapshot(); - }); - - describe("errors", () => { - beforeEach(() => { - // React calls console.error() whenever there's an exception - // thrown by a component. We mock console.error() to avoid - // error reports that we're expecting as part of these tests. - jest.spyOn(console, "error").mockImplementation(() => {}); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it("should throw an error if a render prop is missing", () => { - const action = () => - render( - - {"Hello world!"} - , - ); - - expect(action).toThrowErrorMatchingInlineSnapshot( - `"I18nInlineMarkup: missing render prop for b"`, - ); - }); - - it("should throw an error if `parseSimpleHTML()` throws", () => { - // Arrange - jest.spyOn( - ParseSimpleHTML, - "parseSimpleHTML", - ).mockImplementationOnce(() => { - throw new Error("foo"); - }); - - // Act - const action = () => - render( - // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call. - {value}}> - {"Hello world!"} - , - - // NOTE(somewhatabstract): This component uses its own - // custom error handling instead of relying on error - // boundaries. This seems to break the React error boundary - // stuff, at least when testing, so the `boundary` test - // harness adapter never gets the thrown error. However, - // we can use this `legacyRoot` setting to use synchronous - // rendering like before, and then the test works as-is. - // We probably should rework this stuff before they drop - // this feature. - {legacyRoot: true}, - ); - - // Assert - expect(action).toThrowErrorMatchingInlineSnapshot(`"foo"`); - }); - - it("should call `onError` callback if set when an parse error is thrown", () => { - // Arrange - jest.spyOn( - ParseSimpleHTML, - "parseSimpleHTML", - ).mockImplementationOnce(() => { - throw new Error("foo"); - }); - const onErrorSpy = jest.fn().mockReturnValue("Hello world!"); - - // Act - const {container} = render( - - {"Hello world!"} - , - ); - - // Assert - expect(onErrorSpy).toHaveBeenCalled(); - expect(container).toMatchSnapshot(); - }); - - it("should throw when there's invalid markup", () => { - // Arrange, Act - const action = () => - render( - // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call. -
{label}
}> - { - "This HTML is broken \u003cinvalid\u003einvalid\u003e innner \u003c/invalid\u003e, but here is fine." - } -
, - ); - - // Assert - expect(action).toThrowErrorMatchingInlineSnapshot( - `"I18nInlineMarkup: missing render prop for invalid"`, - ); - }); - - it("should call `onError` callback if set when there's invalid markup", () => { - // Arrange - const onErrorSpy = jest.fn().mockReturnValue("An error occurred!"); - - // Act - const {container} = render( - - { - "This HTML is broken \u003cinvalid\u003einvalid\u003e innner \u003c/invalid\u003e, but here is fine." - } - , - ); - - // Assert - expect(onErrorSpy).toHaveBeenCalledWith( - new Error("I18nInlineMarkup: missing render prop for invalid"), - ); - expect(container).toMatchSnapshot(); - }); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/components/__tests__/parse-simple-html.test.ts b/packages/wonder-blocks-i18n/src/components/__tests__/parse-simple-html.test.ts deleted file mode 100644 index 5d88404646..0000000000 --- a/packages/wonder-blocks-i18n/src/components/__tests__/parse-simple-html.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import {parseSimpleHTML} from "../parse-simple-html"; - -describe("parseSimpleHTML", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("Parses a self-closing tag", () => { - expect(parseSimpleHTML("Test: ")).toEqual([ - { - type: "text", - text: "Test: ", - }, - { - type: "tag", - tag: "myImg", - children: null, - }, - ]); - expect(parseSimpleHTML("Test: .")).toEqual([ - { - type: "text", - text: "Test: ", - }, - { - type: "tag", - tag: "myImg", - children: null, - }, - { - type: "text", - text: ".", - }, - ]); - }); - - it("Trims", () => { - expect(parseSimpleHTML(" Test: ")).toEqual([ - { - type: "text", - text: "Test: ", - }, - { - type: "tag", - tag: "myImg", - children: null, - }, - ]); - }); - - it("Parses simple inline markup", () => { - expect(parseSimpleHTML("Hello, world")).toEqual([ - { - type: "text", - text: "Hello, ", - }, - { - type: "tag", - tag: "b", - children: "world", - }, - ]); - - expect(parseSimpleHTML("help, me>")).toEqual([ - { - type: "tag", - tag: "blink", - children: "help", - }, - { - type: "text", - text: ", ", - }, - { - type: "tag", - tag: "b", - children: "me", - }, - { - type: "text", - text: ">", - }, - ]); - }); - - it("Parses unicode and numbers", () => { - expect(parseSimpleHTML("Olá, mundo")).toEqual([ - { - type: "text", - text: "Olá, ", - }, - { - type: "tag", - tag: "b123á", - children: "mundo", - }, - ]); - }); - - it("Does not accept attributes", () => { - const action1 = () => parseSimpleHTML("Text: "); - - expect(action1).toThrowErrorMatchingInlineSnapshot( - `"I18nInlineMarkup: expected a tag without attributes, but received: "`, - ); - - const action2 = () => - parseSimpleHTML("Text: x"); - expect(action2).toThrowErrorMatchingInlineSnapshot( - `"I18nInlineMarkup: expected a tag without attributes, but received: "`, - ); - }); - - it("Does not accept nesting", () => { - const action = () => - parseSimpleHTML("Text: "); - - expect(action).toThrowErrorMatchingInlineSnapshot( - `"I18nInlineMarkup: nested tags are not supported, but is nested underneath ."`, - ); - }); - - describe("Does warns on unnecessary use", () => { - it.each` - html - ${"Test"} - ${" Test "} - ${""} - `("$html", ({html}: any) => { - const warnSpy = jest - .spyOn(console, "warn") - .mockImplementation(() => {}); - - parseSimpleHTML(html); - expect(warnSpy).toHaveBeenCalledWith( - "Unnecessary use of I18nInlineMarkup.", - ); - }); - }); - - it("Parses tag enclosing all text", () => { - expect(parseSimpleHTML("Test")).toEqual([ - { - type: "tag", - tag: "hello", - children: "Test", - }, - ]); - }); - - it("should throw an error if it encounters and unexpected closing tag", () => { - const action = () => parseSimpleHTML("Test"); - - expect(action).toThrowErrorMatchingInlineSnapshot( - `"I18nInlineMarkup: expected closing tag , but got "`, - ); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/components/i18n-inline-markup.tsx b/packages/wonder-blocks-i18n/src/components/i18n-inline-markup.tsx deleted file mode 100644 index bebca06ffc..0000000000 --- a/packages/wonder-blocks-i18n/src/components/i18n-inline-markup.tsx +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Render some HTML, using functions passed in as props to render tags. - * - * ADR: https://docs.google.com/document/d/15EP6E_nxrEhgb9jlR3v7S93S22E3KAH_XMqN2gBqyUQ/edit - * - * Why? - * ==== - * - * Sometimes strings that you want to translate have links, words that are - * bolded, etc. - * - * Historically, developers have chopped up these strings into bits without - * HTML and stitched them back together manually. This occasionally worked. - * Other times, a bug prevented the strings from being translated. When they - * were translatable, sometimes we'd get translations with mismatched verb - * tenses or other grammatical errors. Developers sometimes gave up and - * resorted to using dangerouslySetInnerHTML, which isn't great either -- it - * gives volunteers the ability to add XSS and it doesn't allow us to use our - * fancy client-side links. - * - * With , things are better. You pass in a full translated - * string, with HTML tags. For every kind of tag in the child string, you also - * pass in a function which, given children, renders that kind of tag. - * - * For example, here's one way you could use to render a - * disclaimer about our terms and conditions and privacy policy. - * - * ``` - * {node}} - * link2={node => {node}} - * link3={node => {node}} - * > - * {i18n._( - "By existing, you agree to our terms and conditions" + - "and privacy policy. If you are a resident " + - "of the European Union, you have options.", - * )} - * - * ``` - * - * Use - * === - * - * accepts a single HTML-like string as children. This - * string must not have any HTML attributes or nested tags. You must pass in - * a render prop for every kind of tag in this string. - * - * Any numeric or named HTML codes will be escaped. That is, if children - * contains `—` or `—`, the text `—` or `—` will - * show up to the end user. Use unicode escape sequences (e.g., \u2014) - * instead. - * - * Here's a trivial use of I18nInlineMarkup: - * - * ``` - * {node}}> - * {i18n._("Some markup")} - * - * ``` - * - * Note that `b` doesn't need to actually return a `` tag. It could, for - * example, return a Wonder Blocks Label instead. - * - * You can the same kind of tag multiple times: - * - * ``` - * {node}}> - * {i18n._("Some markup that I wrote")} - * - * ``` - * - * Since attributes aren't supported, if you want to support different props - * on the same kind of component, use multiple tag names. See the first - * example in this comment. - */ - -import * as React from "react"; - -import {parseSimpleHTML} from "./parse-simple-html"; -import type {SimpleHtmlNode} from "./parse-simple-html"; - -type Props = { - // TODO(FEI-5019): This should be `[tag: string]: (content: string) => React.ReactElement` - // but TypeScript requires that the type of string indexers be compatible with all other - // properties in the type/interface. - [tag: string]: any; - - /** - * A translated string. - * - * TODO(joshuan): if we ever add a type for translated strings, replace - * "string" with that type. - */ - children: string; - /** - * A function which takes each top-level text or rendered tag, - * and returns an element that wraps it. - * - * `type` is "text" if the element is text, and a pseudotag, like - * "newline", or "cirlced-box" otherwise. - * - * We use this on the LOHP and marketing pages to provide a - * background around text when they are on top of an illustration. - * - * i is the index of the text or tag. - */ - elementWrapper?: ( - elem: React.ReactNode, - type: string, - i: number, - ) => React.ReactNode; - onError?: (e: Error) => React.ReactNode; -}; - -export class I18nInlineMarkup extends React.PureComponent { - /** - * If an error occurs, we either call the onError prop, or throw the - * error. - */ - handleError(error: Error): React.ReactNode { - const {onError} = this.props; - - if (onError) { - return onError(error); - } - - throw error; - } - - render(): React.ReactNode { - const {children, elementWrapper, ...restProps} = this.props; - const renderers: Record< - string, - (content: string) => React.ReactElement - > = restProps; - let tree: ReadonlyArray; - try { - tree = parseSimpleHTML(children); - } catch (e: any) { - return this.handleError(e); - } - const nodes: Array = tree.map((node, i) => { - if (node.type === "text") { - if (elementWrapper) { - return ( - - {elementWrapper(node.text, "text", i)} - - ); - } - return node.text; - } - if (node.type === "tag") { - const renderer = renderers[node.tag]; - if (!renderer) { - return this.handleError( - new Error( - `I18nInlineMarkup: missing render prop for ${node.tag}`, - ), - ); - } - if (elementWrapper) { - return ( - - {elementWrapper( - /** - * TODO(somewhatabstract, JIRA-XXXX): - * node.children can be null but renderer does - * not accept null. - */ - // @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'string | null' is not assignable to parameter of type 'string'. - renderer(node.children), - node.tag, - i, - )} - - ); - } - return ( - - { - /** - * TODO(somewhatabstract, JIRA-XXXX): - * node.children can be null but renderer does - * not accept null. - */ - // @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'string | null' is not assignable to parameter of type 'string'. - renderer(node.children) - } - - ); - } - - // istanbul ignore - return this.handleError(new Error("Unknown child type.")); - }); - return nodes; - } -} diff --git a/packages/wonder-blocks-i18n/src/components/parse-simple-html.ts b/packages/wonder-blocks-i18n/src/components/parse-simple-html.ts deleted file mode 100644 index 1d1a77e217..0000000000 --- a/packages/wonder-blocks-i18n/src/components/parse-simple-html.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Simple parser for a subset of HTML. - * - * This is an internal function intended to be used by I18nInlineMarkup. - */ - -export type Tag = { - type: "tag"; - tag: string; - children: string | null; -}; - -export type Text = { - type: "text"; - text: string; -}; - -export type SimpleHtmlNode = Tag | Text; - -export function parseSimpleHTML(html: string): ReadonlyArray { - // This is a regex that can capture the following kinds of things: - // - self-closing tags: (e.g., ) - // - non self-closing tags. This regex captures an opening tag, and the - // next tag (regardless of whether it is opening, closing, or - // self-closing). We make sure, later in this function, that the second - // tag is a closing tag of the same type as the opening tag. - // - text (i.e., parts of the string that aren't tags). - // - // NOTE: Don't move simpleHtmlRegex out of this function. Regexes are - // stateful when used with `exec`, so we need to recreate it every time - // parseSimpleHTML is called. - // - // TODO(joshuan): This would be a great opportunity for named captures. - // If those are ever implemented in browsers we care about, this should - // be rewritten to use them. - const simpleHtmlRegex = - /(<([^>^/]+)\s*\/>)|(<([^>]+)>([^<]*)<(\/?)([^>]+)>)|([^<]+)/gm; - // simpleHtmlRegex has numbered captures. The following are names for those - // captures. If you ever need to modify the above regex and need to - // figure out what the new capture numbers are, https://regex101.com - // must be helpful. - const SELF_CLOSE_NAME = 2; - const OPEN_NAME = 4; - const CHILDREN = 5; - const CLOSING_SLASH = 6; - const CLOSING_NAME = 7; - const TEXT = 8; - - html = html.trim(); - - const result = []; - - let match; - while ((match = simpleHtmlRegex.exec(html))) { - if (match[SELF_CLOSE_NAME] != null) { - const tag = match[SELF_CLOSE_NAME].trim(); - if (tag.includes(" ")) { - throw new Error( - "I18nInlineMarkup: expected a tag without " + - "attributes, but received: " + - `<${tag}/>`, - ); - } - result.push({ - type: "tag", - tag, - children: null, - }); - } else if (match[OPEN_NAME] != null) { - const tag = match[OPEN_NAME].trim(); - if (tag.includes(" ")) { - throw new Error( - "I18nInlineMarkup: expected a tag without " + - "attributes, but received: " + - `<${match[OPEN_NAME]}>`, - ); - } - - if (match[CLOSING_SLASH] !== "/") { - throw new Error( - `I18nInlineMarkup: nested tags are not ` + - `supported, but <${match[CLOSING_NAME]}> is nested underneath ` + - `<${tag}>.`, - ); - } - - const closingTag = match[CLOSING_NAME].trim(); - if (tag !== closingTag) { - throw new Error( - `I18nInlineMarkup: expected closing tag ` + - `, but got `, - ); - } - result.push({ - type: "tag", - tag, - children: match[CHILDREN], - }); - } else if (match[TEXT] != null) { - result.push({ - type: "text", - text: match[TEXT], - }); - } else { - throw new Error( - "I18nInlineMarkup: unknown error (maybe you have an extra '<')?", - ); - } - } - - if ( - result.length === 1 && - // A tag is allowed to wrap all NL text in the html. - (result[0].type === "text" || !result[0].children) - ) { - // eslint-disable-next-line no-console - console.warn("Unnecessary use of I18nInlineMarkup."); - } - // @ts-expect-error [FEI-5019] - TS2322 - Type '({ type: string; tag: string; children: null; } | { type: string; tag: string; children: string; } | { type: string; text: string; })[]' is not assignable to type 'readonly SimpleHtmlNode[]'. - return result; -} diff --git a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-accents.test.ts b/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-accents.test.ts deleted file mode 100644 index c9d27b7f2b..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-accents.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import Accents from "../i18n-accents"; - -describe("i18n-accents.js: Accents", () => { - describe("#constructor", () => { - it("should throw on a zero scaling factor", () => { - // Arrange - // Act - const underTest = () => new Accents(0); - - // Assert - expect(underTest).toThrow(); - }); - - it("should throw on a negative scaling factor", () => { - // Arrange - // Act - const underTest = () => new Accents(-1); - - // Assert - expect(underTest).toThrow(); - }); - }); - - describe("#translate", () => { - it("should return an empty string if passed undefined", () => { - // Arrange - const underTest = new Accents(); - - // Act - const result = underTest.translate(undefined as any); - - // Assert - expect(result).toEqual(""); - }); - - it("should return an empty string if passed an empty string", () => { - // Arrange - const underTest = new Accents(); - - // Act - const result = underTest.translate(""); - - // Assert - expect(result).toEqual(""); - }); - - it("should return predictable accented version of the input", () => { - // Arrange - const underTest = new Accents(); - const toTranslate = - "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ 0123456789 !@#$%^&*()_+"; - const expectation = - "áÁƀɃćĆďĎéÉḟḞĝĜĥĤíÎĵĴķĶĺĹḿḾńŃóÓṕṔʠɊŕŔśŚťŤúÚṽṼẃẂẍẌýÝźŹ 0123456789 !@#$%^&*()_+"; - - // Act - const result = underTest.translate(toTranslate); - - // Assert - expect(result).toEqual(expectation); - }); - - it("should vary substitution on character repetitions", () => { - // Arrange - const underTest = new Accents(); - const toTranslate = "hhEEllOO WWooLLrrDD!"; - const expectation = "ĥȟÉÈĺľÓÒ ẂẀóòĹĽŕřĎĐ!"; - - // Act - const result = underTest.translate(toTranslate); - - // Assert - expect(result).toEqual(expectation); - }); - - it("should translate the same string the same way on repeated calls", () => { - // Arrange - const underTest = new Accents(); - const toTranslate = "hhEEllOO WWooLLrrDD!"; - - // Act - const result1 = underTest.translate(toTranslate); - const result2 = underTest.translate(toTranslate); - - // Assert - expect(result2).toEqual(result1); - }); - - it("should repeat substitutions when they have been used once", () => { - // Arrange - const underTest = new Accents(); - const toTranslate = "x x x"; - const expectation = "ẍ ẋ ẍ"; - - // Act - const result = underTest.translate(toTranslate); - - // Assert - expect(result).toEqual(expectation); - }); - - it("should scale the translation based on the provided scaling factor", () => { - // Arrange - const underTest = new Accents(3); - const toTranslate = "hhEEllOO WWooLLrrDD!"; - const expectation = - "ĥĥĥȟȟȟÉÉÉÈÈÈĺĺĺľľľÓÓÓÒÒÒ ẂẂẂẀẀẀóóóòòòĹĹĹĽĽĽŕŕŕřřřĎĎĎĐĐĐ!"; - - // Act - const result = underTest.translate(toTranslate); - - // Assert - expect(result).toEqual(expectation); - }); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-boxes.test.ts b/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-boxes.test.ts deleted file mode 100644 index 386739336b..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-boxes.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import Boxes, {BoxChar} from "../i18n-boxes"; - -describe("i18n-boxes.js: Boxes", () => { - describe("#translate", () => { - it("takes undefined, returns empty string", () => { - // Arrange - const underTest = new Boxes(); - - // Act - const result = underTest.translate(undefined as any); - - // Assert - expect(result).toEqual(""); - }); - - it("takes empty string, returns empty string", () => { - // Arrange - const underTest = new Boxes(); - - // Act - const result = underTest.translate(""); - - // Assert - expect(result).toEqual(""); - }); - - it("takes an entity, returns box", () => { - // Arrange - const underTest = new Boxes(); - - // Act - const result = underTest.translate(" "); - - // Assert - expect(result).toEqual(BoxChar); - }); - - it("takes single line string, returns boxes for alphanumerics", () => { - // Arrange - const underTest = new Boxes(); - - // Act - const result = underTest.translate("abcde 12#$%%% _(#*$(#%("); - - // Assert - expect(result).toEqual("□□□□□ □□#$%%% □(#*$(#%("); - expect(result && result[0]).toEqual(BoxChar); - }); - - it("takes multiline string, returns boxes for alphanumerics", () => { - // Arrange - const underTest = new Boxes(); - - // Act - const result = underTest.translate( - "abcde 12#$%%% _(#*$(#%(\nthis is a test\nfor boxes.", - ); - - // Assert - expect(result).toEqual( - "□□□□□ □□#$%%% □(#*$(#%(\n□□□□ □□ □ □□□□\n□□□ □□□□□.", - ); - expect(result && result[0]).toEqual(BoxChar); - }); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-faketranslate.test.ts b/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-faketranslate.test.ts deleted file mode 100644 index 679bd61818..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-faketranslate.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import * as Locale from "../locale"; -import FakeTranslate, {Translators} from "../i18n-faketranslate"; - -import type {IProvideTranslation} from "../types"; - -describe("i18n-faketranslate", () => { - describe("Translators", () => { - it("has the entries we expect", () => { - // Arrange - const expectation = ["boxes", "accents"]; - - // Act - const result = Object.keys(Translators); - - // Assert - expect(result).toEqual(expectation); - }); - - it("each entry maps to a translator", () => { - // Arrange - const entries: Array<[string, IProvideTranslation]> = - Object.entries(Translators) as any; - - // Act - const result = entries.every( - ([key, translator]: [any, any]) => - typeof translator.translate === "function", - ); - - // Assert - expect(result).toBe(true); - }); - }); - - describe("FakeTranslate", () => { - it("should return the input when there is no specified locale", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "en"); - const underTest = new FakeTranslate(); - - // Act - const result = underTest.translate("Test"); - - // Assert - expect(result).toEqual("Test"); - }); - - describe("Use kaLocale as default", () => { - it("should return the input when there is no matching language", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "gubbins", - ); - const underTest = new FakeTranslate(); - - // Act - const result = underTest.translate("Test"); - - // Assert - expect(result).toEqual("Test"); - }); - - it("should return the result of translation when there is a match", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "boxes", - ); - const underTest = new FakeTranslate(); - const expectation = Translators["boxes"].translate("Test"); - - // Act - const result = underTest.translate("Test"); - - // Assert - expect(result).toEqual(expectation); - }); - - it("should not translate HTML element tags and attributes", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "boxes", - ); - - const underTest = new FakeTranslate(); - const expectation = '□□□□ □□□□'; - - // Act - const result = underTest.translate("Test Link"); - - // Assert - expect(result).toEqual(expectation); - }); - - it("should not translate code tag contents", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "boxes", - ); - - const underTest = new FakeTranslate(); - const expectation = "□□□□ this is code"; - - // Act - const result = underTest.translate( - "Test this is code", - ); - - // Assert - expect(result).toEqual(expectation); - }); - - it("should not translate pre tag contents", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "boxes", - ); - - const underTest = new FakeTranslate(); - const expectation = "□□□□
this is preformatted
"; - - // Act - const result = underTest.translate( - "Test
this is preformatted
", - ); - - // Assert - expect(result).toEqual(expectation); - }); - - it("should not translate variable substitutions", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "boxes", - ); - - const underTest = new FakeTranslate(); - const expectation = "□□□□ %(var)s"; - - // Act - const result = underTest.translate("Test %(var)s"); - - // Assert - expect(result).toEqual(expectation); - }); - - it("should not translate URLs", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "boxes", - ); - - const underTest = new FakeTranslate(); - const expectation = - "□□□□ http://example.com?lang=boxes&test=1 https://test.com"; - - // Act - const result = underTest.translate( - "Test http://example.com?lang=boxes&test=1 https://test.com", - ); - - // Assert - expect(result).toEqual(expectation); - }); - - it("should not add HTML entities", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "boxes", - ); - - const underTest = new FakeTranslate(); - const expectation = "□□□□ <>&"; - - // Act - const result = underTest.translate("Test <>&"); - - // Assert - expect(result).toEqual(expectation); - }); - }); - - it("should not call document.createElement() for real locales", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "es"); - const createElementSpy = jest.spyOn(document, "createElement"); - const underTest = new FakeTranslate(); - - // Act - // We use a Symbol to ensure that .translate() is a passthrough. - underTest.translate("hello, world"); - - // Assert - expect(createElementSpy).not.toHaveBeenCalled(); - }); - - it("should passthrough the arg for real locales", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "es"); - const underTest = new FakeTranslate(); - const arg = Symbol("Hello, world!"); - - // Act - // We use a Symbol to ensure that .translate() is a passthrough. - // @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'typeof arg' is not assignable to parameter of type 'string'. - const result = underTest.translate(arg); - - // Assert - expect(result).toBe(arg); - }); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-store.test.ts b/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-store.test.ts deleted file mode 100644 index 4e46e8464d..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n-store.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { - clearTranslations, - getPluralTranslation, - getSingularTranslation, - loadTranslations, -} from "../i18n-store"; -import {setLocale} from "../locale"; - -describe("getSingularTranslation", () => { - const TEST_LOCALE = "en-pt"; - - afterEach(() => { - clearTranslations(TEST_LOCALE); - }); - - it("should return the translated string", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test: "arrrr matey", - }); - setLocale(TEST_LOCALE); - - // Act - const result = getSingularTranslation("test"); - - // Assert - expect(result).toMatchInlineSnapshot(`"arrrr matey"`); - }); - - it("should return the original string if no translation found", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test2: "arrrr matey", - }); - setLocale(TEST_LOCALE); - - // Act - const result = getSingularTranslation("test"); - - // Assert - expect(result).toMatchInlineSnapshot(`"test"`); - }); - - it("should return the translated string even if it's plural", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test: ["arrrr matey", "arrrr mateys"], - }); - setLocale(TEST_LOCALE); - - // Act - const result = getSingularTranslation("test"); - - // Assert - expect(result).toMatchInlineSnapshot(`"arrrr matey"`); - }); - - it("should return a fake translation", () => { - // Arrange - setLocale("boxes"); - - // Act - const result = getSingularTranslation("test"); - - // Assert - expect(result).toMatchInlineSnapshot(`"□□□□"`); - }); -}); - -describe("getPluralTranslation", () => { - const TEST_LOCALE = "en-pt"; - - afterEach(() => { - clearTranslations(TEST_LOCALE); - }); - - it("should return the translated plural singular string", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test: "arrrr matey", - }); - setLocale(TEST_LOCALE); - - // Act - const result = getPluralTranslation( - { - lang: "en", - messages: ["test singular", "test plural"], - }, - 1, - ); - - // Assert - expect(result).toMatchInlineSnapshot(`"test singular"`); - }); - - it("should return the translated plural string", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test: "arrrr matey", - }); - setLocale(TEST_LOCALE); - - // Act - const result = getPluralTranslation( - { - lang: "en", - messages: ["test singular", "test plural"], - }, - 2, - ); - - // Assert - expect(result).toMatchInlineSnapshot(`"test plural"`); - }); - - it("should return the original string if no translation found", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test: "arrrr matey", - }); - setLocale(TEST_LOCALE); - - // Act - const result = getSingularTranslation("test2"); - - // Assert - expect(result).toMatchInlineSnapshot(`"test2"`); - }); - - it("should return a fake translation", () => { - // Arrange - setLocale("boxes"); - - // Act - const result = getPluralTranslation( - { - lang: "en", - messages: ["test singular", "test plural"], - }, - 1, - ); - - // Assert - expect(result).toMatchInlineSnapshot(`"□□□□ □□□□□□□□"`); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n.test.ts b/packages/wonder-blocks-i18n/src/functions/__tests__/i18n.test.ts deleted file mode 100644 index 5fe5bac4e7..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/__tests__/i18n.test.ts +++ /dev/null @@ -1,913 +0,0 @@ -import * as React from "react"; - -import * as Locale from "../locale"; -import * as FakeTranslate from "../i18n-faketranslate"; -import {_, $_, ngettext, doNotTranslate, doNotTranslateYet} from "../i18n"; -import {clearTranslations, loadTranslations} from "../i18n-store"; - -jest.mock("react", () => { - return { - __esModule: true, - Fragment: "", - createElement: jest.fn((element: any, props: any, ...children: any) => { - return { - props: { - ...props, - children, - }, - }; - }), - }; -}); - -describe("i18n", () => { - beforeEach(() => { - // "en" is the default locale so that's going to be our default - // mock for `getLocale()`. - jest.spyOn(Locale, "getLocale").mockImplementation(() => "en"); - jest.clearAllMocks(); - }); - - describe("FakeTranslate integration tests", () => { - beforeEach(() => { - jest.spyOn(Locale, "getLocale").mockImplementation(() => "boxes"); - }); - - it("_ should translate", () => { - // Arrange - const expectation = - FakeTranslate.Translators["boxes"].translate("Test"); - - // Act - const result = _("Test"); - - // Assert - expect(result).toEqual(expectation); - }); - - it("$_ should translate", () => { - // Arrange - const expectation = - FakeTranslate.Translators["boxes"].translate("Test"); - - // Act - const result = $_("Test"); - - // Assert - expect(result).toEqual(expectation); - }); - - it("ngettext should translate", () => { - // Arrange - const expectation = - FakeTranslate.Translators["boxes"].translate("Plural"); - - // Act - const result = ngettext("Singular", "Plural", 0); - - // Assert - expect(result).toEqual(expectation); - }); - - it("doNotTranslate should not translate", () => { - // Arrange - - // Act - const result = doNotTranslate("Test"); - - // Assert - expect(result).toEqual("Test"); - }); - - it("doNotTranslateYet should not translate", () => { - // Arrange - - // Act - const result = doNotTranslateYet("Test"); - - // Assert - expect(result).toEqual("Test"); - }); - }); - - describe("I18nStore integration tests", () => { - const TEST_LOCALE = "en-pt"; - - afterEach(() => { - clearTranslations(TEST_LOCALE); - }); - - beforeEach(() => { - jest.spyOn(Locale, "getLocale").mockImplementation( - () => TEST_LOCALE, - ); - }); - - it("_ should translate", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test: "arrrr matey", - }); - - // Act - const result = _("test"); - - // Assert - expect(result).toMatchInlineSnapshot(`"arrrr matey"`); - }); - - it("$_ should translate", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test: "arrrr matey", - }); - - // Act - const result = $_("test"); - - // Assert - expect(result).toMatchInlineSnapshot(`"arrrr matey"`); - }); - - it("ngettext should translate", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - Singular: ["arrrr matey", "arrrr mateys"], - }); - - // Act - const result = ngettext("Singular", "Plural", 0); - - // Assert - expect(result).toMatchInlineSnapshot(`"arrrr mateys"`); - }); - - it("ngettext should handle missing translations", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "ru"); - - // Act - const result = ngettext("Singular", "Plural", 0); - - // Assert - expect(result).toMatchInlineSnapshot(`"Plural"`); - }); - - it("doNotTranslate should not translate", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test: "arrrr matey", - }); - - // Act - const result = doNotTranslate("test"); - - // Assert - expect(result).toEqual("test"); - }); - - it("doNotTranslateYet should not translate", () => { - // Arrange - loadTranslations(TEST_LOCALE, { - test: "arrrr matey", - }); - - // Act - const result = doNotTranslateYet("test"); - - // Assert - expect(result).toEqual("test"); - }); - }); - - describe("# _", () => { - it("returns input when no translation nor substitutions", () => { - // Arrange - - // Act - const result = _("Test"); - - // Assert - expect(result).toEqual("Test"); - }); - - it("handles % outside of a substitution", () => { - // Arrange - - // Act - const result = _("Test %s"); - - // Assert - expect(result).toEqual("Test %s"); - }); - - it("returns input when using substituion syntax but no value to substitute,", () => { - // Arrange - - // Act - const result = _("Test %(name)s"); - - // Assert - expect(result).toEqual("Test %(name)s"); - }); - - it("with string substitution returns substituted value", () => { - // Arrange - - // Act - const result = _("Test %(str)s", {str: "string"}); - - // Assert - expect(result).toEqual("Test string"); - }); - - it("with number substitution returns substituted value", () => { - // Arrange - - // Act - const result = _("Test %(num)s", {num: 2}); - - // Assert - expect(result).toEqual("Test 2"); - }); - - it("returns singular when given a plural configuration object", () => { - // Arrange - - // Act - const result = _({lang: "en", messages: ["Singular", "Plural"]}); - - // Assert - expect(result).toEqual("Singular"); - }); - - it("calls our fake translation", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "boxes"); - - const spy = jest.spyOn( - FakeTranslate.Translators["boxes"], - "translate", - ); - - // Act - _("Test"); - - // Assert - expect(spy).toHaveBeenCalled(); - }); - }); - - describe("# $_", () => { - it("should return input when options are null", () => { - // Arrange - - // Act - const result = $_("Test", null); - - // Assert - expect(result).toEqual("Test"); - }); - - it("should return input when options are empty", () => { - // Arrange - - // Act - const result = $_("Test", {}); - - // Assert - expect(result).toEqual("Test"); - }); - - it("should return input with % when options are null", () => { - // Arrange - - // Act - const result = $_("Test %s", {}); - - // Assert - expect(result).toEqual("Test %s"); - }); - - it("should return input with % when options are empty", () => { - // Arrange - - // Act - const result = $_("Test %s", {}); - - // Assert - expect(result).toEqual("Test %s"); - }); - - it("should return array combining text with substitutions when substitutions given", () => { - // Arrange - - // Act - const result = $_("Test %(num)s %(str)s", {str: "string", num: 2}); - - // Assert - // @ts-expect-error: `props` isn't available on all ReactNodes variants. - expect(result?.props.children).toEqual([ - "Test ", - 2, - " ", - "string", - "", - ]); - }); - - it("should pass children as separate arguments, not an array", () => { - // Arrange - - // Act - $_("Test %(num)s %(str)s", {str: "string", num: 2}); - - // Assert - expect(React.createElement).toHaveBeenCalledWith( - "", - {}, - "Test ", - 2, - " ", - "string", - "", - ); - }); - - it("calls our fake translation", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "boxes"); - const spy = jest.spyOn( - FakeTranslate.Translators["boxes"], - "translate", - ); - - // Act - $_("Test %(num)s %(str)s", {str: "string", num: 2}); - - // Assert - expect(spy).toHaveBeenCalled(); - }); - - it("should pass-through markers that don't appear in options", () => { - // Arrange - - // Act - $_("Test %(num)s %(str)s", {num: 2}); - - // Assert - expect(React.createElement).toHaveBeenCalledWith( - "", - {}, - "Test ", - 2, - " ", - "%(str)s", - "", - ); - }); - - it("should handle reused markers", () => { - // Arrange - - // Act - $_("Test %(num)s %(num)s %(num)s", {num: 2}); - - // Assert - expect(React.createElement).toHaveBeenCalledWith( - "", - {}, - "Test ", - 2, - " ", - 2, - " ", - 2, - "", - ); - }); - }); - - describe("# ngettext", () => { - it("calls our fake translation", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "boxes"); - const spy = jest.spyOn( - FakeTranslate.Translators["boxes"], - "translate", - ); - - // Act - ngettext("Singular", "Plural", 0); - - // Assert - expect(spy).toHaveBeenCalled(); - }); - - describe("for default locale/en", () => { - describe("without substitutions", () => { - it("should return plural form for num 0", () => { - // Arrange - - // Act - const result = ngettext("Singular", "Plural", 0); - - // Assert - expect(result).toEqual("Plural"); - }); - - it("should return singular form for num 1", () => { - // Arrange - - // Act - const result = ngettext("Singular", "Plural", 1); - - // Assert - expect(result).toEqual("Singular"); - }); - - it("should return plural form for num 2 or more", () => { - // Arrange - - // Act - const result = ngettext("Singular", "Plural", 2); - - // Assert - expect(result).toEqual("Plural"); - }); - }); - - describe("with num substitution", () => { - it("should return plural form for num 0", () => { - // Arrange - - // Act - const result = ngettext( - "Singular %(num)s", - "Plural %(num)s", - 0, - ); - - // Assert - expect(result).toEqual("Plural 0"); - }); - - it("should return singular form for num 1", () => { - // Arrange - - // Act - const result = ngettext( - "Singular %(num)s", - "Plural %(num)s", - 1, - ); - - // Assert - expect(result).toEqual("Singular 1"); - }); - - it("should return plural form for num 2 or more", () => { - // Arrange - - // Act - const result = ngettext( - "Singular %(num)s", - "Plural %(num)s", - 2, - ); - - // Assert - expect(result).toEqual("Plural 2"); - }); - }); - - describe("with num substitution and non-en locale", () => { - it("should return plural form for num 0", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "fr-fr", - ); - const formatSpy = jest.spyOn(Intl, "NumberFormat"); - - // Act - ngettext("Singular %(num)s", "Plural %(num)s", 0); - - // Assert - expect(formatSpy).toHaveBeenCalledWith("fr-fr"); - }); - - it("should return singular form for num 1", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "fr-fr", - ); - const formatSpy = jest.spyOn(Intl, "NumberFormat"); - - // Act - ngettext("Singular %(num)s", "Plural %(num)s", 1); - - // Assert - expect(formatSpy).toHaveBeenCalledWith("fr-fr"); - }); - - it("should return plural form for num 2 or more", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation( - () => "fr-fr", - ); - const formatSpy = jest.spyOn(Intl, "NumberFormat"); - - // Act - ngettext("Singular %(num)s", "Plural %(num)s", 2000); - - // Assert - expect(formatSpy).toHaveBeenCalledWith("fr-fr"); - }); - }); - - describe("with num substitution and other substitutions", () => { - it("should return plural form for num 0", () => { - // Arrange - - // Act - const result = ngettext( - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural %(num)s", - 0, - {canIDigIt: "yes you can"}, - ); - - // Assert - expect(result).toEqual("yes you can Plural 0"); - }); - - it("should return singular form for num 1", () => { - // Arrange - - // Act - const result = ngettext( - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural %(num)s", - 1, - {canIDigIt: "yes you can"}, - ); - - // Assert - expect(result).toEqual("yes you can Singular 1"); - }); - - it("should return plural form for num 2 or more", () => { - // Arrange - - // Act - const result = ngettext( - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural %(num)s", - 2, - {canIDigIt: "yes you can"}, - ); - - // Assert - expect(result).toEqual("yes you can Plural 2"); - }); - }); - }); - - describe("no plurals locale (using zh-hans)", () => { - it("should return singular for 0", () => { - // Arrange - - // Act - const result = ngettext( - { - lang: "zh-hans", - messages: [ - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural %(num)s", - ], - }, - 0, - {canIDigIt: "yes you can"}, - ); - - // Assert - expect(result).toEqual("yes you can Singular 0"); - }); - - it("should return singular for 1", () => { - // Arrange - - // Act - const result = ngettext( - { - lang: "zh-hans", - messages: [ - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural %(num)s", - ], - }, - 1, - {canIDigIt: "yes you can"}, - ); - - // Assert - expect(result).toEqual("yes you can Singular 1"); - }); - - it("should return singular for 2", () => { - // Arrange - - // Act - const result = ngettext( - { - lang: "zh-hans", - messages: [ - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural %(num)s", - ], - }, - 2, - {canIDigIt: "yes you can"}, - ); - - // Assert - expect(result).toEqual("yes you can Singular 2"); - }); - }); - - describe("other no plurals locale (using km)", () => { - it("should return other for 0", () => { - // Arrange - - // Act - const result = ngettext( - { - lang: "km", - messages: ["Other"], - }, - 0, - ); - - // Assert - expect(result).toEqual("Other"); - }); - }); - - describe("multiple plurals local (using pl)", () => { - it("should return second plural form for 0", () => { - // Arrange - - // Act - const result = ngettext( - { - lang: "pl", - messages: [ - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural 1 %(num)s", - "%(canIDigIt)s Plural 2 %(num)s", - ], - }, - 0, - {canIDigIt: "yes you can"}, - ); - - // Assert - expect(result).toEqual("yes you can Plural 2 0"); - }); - - it("should return singular form for 1", () => { - // Arrange - - // Act - const result = ngettext( - { - lang: "pl", - messages: [ - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural 1 %(num)s", - "%(canIDigIt)s Plural 2 %(num)s", - ], - }, - 1, - {canIDigIt: "yes you can"}, - ); - - // Assert - expect(result).toEqual("yes you can Singular 1"); - }); - - it("should return first plural form for 2 through 4", () => { - // Arrange - const testPoints = [2, 3, 4]; - - // Act - const results = testPoints.map((n: any) => - ngettext( - { - lang: "pl", - messages: [ - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural 1 %(num)s", - "%(canIDigIt)s Plural 2 %(num)s", - ], - }, - n, - {canIDigIt: "yes you can"}, - ), - ); - - // Assert - expect(results).toEqual([ - "yes you can Plural 1 2", - "yes you can Plural 1 3", - "yes you can Plural 1 4", - ]); - }); - - it("should return second plural form for 5", () => { - // Arrange - - // Act - const result = ngettext( - { - lang: "pl", - messages: [ - "%(canIDigIt)s Singular %(num)s", - "%(canIDigIt)s Plural 1 %(num)s", - "%(canIDigIt)s Plural 2 %(num)s", - ], - }, - 5, - {canIDigIt: "yes you can"}, - ); - - // Assert - expect(result).toEqual("yes you can Plural 2 5"); - }); - }); - }); - - describe("# doNotTranslate", () => { - it("should not call our fake translation", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "boxes"); - const spy = jest.spyOn( - FakeTranslate.Translators["boxes"], - "translate", - ); - spy.mockReset(); - - // Act - doNotTranslate("Test"); - - // Assert - expect(spy).not.toHaveBeenCalled(); - }); - - it("returns input when no translation nor substitutions", () => { - // Arrange - - // Act - const result = doNotTranslate("Test"); - - // Assert - expect(result).toEqual("Test"); - }); - - it("handles % outside of a substitution", () => { - // Arrange - - // Act - const result = doNotTranslate("Test %s"); - - // Assert - expect(result).toEqual("Test %s"); - }); - - it("returns input when using substituion syntax but no value to substitute,", () => { - // Arrange - - // Act - const result = doNotTranslate("Test %(name)s"); - - // Assert - expect(result).toEqual("Test %(name)s"); - }); - - it("with string substitution returns substituted value", () => { - // Arrange - - // Act - const result = doNotTranslate("Test %(str)s", {str: "string"}); - - // Assert - expect(result).toEqual("Test string"); - }); - - it("with number substitution returns substituted value", () => { - // Arrange - - // Act - const result = doNotTranslate("Test %(num)s", {num: 2}); - - // Assert - expect(result).toEqual("Test 2"); - }); - - it("returns singular when given a plural configuration object", () => { - // Arrange - - // Act - const result = doNotTranslate({ - lang: "en", - messages: ["Singular", "Plural"], - }); - - // Assert - expect(result).toEqual("Singular"); - }); - }); - - describe("# doNotTranslateYet", () => { - it("should not call our fake translation", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "boxes"); - const spy = jest.spyOn( - FakeTranslate.Translators["boxes"], - "translate", - ); - spy.mockReset(); - - // Act - doNotTranslateYet("Test"); - - // Assert - expect(spy).not.toHaveBeenCalled(); - }); - - it("returns input when no translation nor substitutions", () => { - // Arrange - - // Act - const result = doNotTranslateYet("Test"); - - // Assert - expect(result).toEqual("Test"); - }); - - it("handles % outside of a substitution", () => { - // Arrange - - // Act - const result = doNotTranslateYet("Test %s"); - - // Assert - expect(result).toEqual("Test %s"); - }); - - it("returns input when using substituion syntax but no value to substitute,", () => { - // Arrange - - // Act - const result = doNotTranslateYet("Test %(name)s"); - - // Assert - expect(result).toEqual("Test %(name)s"); - }); - - it("with string substitution returns substituted value", () => { - // Arrange - - // Act - const result = doNotTranslateYet("Test %(str)s", { - str: "string", - }); - - // Assert - expect(result).toEqual("Test string"); - }); - - it("with number substitution returns substituted value", () => { - // Arrange - - // Act - const result = doNotTranslateYet("Test %(num)s", {num: 2}); - - // Assert - expect(result).toEqual("Test 2"); - }); - - it("returns singular when given a plural configuration object", () => { - // Arrange - - // Act - const result = doNotTranslateYet({ - lang: "en", - messages: ["Singular", "Plural"], - }); - - // Assert - expect(result).toEqual("Singular"); - }); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/functions/__tests__/l10n.test.ts b/packages/wonder-blocks-i18n/src/functions/__tests__/l10n.test.ts deleted file mode 100644 index a83c775452..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/__tests__/l10n.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import * as Locale from "../locale"; -import * as FakeTranslate from "../i18n-faketranslate"; -import {localeToFixed, getDecimalSeparator} from "../l10n"; - -describe("l10n", () => { - beforeEach(() => { - // "en" is the default locale so that's going to be our default - // mock for `getLocale()`. - jest.spyOn(Locale, "getLocale").mockImplementation(() => "en"); - jest.clearAllMocks(); - }); - - describe("# localeToFixed", () => { - it("should not call our fake translation", () => { - // Arrange - // Let's stub out FakeTranslate so we can detect it getting called. - const fakeTranslate = { - translate: jest.fn().mockReturnValue(""), - } as const; - jest.spyOn(FakeTranslate, "default").mockImplementation( - // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '() => { readonly translate: jest.Mock; }' is not assignable to parameter of type '() => FakeTranslate'. - () => fakeTranslate, - ); - - // Act - localeToFixed(4, 5); - - // Assert - expect(fakeTranslate.translate).not.toHaveBeenCalled(); - }); - - describe("default locale (en)", () => { - it("should round value up to the given decimal places", () => { - // Arrange - - // Act - const result = localeToFixed(4.337, 2); - - // Assert - expect(result).toEqual("4.34"); - }); - - it("should extend the value to the given decimal places", () => { - // Arrange - - // Act - const result = localeToFixed(4, 2); - - // Assert - expect(result).toEqual("4.00"); - }); - - it("should round the value down to the given decimal places", () => { - // Arrange - - // Act - const result = localeToFixed(0.0004, 2); - - // Assert - expect(result).toEqual("0.00"); - }); - - it("should round negative values and retain the sign", () => { - // Arrange - - // Act - const result = localeToFixed(-0.0004, 2); - - // Assert - expect(result).toEqual("-0.00"); - }); - - it("should round the value to a whole number when 0 decimal places", () => { - // Arrange - - // Act - const result = localeToFixed(0.0004, 0); - - // Assert - expect(result).toEqual("0"); - }); - - it("should round a negative to a whole number when 0 decimal places", () => { - // Arrange - - // Act - const result = localeToFixed(-0.0004, 0); - - // Assert - expect(result).toEqual("0"); - }); - }); - }); - - describe("# getDecimalSeparator", () => { - it("should handle 'en'", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "en"); - // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '() => { format: () => string; }' is not assignable to parameter of type '(locales?: string | string[] | undefined, options?: NumberFormatOptions | undefined) => NumberFormat'. - jest.spyOn(Intl, "NumberFormat").mockImplementation(() => { - return { - format: () => "1.1", - }; - }); - - // Act - const result = getDecimalSeparator(); - - // Assert - expect(result).toEqual("."); - }); - - it("should handle 'pl'", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "pl"); - // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '() => { format: () => string; }' is not assignable to parameter of type '(locales?: string | string[] | undefined, options?: NumberFormatOptions | undefined) => NumberFormat'. - jest.spyOn(Intl, "NumberFormat").mockImplementation(() => { - return { - format: () => "1,1", - }; - }); - - // Act - const result = getDecimalSeparator(); - - // Assert - expect(result).toEqual(","); - }); - - it("should handle 'ar'", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "ar"); - // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '() => { format: () => string; }' is not assignable to parameter of type '(locales?: string | string[] | undefined, options?: NumberFormatOptions | undefined) => NumberFormat'. - jest.spyOn(Intl, "NumberFormat").mockImplementation(() => { - return { - format: () => "١٫١", - }; - }); - - // Act - const result = getDecimalSeparator(); - - // Assert - expect(result).toEqual("٫"); - }); - - it("should handle 'fa-af'", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "fa-af"); - // @ts-expect-error [FEI-5019] - TS2345 - Argument of type '() => { format: () => string; }' is not assignable to parameter of type '(locales?: string | string[] | undefined, options?: NumberFormatOptions | undefined) => NumberFormat'. - jest.spyOn(Intl, "NumberFormat").mockImplementation(() => { - return { - format: () => "۱٫۱", - }; - }); - - // Act - const result = getDecimalSeparator(); - - // Assert - expect(result).toEqual("٫"); - }); - - it("should handle 'ka'", () => { - // Arrange - jest.spyOn(Locale, "getLocale").mockImplementation(() => "ka"); - - // Act - const result = getDecimalSeparator(); - - // Assert - expect(result).toEqual(","); - }); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/functions/__tests__/locale.test.ts b/packages/wonder-blocks-i18n/src/functions/__tests__/locale.test.ts deleted file mode 100644 index 611d3373cf..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/__tests__/locale.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {getLocale, setLocale} from "../locale"; - -describe("#getLocale/#setLocale", () => { - afterEach(() => { - // Reset locale to the default - setLocale("en"); - }); - - it("should return the default", () => { - const result = getLocale(); - - expect(result).toEqual("en"); - }); - - it("should return whatever setLocale() set it to", () => { - setLocale("es"); - - const result = getLocale(); - - expect(result).toEqual("es"); - }); - - it("should return whatever setLocale() set it to last", () => { - setLocale("es"); - setLocale("fr"); - - const result = getLocale(); - - expect(result).toEqual("fr"); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/functions/__tests__/plural-forms.test.ts b/packages/wonder-blocks-i18n/src/functions/__tests__/plural-forms.test.ts deleted file mode 100644 index f495432eef..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/__tests__/plural-forms.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import {allPluralForms, likeFrench} from "../plural-forms"; - -describe("allPluralForms", () => { - describe("likeFrench", () => { - test.each` - number | form - ${0} | ${false} - ${1} | ${false} - ${2} | ${true} - ${100} | ${true} - `("likeFrench($number) = $form", ({number, form}: any) => { - expect(likeFrench(number)).toEqual(form); - }); - }); - - describe("ar", () => { - test.each` - number | form - ${0} | ${0} - ${1} | ${1} - ${2} | ${2} - ${404} | ${3} - ${1259} | ${4} - ${501} | ${5} - `("allPluralForms.ar($number) = $form", ({number, form}: any) => { - expect(allPluralForms.ar(number)).toEqual(form); - }); - }); - - describe("cs", () => { - test.each` - number | form - ${1} | ${0} - ${3} | ${1} - ${0} | ${2} - ${5} | ${2} - `("allPluralForms.cs($number) = $form", ({number, form}: any) => { - expect(allPluralForms.cs(number)).toEqual(form); - }); - }); - - describe("hr", () => { - test.each` - number | form - ${21} | ${0} - ${22} | ${1} - ${20} | ${2} - ${25} | ${2} - `("allPluralForms.hr($number) = $form", ({number, form}: any) => { - expect(allPluralForms.hr(number)).toEqual(form); - }); - }); - - describe("lt", () => { - test.each` - number | form - ${21} | ${0} - ${22} | ${1} - ${25} | ${1} - ${20} | ${2} - `("allPluralForms.lt($number) = $form", ({number, form}: any) => { - expect(allPluralForms.lt(number)).toEqual(form); - }); - }); - - describe("lv", () => { - test.each` - number | form - ${0} | ${0} - ${21} | ${1} - ${31} | ${1} - ${11} | ${2} - ${111} | ${2} - `("allPluralForms.lv($number) = $form", ({number, form}: any) => { - expect(allPluralForms.lv(number)).toEqual(form); - }); - }); - - describe("pl", () => { - test.each` - number | form - ${1} | ${0} - ${3} | ${1} - ${23} | ${1} - ${33} | ${1} - ${13} | ${2} - `("allPluralForms.pl($number) = $form", ({number, form}: any) => { - expect(allPluralForms.pl(number)).toEqual(form); - }); - }); - - describe("ro", () => { - test.each` - number | form - ${1} | ${0} - ${16} | ${1} - ${216} | ${1} - ${21} | ${2} - ${221} | ${2} - `("allPluralForms.ro($number) = $form", ({number, form}: any) => { - expect(allPluralForms.ro(number)).toEqual(form); - }); - }); - - describe("ru", () => { - test.each` - number | form - ${1} | ${0} - ${101} | ${0} - ${3} | ${1} - ${23} | ${1} - ${13} | ${2} - ${11} | ${2} - `("allPluralForms.ru($number) = $form", ({number, form}: any) => { - expect(allPluralForms.ru(number)).toEqual(form); - }); - }); - - describe("sk", () => { - test.each` - number | form - ${1} | ${0} - ${3} | ${1} - ${5} | ${2} - ${10} | ${2} - ${100} | ${2} - `("allPluralForms.sk($number) = $form", ({number, form}: any) => { - expect(allPluralForms.sk(number)).toEqual(form); - }); - }); - - // NOTE: Serbian plurals are the same as Russian plurals - describe("sr", () => { - test.each` - number | form - ${1} | ${0} - ${101} | ${0} - ${3} | ${1} - ${23} | ${1} - ${13} | ${2} - ${11} | ${2} - `("allPluralForms.sr($number) = $form", ({number, form}: any) => { - expect(allPluralForms.sr(number)).toEqual(form); - }); - }); - - // NOTE: Ukranian plurals are the same as Russian plurals - describe("uk", () => { - test.each` - number | form - ${1} | ${0} - ${101} | ${0} - ${3} | ${1} - ${23} | ${1} - ${13} | ${2} - ${11} | ${2} - `("allPluralForms.uk($number) = $form", ({number, form}: any) => { - expect(allPluralForms.uk(number)).toEqual(form); - }); - }); -}); diff --git a/packages/wonder-blocks-i18n/src/functions/i18n-accents.ts b/packages/wonder-blocks-i18n/src/functions/i18n-accents.ts deleted file mode 100644 index d7f427dabd..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/i18n-accents.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type {IProvideTranslation} from "./types"; - -// This map provides a way to get an "accented" character for any of the -// 26 english alphabet characters in either upper or lower case. -// This map is sourced from Wikipedia pages on diacritics and specific letters -// (example URLs below): -// - https://en.wikipedia.org/wiki/Diacritic -// - https://en.wikipedia.org/wiki/W -const ACCENT_MAP = { - a: "áàăắặâấåäãąā", - A: "ÁÀĂẮẶÂẤÅÄÃĄĀ", - b: "ƀḃḅ", - B: "ɃḂḄ", - c: "ćĉčç", - C: "ĆĈČÇ", - d: "ďđḑ", - D: "ĎĐḐ", - e: "éèêềěëėęē", - E: "ÉÈÊỀĚËĖĘĒ", - f: "ḟ", - F: "Ḟ", - g: "ĝǧģ", - G: "ĜǦĢ", - h: "ĥȟħḥ", - H: "ĤȞĦḤ", - i: "íìîïįī", - I: "ÎÏÍÌĮĪ", - j: "ĵ", - J: "Ĵ", - k: "ķḱ", - K: "ĶḰ", - l: "ĺľłļḷ", - L: "ĹĽŁĻḶ", - m: "ḿṁṃm̃", - M: "ḾṀṂM̃", - n: "ńňñņŋ", - N: "ŃŇÑŅŊ", - o: "óòôöőõȯȱøōỏ", - O: "ÓÒÔÖŐÕȮȰØŌỏ", - p: "ṕṗᵽ", - P: "ṔṖⱣ", - q: "ʠ", - Q: "Ɋ", - r: "ŕřŗ", - R: "ŔŘŖ", - s: "śŝšș", - S: "ŚŜŠŞ", - t: "ťț", - T: "ŤŢ", - u: "úùŭûůüųűūư", - U: "ÚÙŬÛŮÜŲŰŪƯ", - v: "ṽṿ", - V: "ṼṾ", - w: "ẃẁŵẅ", - W: "ẂẀŴẄ", - y: "ý", - Y: "Ý", - x: "ẍẋ", - X: "ẌẊ", - z: "źžż", - Z: "ŹŽŻ", -} as const; - -// This regular expression matches the keys in our map. -const SubstitutionRegex = new RegExp( - `[${Object.keys(ACCENT_MAP).join("")}]`, - "g", -); - -export default class Accents implements IProvideTranslation { - _scaleFactor: number; - - constructor(scaleFactor = 1) { - if (scaleFactor < 1) { - throw new Error("Scaling factor must be 1 or greater."); - } - this._scaleFactor = scaleFactor; - } - - translate: (input: string) => string = (input: string): string => { - if (!input) { - return ""; - } - - const countMap: Record = {}; - const updateCount = (char: string) => { - const count = countMap[char] || 0; - countMap[char] = count + 1; - return count; - }; - - // We want to substitute each character in the input string with - // a corresponding accent character from the map. We also want to - // ensure this is entirely deterministic, so we vary the accents based - // on our intended string width and the repetition of the characters. - return input.replace(SubstitutionRegex, (substring) => { - // @ts-expect-error [FEI-5019] - TS7053 - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ readonly a: "áàăắặâấåäãąā"; readonly A: "ÁÀĂẮẶÂẤÅÄÃĄĀ"; readonly b: "ƀḃḅ"; readonly B: "ɃḂḄ"; readonly c: "ćĉčç"; readonly C: "ĆĈČÇ"; readonly d: "ďđḑ"; readonly D: "ĎĐḐ"; readonly e: "éèêềěëėęē"; ... 42 more ...; readonly Z: "ŹŽŻ"; }'. - const possibles = ACCENT_MAP[substring]; - const count = updateCount(substring); - return possibles[count % possibles.length].repeat( - this._scaleFactor, - ); - }); - }; -} diff --git a/packages/wonder-blocks-i18n/src/functions/i18n-boxes.ts b/packages/wonder-blocks-i18n/src/functions/i18n-boxes.ts deleted file mode 100644 index 3c51e80ed5..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/i18n-boxes.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type {IProvideTranslation} from "./types"; - -// c.f. http://www.alanflavell.org.uk/unicode/unidata25.html -// hollow (white) square; also try \u25a0 or \u25aa+b -export const BoxChar = "\u25a1"; - -const AlphaNumRegex = /\w/g; - -export default class Boxes implements IProvideTranslation { - translate(input: string): string { - if (!input) { - return ""; - } - - if (input.startsWith("&")) { - return BoxChar; - } - - return input.replace(AlphaNumRegex, BoxChar); - } -} diff --git a/packages/wonder-blocks-i18n/src/functions/i18n-faketranslate.ts b/packages/wonder-blocks-i18n/src/functions/i18n-faketranslate.ts deleted file mode 100644 index 2770048747..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/i18n-faketranslate.ts +++ /dev/null @@ -1,168 +0,0 @@ -import Accents from "./i18n-accents"; -import Boxes from "./i18n-boxes"; -import {getLocale} from "./locale"; - -import type {IProvideTranslation} from "./types"; - -type TranslatorMap = { - [key: string]: IProvideTranslation; -}; - -/** - * A map of language key to translator. - * - * This map is used by our i18n calls to look up client-side translations when - * a match does not otherwise exist. - */ -export const Translators: TranslatorMap = { - boxes: new Boxes(), - - // Our accents translation also makes the string twice as long. - // NOTE(jeff): If we want longer strings, change this number. - accents: new Accents(2), -}; - -/** - * A translator that performs all the work of looking up what translation to use - * and then uses it. - * - * @export - * @class FakeTranslate - * @implements {IProvideTranslation} - */ -export default class FakeTranslate implements IProvideTranslation { - get _translator(): IProvideTranslation | undefined { - // We look up our fake translator on the fly in case the kaLocale - // was changed. - return Translators[getLocale()]; - } - - _translateSegment(input: string): string { - // This method takes a string and returns an array. - // It looks for two things to separate text: - // 1. URLs - // 2. Python variable substitutions - const urlRegex = - /((http[s]?|ftp):\/\/)?([\w-]+\.)([\w-.]+)((\/[\w-]+)*)?\/?(#[\w-]*)?(\?[\w-]+(=[\w%"']+)?(&[\w-]+(=[\w%"']+)?)*)?/g; - const pythonSubstRegex = /%\([\w]+\)s/g; - - const tokenSearchRegex = new RegExp( - `${urlRegex.source}|${pythonSubstRegex.source}`, - "g", - ); - - const safeTranslate = (str: string) => - // We know that we have a translator at this point, so just ignore - // TypeScript. - // @ts-expect-error [FEI-5019] - TS2533 - Object is possibly 'null' or 'undefined'. - this._translator.translate(str); - - // The way it works. - // 1. Look for a thing - // 2. Take before the thing and lex against other things - // 3. Take after the thing and lex that for python vars - // 4. Flatten into a single array and return. - const subsegments: Array = []; - let lastMatchEndIndex = 0; - let match = tokenSearchRegex.exec(input); - while (match !== null) { - if (match.index !== lastMatchEndIndex) { - subsegments.push( - safeTranslate( - input.substring(lastMatchEndIndex, match.index), - ), - ); - } - subsegments.push(match[0]); - lastMatchEndIndex = match.index + match[0].length; - match = tokenSearchRegex.exec(input); - } - - if (lastMatchEndIndex < input.length) { - subsegments.push(safeTranslate(input.substring(lastMatchEndIndex))); - } - - return subsegments.join(""); - } - - _parseAndTranslate(input: string): string { - if (!this._translator || input == null) { - // We're doing nothing if we don't have to. - return input; - } - - // Here we parse the input text into chunks. Some chunks need - // translating and some do not. - - // The input is chunked to cater for embedded HTML tags and variable - // subsitution syntax. Tags and variables are left alone, with the - // surrounding text being translated using our fake translation. - - // This is based off the more complex work done in the backend - // fake_translate.py. However, that handles far more scenarios as - // a lot more content passes through that system. We're fortunate here - // in that we only need consider the thing things that might get passed - // in frontend code, so we can be a little more general. - - // The things that we are specifically looking for are: - // 1. HTML tags - // 2. Python-style variable substitutions like %(str)s - // 3. URLs - - // So, first, let's get the content as an array of HTML and text nodes. - // We're leveraging the dom for this since it already knows about HTML. - const stringToHTMLElements = (htmlString: string) => { - const template = document.createElement("template"); - template.innerHTML = htmlString; - return template; - }; - - const html = stringToHTMLElements(input); - - // Now we can go through each one and translate the text bits. - // This is a recursive function to cater for situations such as: - // - // Some bold and not bold link text - const processChildNodes = (parent: Node | DocumentFragment): void => { - for (const node of parent.childNodes) { - if (node.nodeType === Node.TEXT_NODE) { - // Something to translate! - // @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'string | null' is not assignable to parameter of type 'string'. - const newText = this._translateSegment(node.textContent); - if (newText != null) { - const newTextNode = document.createTextNode(newText); - parent.replaceChild(newTextNode, node); - } - } else { - switch (node.nodeName) { - case "CODE": - case "PRE": - // Don't want to translate these tags. - break; - - default: - // Recurse into everything else. - processChildNodes(node); - break; - } - } - } - }; - - processChildNodes(html.content); - - // Because we used the parsing powers of the DOM, any special characters - // will have been encoded as entities. We want to strip those, so we do - // that here. - const tempNode = document.createElement("template"); - // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call. - const entitiesDecoded = html.innerHTML.replace(/&(\w+);/g, (match) => { - tempNode.innerHTML = match; - return tempNode.content.textContent; - }); - return entitiesDecoded; - } - - translate: (input: string) => string = (input: string): string => - this._parseAndTranslate(input); -} diff --git a/packages/wonder-blocks-i18n/src/functions/i18n-store.ts b/packages/wonder-blocks-i18n/src/functions/i18n-store.ts deleted file mode 100644 index 159785a961..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/i18n-store.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Functions for storing and retrieving translations. - * - * The i18n store is a simple cache that stores translations for a given locale. - */ -import FakeTranslate from "./i18n-faketranslate"; -import {getLocale} from "./locale"; -import {allPluralForms} from "./plural-forms"; -import {PluralConfigurationObject} from "./types"; - -type Language = keyof typeof allPluralForms; - -// The cache of strings that have been translated, by locale. -const localeMessageStore = new Map< - string, - // Singular strings are stored as a string, plural strings are stored as - // an arrays of strings. - Record> ->(); - -// Create a fake translate object to use if we can't find a translation. -const {translate: fakeTranslate} = new FakeTranslate(); - -/* - * Return the ngettext position that matches the given number and lang. - * - * Arguments: - * - num: The number upon which to toggle the plural forms. - * - lang: The language to use as the basis for the pluralization. - */ -export const ngetpos = function (num: number, lang?: Language): number { - const pluralForm = (lang && allPluralForms[lang]) || allPluralForms["en"]; - const pos = pluralForm(num); - // Map true to 1 and false to 0, keep any numeric return value the same. - return pos === true ? 1 : pos ? pos : 0; -}; - -/** - * Get the translation for a given id and locale. - * - * @param id the id of the string to translate - * @param locale the locale to translate to - * @returns the translated string, or null if no translation is found - */ -const getTranslationFromStore = (id: string, locale: string) => { - // See if we have a cache for the locale. - const messageCache = localeMessageStore.get(locale); - - if (!messageCache) { - return null; - } - - // See if we have a cached message for the id. - const cachedMessage = messageCache[id]; - - if (!cachedMessage) { - return null; - } - - // We found the translated string (or strings) so we can return it. - return cachedMessage; -}; - -/** - * Get the translation for a given message. If no translation is found, attempt - * to get the translation using FakeTranslate. If that fails, return the message. - * - * @param strOrPluralConfig the id of the string to translate, or a plural - * configuration object - * @returns the translated string - */ -export const getSingularTranslation = ( - strOrPluralConfig: string | PluralConfigurationObject, -) => { - // Sometimes we're given an argument that's meant for ngettext(). This - // happens if the same string is used in both i18n._() and i18n.ngettext() - // (.g. a = i18n._(foo); b = i18n.ngettext("foo", "bar", count); - // In such cases, only the plural form ends up in the .po file, and - // then it gets sent to us for the i18n._() case too. No problem, though: - // we'll just take the singular arg. - const id = - typeof strOrPluralConfig === "string" - ? strOrPluralConfig - : strOrPluralConfig.messages[0]; - - // We try to find the translation in the cache. - const message = getTranslationFromStore(id, getLocale()); - - // We found the translation so we can return it. - // We need to make sure that we only return the first message, in the case - // where there is a plural form for the same message. - if (message) { - return Array.isArray(message) ? message[0] : message; - } - - // Otherwise, there's no translation, so we try to do fake translation. - return fakeTranslate(id); -}; - -/** - * Get the plural translation for a given plural configuration object. - * - * @param pluralConfig the plural configuration object - * @param idx the index of the plural form to use - * @returns the translated string - */ -export const getPluralTranslation = ( - pluralConfig: PluralConfigurationObject, - num: number, -) => { - const {lang, messages} = pluralConfig; - - // We try to find the translation in the cache. - const translatedMessages = getTranslationFromStore( - messages[0], - getLocale(), - ); - - // We found the translation so we can return the right plural form. - if (translatedMessages) { - if (!Array.isArray(translatedMessages)) { - // NOTE(john): This should never happen, but we should handle it - // just in case. - return translatedMessages; - } - // Get the translated string - const idx = ngetpos(num, getLocale()); - return translatedMessages[idx]; - } - - // Otherwise, there's no translation, so we try to do fake translation. - const idx = ngetpos(num, lang); - return fakeTranslate(messages[idx]); -}; - -/** - * Load locale data into the cache. - * - * @param locale the locale to load data for - * @param data the id-message pairs to load - */ -export const loadTranslations = ( - locale: string, - data: Record>, -) => { - const messageCache = localeMessageStore.get(locale); - localeMessageStore.set(locale, {...messageCache, ...data}); -}; - -/** - * Clear locale data from the cache. - * - * @param locale the locale to clear data for - */ -export const clearTranslations = (locale: string) => { - localeMessageStore.delete(locale); -}; diff --git a/packages/wonder-blocks-i18n/src/functions/i18n.ts b/packages/wonder-blocks-i18n/src/functions/i18n.ts deleted file mode 100644 index cb2aafe642..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/i18n.ts +++ /dev/null @@ -1,262 +0,0 @@ -/* eslint-disable @babel/no-invalid-this */ -/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */ -/* To fix, remove an entry above, run ka-lint, and fix errors. */ -import * as React from "react"; - -import {getLocale} from "./locale"; -import {PluralConfigurationObject} from "./types"; -import {getPluralTranslation, getSingularTranslation} from "./i18n-store"; - -type InterpolationOptions = { - [key: string]: T; -}; - -type NGetOptions = { - [key: string]: any; -}; - -interface ngettextOverloads { - ( - config: PluralConfigurationObject, - num?: number | null | undefined, - options?: NGetOptions, - ): string; - ( - singular: string, - plural: string, - num?: number | null | undefined, - options?: NGetOptions, - ): string; -} - -interface _Overloads { - ( - str: string, - options?: - | InterpolationOptions - | null - | undefined, - ): string; - ( - pluralConfig: PluralConfigurationObject, - options?: - | InterpolationOptions - | null - | undefined, - ): string; -} - -const interpolationMarker = /%\(([\w_]+)\)s/g; - -/** - * Performs sprintf-like %(name)s replacement on str, and returns a React - * fragment of the string interleaved with those replacements. The replacements - * can be any valid React node including strings and numbers. - * - * For example: - * interpolateStringToFragment("test", {}) -> - * test - * interpolateStringToFragment("test %(num)s", {num: 5}) -> - * test 5 - * interpolateStringToFragment("test %(num)s", {num: }) -> - * test - */ -const interpolateStringToFragment = function ( - str: string, - options?: InterpolationOptions | null, -): React.ReactNode { - options = options || {}; - - // Split the string into its language fragments and substitutions - const split = getSingularTranslation(str).split(interpolationMarker); - - const result: { - [key: string]: React.ReactNode; - } = {text_0: split[0]}; - - // Replace the substitutions with the appropriate option - for (let i = 1; i < split.length; i += 2) { - const key = split[i]; - let replaceWith = options[key]; - if (replaceWith === undefined) { - replaceWith = `%(${key})s`; - } - - // We prefix each substitution key with a number that increments each - // time it's used, so "test %(num)s %(fruit)s and %(num)s again" turns - // into an object with keys: - // [text_0, 0_num, text_2, 0_fruit, text_4, 1_num, text_6] - // This is better than just using the array index in the case that we - // switch between two translated strings with the same variables. - // Admittedly, an edge case. - let j = 0; - while (`${j}_${key}` in result) { - j++; - } - result[`${j}_${key}`] = replaceWith; - // Because the regex has one capturing group, the `split` array always - // has an odd number of elements, so this always stays in bounds. - result[`text_${i + 1}`] = split[i + 1]; - } - - if (Object.keys(result).length === 1 && result.text_0) { - return result.text_0; - } - - // We have to cast the result to any here before then back to React.Node - // because Object.values is typed as returning Array and TypeScript - // is not happy about `unknown` being turned into React.ReactElement and a couple - // of other subtypes of React.ReactNode. However, we know this is an array of - // React.Node so rather than lose all typing and ignoring the error, we - // do a little casting to any and then to React.ReactNode. - const children: Array = Object.values( - result, - ) as Array; - - // NOTE: We need to use createElement here because - // {Object.values(result)} - // triggers the following error: - // Warning: Each child in an array or iterator should have a - // unique "key" prop. - return React.createElement(React.Fragment, {}, ...children); -}; - -/** - * Handle string interpolation (for plain strings, not React fragments). - */ -const interpolateString = ( - message: string, - options: - | InterpolationOptions - | null - | undefined, -) => { - // Options are optional, if we don't have any, just return the string. - if (options == null) { - return message; - } - - // Otherwise, let's see if our string has anything to be replaced. - return message.replace(interpolationMarker, (match, key) => { - const replaceWith = options[key]; - return replaceWith != null ? String(replaceWith) : match; - }); -}; - -/** - * Simple i18n method with sprintf-like %(name)s replacement - * To be used like so: - * i18n._("Some string") - * i18n._("Hello %(name)s", {name: "John"}) - */ -export const _: _Overloads = (strOrPluralConfig, options) => { - const message = getSingularTranslation(strOrPluralConfig); - return interpolateString(message, options); -}; - -/** - * i18n method that supports sprintf-like %(name)s replacement for React nodes - * as well as strings. To be used like so: - * i18n._( - * "Look at this flashing text!: %(node)s", - * {node: Ahh my eyes!} - * ) - */ -export const $_: ( - str: string, - options?: - | InterpolationOptions - | null - | undefined, -) => React.ReactNode = function (str, options) { - return interpolateStringToFragment(str, options); -}; - -/** - * Simple ngettext method with sprintf-like %(name)s replacement - * To be used like so: - * i18n.ngettext("Singular", "Plural", 3) - * i18n.ngettext("%(num)s Cat", "%(num)s Cats", 3) - * i18n.ngettext( - * "%(num)s %(type)s Cat", - * "%(num)s %(type)s Cats", - * 3, {type: "Calico"}) - * - * This method is also meant to be used when injecting for other - * non-English languages, like so (taking an array of plural messages, - * which varies based upon the language): - * i18n.ngettext({ - * lang: "ja", - * messages: ["%(num)s 猫 %(username)s"] - * }, 3, {username: "John"}); - * - * Note: the "singular variant" of a string isn't always associated with a - * quantity of 1 in all languages, so do not hardcode one into the variant. - * French uses it for 0 and 1. In Japanese, if the number of results is 100 - * (or any number) we'd be showing "1 結果" regardless of the number. - * In Russian, any number ending in 1 (e.g. 21, 61) uses singular form. - * You should instead still parameterize the number. - */ -export const ngettext: ngettextOverloads = ( - singular: string | PluralConfigurationObject, - plural?: string | number | null | undefined, - num?: number | null | undefined | NGetOptions, - options?: NGetOptions, -) => { - const pluralConfObj: PluralConfigurationObject = - typeof singular === "object" - ? singular - : { - lang: "en", - // We know plural is a string if singular is not a config object - messages: [singular, plural as any], - }; - - const actualNum: number = - typeof singular === "object" ? plural : (num as any); - const actualOptions: NGetOptions = - (typeof singular === "object" ? num : (options as any)) || {}; - - const translation = getPluralTranslation(pluralConfObj, actualNum); - - // Get the options to substitute into the string. - // We automatically add in the 'magic' option-variable 'num'. - actualOptions.num = formatNumber(actualNum); - - // Then do the actual substitution - return interpolateString(translation, actualOptions); -}; - -/** - * Format a number to a string using the current locale. - * This is a thin wrapper around Intl.NumberFormat. - * - * @param num the number to format to a string - * @returns a formatted number string - */ -const formatNumber = (num: number): string => { - return Intl.NumberFormat(getLocale()).format(num); -}; - -/* - * A dummy identity function. It's used as a signal to automatic - * translation-identification tools that they shouldn't mark this - * text up to be translated, even though it looks like - * natural-language text. (And likewise, a signal to linters that - * they shouldn't complain that this text isn't translated.) - * Use it like so: 'tag.author = i18n.doNotTranslate("Jim");' - */ -export const doNotTranslate: _Overloads = (strOrPluralConfig, options) => { - const id = - typeof strOrPluralConfig === "string" - ? strOrPluralConfig - : strOrPluralConfig.messages[0]; - return interpolateString(id, options); -}; - -/* - * A dummy identity function, like i18n.doNotTranslate. It's used to - * represent strings that may undergo revisions and should not be - * translated yet. - */ -export const doNotTranslateYet: _Overloads = doNotTranslate; diff --git a/packages/wonder-blocks-i18n/src/functions/l10n.ts b/packages/wonder-blocks-i18n/src/functions/l10n.ts deleted file mode 100644 index fb4f84f59f..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/l10n.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {getLocale} from "./locale"; - -/** - * Rounds num to X places, and uses the proper decimal seperator. - * But does *not* insert thousands separators. - */ -export const localeToFixed = function (num: number, places: number): string { - const decimalSeperator = getDecimalSeparator(); - let langFixed = num.toFixed(places).replace(".", decimalSeperator); - if (langFixed === "-0") { - langFixed = "0"; - } - return langFixed; -}; - -/** - * Get the character used for separating decimals. - */ -export const getDecimalSeparator = (): string => { - const locale = getLocale(); - - switch (locale) { - // TODO(somewhatabstract): Remove this when Chrome supports the `ka` - // locale properly. - // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201 - // - // Supported locales in Chrome: - // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list - case "ka": - return ","; - - default: - const numberWithDecimalSeparator = 1.1; - // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to - // support Safari 12. - const match = new Intl.NumberFormat(getLocale()) - .format(numberWithDecimalSeparator) - // 0x661 is ARABIC-INDIC DIGIT ONE - // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE - .match(/[^\d\u0661\u06F1]/); - return match?.[0] ?? "."; - } -}; diff --git a/packages/wonder-blocks-i18n/src/functions/locale.ts b/packages/wonder-blocks-i18n/src/functions/locale.ts deleted file mode 100644 index 0e4f8d40d4..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/locale.ts +++ /dev/null @@ -1,9 +0,0 @@ -let __locale = "en"; // We default to English if no locale has been set - -export const setLocale = (locale: string): void => { - __locale = locale; -}; - -export const getLocale = (): string => { - return __locale; -}; diff --git a/packages/wonder-blocks-i18n/src/functions/plural-forms.ts b/packages/wonder-blocks-i18n/src/functions/plural-forms.ts deleted file mode 100644 index 2f04e573bd..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/plural-forms.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint-disable eqeqeq */ - -type PluralFormsMap = { - [key: string]: (count: number) => boolean | number; -}; - -// Pluralization rule should be `likeEnglish` unless specified by -// someone on the language's advocate team in the Jira ticket. - -export const likeEnglish = (n: number): boolean => n != 1; -export const likeFrench = (n: number): boolean => n > 1; -export const likeJapanese = (n: number): number => 0; - -// If you need to add a new locale, find the Crowdin locale code and assign it -// to a shell variable `CROWDIN_LOCALE`, and run the following: -// -// gsutil cat $( -// gsutil ls "gs://ka_translations_archive/$CROWDIN_LOCALE/*.tar.gz" \ -// | grep -v pofiles \ -// | tail -n 1 -// ) \ -// | tar xzf - -O 1_high_priority_platform/about.donate.po \ -// | grep '"Plural-Forms:' -// -// This grabs a tar/gzip file with the most recent set of translation files -// we've downloaded from Crowdin, and opens one of them and looks for an -// annotation that describes the plural rule in a `gettext`-specific format. -// If it prints exactly the following, use `likeEnglish` below. -// -// "Plural-Forms: nplurals=2; plural=(n != 1);\n" -// -// If it prints anything else, use the `plural` expression, which should -// hopefully be usable in JavaScript. -// -// TODO(csilvers): auto-generate this list instead. -// -// NOTE(mdr): Disable Prettier for this object, because intl_js_test.py -// expects it to be in a particular format. -// prettier-ignore -export const allPluralForms: PluralFormsMap = { - "accents": likeEnglish, // a 'fake' language - "af": likeEnglish, - "ar": (n) => n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5, - "as": likeEnglish, - "az": likeEnglish, - "bg": likeEnglish, - "bn": likeEnglish, - "boxes": likeEnglish, // a 'fake' language - "ca": likeEnglish, - "cs": (n) => (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2, - "da": likeEnglish, - "de": likeEnglish, - "el": likeEnglish, - "empty": likeEnglish, // a 'fake' language - "en": likeEnglish, - "en-pt": likeEnglish, // a 'fake' language, used by crowdin for JIPT - "es": likeEnglish, - "et": likeEnglish, - "fa": likeJapanese, - "fa-af": likeJapanese, - "fi": likeEnglish, - "fil": likeFrench, - "fr": likeFrench, - "fv": likeFrench, - "gu": likeEnglish, - "he": likeEnglish, - "hi": likeEnglish, - "hr": (n) => (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2), - "hu": likeEnglish, - "hy": likeEnglish, - "id": likeJapanese, - "is": likeEnglish, - "it": likeEnglish, - "ja": likeJapanese, - "ka": likeEnglish, - "kk": likeEnglish, - "km": likeJapanese, - "kn": likeEnglish, - "ko": likeJapanese, - "ky": likeJapanese, - "lol": likeEnglish, // a 'fake' language - "lt": (n) => (n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2), - "lv": (n) => (n==0 ? 0 : n%10==1 && n%100!=11 ? 1 : 2), - "mn": likeEnglish, - "mr": likeEnglish, - "ms": likeJapanese, - "my": likeJapanese, - "nb": likeEnglish, - "nl": likeEnglish, - "nn": likeEnglish, - "or": likeEnglish, - "pa": likeEnglish, - "pl": (n) => (n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2), - "pt": likeEnglish, - "pt-pt": likeEnglish, - "ro": (n) => (n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2), - "ru": (n) => n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2, - "rw": likeEnglish, - "sgn-us": likeEnglish, - "si": likeEnglish, - "si-LK": likeEnglish, - "sk": (n) => (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2, - "sr": (n) => (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2), - "sv": likeEnglish, - "sv-SE": likeEnglish, - "sw": likeEnglish, - "ta": likeEnglish, - "te": likeEnglish, - "th": likeJapanese, - "tr": likeJapanese, - "uk": (n) => (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2), - "ur": likeEnglish, - "uz": likeFrench, - "vi": likeJapanese, - "xh": likeEnglish, - "zh-hans": likeJapanese, - "zh-hant": likeJapanese, - "zu": likeEnglish, -}; diff --git a/packages/wonder-blocks-i18n/src/functions/types.ts b/packages/wonder-blocks-i18n/src/functions/types.ts deleted file mode 100644 index a30acfb0eb..0000000000 --- a/packages/wonder-blocks-i18n/src/functions/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Implementations provide the ability to translate strings to a different - * language. - * - * @export - * @interface IProvideTranslation - */ -export interface IProvideTranslation { - /** - * Translate the input string. - * - * @param {string} input The value to translate. - * @returns {string} The translated string or the original input value. - */ - translate(input: string): string; -} - -export type PluralConfigurationObject = { - lang: string; - messages: Array; -}; diff --git a/packages/wonder-blocks-i18n/src/index.ts b/packages/wonder-blocks-i18n/src/index.ts deleted file mode 100644 index 21349ac44e..0000000000 --- a/packages/wonder-blocks-i18n/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { - _, - $_, - ngettext, - doNotTranslate, - doNotTranslateYet, // used by handlebars translation functions in webapp -} from "./functions/i18n"; -export { - loadTranslations, - clearTranslations, - ngetpos, -} from "./functions/i18n-store"; - -export {localeToFixed, getDecimalSeparator} from "./functions/l10n"; -export {getLocale, setLocale} from "./functions/locale"; -export {I18nInlineMarkup} from "./components/i18n-inline-markup"; diff --git a/packages/wonder-blocks-i18n/tsconfig-build.json b/packages/wonder-blocks-i18n/tsconfig-build.json deleted file mode 100644 index 28f60567d1..0000000000 --- a/packages/wonder-blocks-i18n/tsconfig-build.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "exclude": ["dist"], - "extends": "../tsconfig-shared.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "src", - }, - "references": [] -} \ No newline at end of file diff --git a/packages/wonder-blocks-i18n/types b/packages/wonder-blocks-i18n/types deleted file mode 120000 index 8788aa2845..0000000000 --- a/packages/wonder-blocks-i18n/types +++ /dev/null @@ -1 +0,0 @@ -../../types \ No newline at end of file diff --git a/tsconfig-build.json b/tsconfig-build.json index 50d327c250..efc145fba2 100644 --- a/tsconfig-build.json +++ b/tsconfig-build.json @@ -17,7 +17,6 @@ {"path": "./packages/wonder-blocks-dropdown/tsconfig-build.json"}, {"path": "./packages/wonder-blocks-form/tsconfig-build.json"}, {"path": "./packages/wonder-blocks-grid/tsconfig-build.json"}, - {"path": "./packages/wonder-blocks-i18n/tsconfig-build.json"}, {"path": "./packages/wonder-blocks-icon/tsconfig-build.json"}, {"path": "./packages/wonder-blocks-icon-button/tsconfig-build.json"}, {"path": "./packages/wonder-blocks-labeled-field/tsconfig-build.json"},