From 3a212a51d635264a663bfccc8b8a099ca7f28ce3 Mon Sep 17 00:00:00 2001 From: JonasDov <100586436+JonasDov@users.noreply.github.com> Date: Thu, 23 Nov 2023 10:32:25 +0200 Subject: [PATCH] Add localization to core-interop and hierarchy-builder packages (#349) * Add localization * Add changeset * Add eslint disable * Resolve comments * Add test for 100% coverage * Remove unused export * Run extract api --- .changeset/fifty-dancers-thank.md | 2 + .../api/presentation-core-interop.api.md | 5 +++ .../src/core-interop/Localization.ts | 16 ++++++++ .../src/presentation-core-interop.ts | 1 + .../src/test/Localization.test.ts | 24 ++++++++++++ .../full-stack-tests/src/IntegrationTests.ts | 8 ++-- .../hierarchy-builder/Localization.test.ts | 30 +++++++++++++++ packages/hierarchy-builder/.npmignore | 14 +++++++ .../api/presentation-hierarchy-builder.api.md | 9 +++++ packages/hierarchy-builder/package.json | 5 ++- .../en/PresentationHierarchyBuilder.json | 6 +++ .../src/hierarchy-builder/Localization.ts | 38 +++++++++++++++++++ .../src/presentation-hierarchy-builder.ts | 1 + .../src/test/Localization.test.ts | 23 +++++++++++ 14 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 .changeset/fifty-dancers-thank.md create mode 100644 packages/core-interop/src/core-interop/Localization.ts create mode 100644 packages/core-interop/src/test/Localization.test.ts create mode 100644 packages/full-stack-tests/src/hierarchy-builder/Localization.test.ts create mode 100644 packages/hierarchy-builder/.npmignore create mode 100644 packages/hierarchy-builder/public/locales/en/PresentationHierarchyBuilder.json create mode 100644 packages/hierarchy-builder/src/hierarchy-builder/Localization.ts create mode 100644 packages/hierarchy-builder/src/test/Localization.test.ts diff --git a/.changeset/fifty-dancers-thank.md b/.changeset/fifty-dancers-thank.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/fifty-dancers-thank.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/core-interop/api/presentation-core-interop.api.md b/packages/core-interop/api/presentation-core-interop.api.md index b4d62b6b3..306cd5b98 100644 --- a/packages/core-interop/api/presentation-core-interop.api.md +++ b/packages/core-interop/api/presentation-core-interop.api.md @@ -9,6 +9,8 @@ import { IECSqlQueryExecutor } from '@itwin/presentation-hierarchy-builder'; import { ILogger } from '@itwin/presentation-hierarchy-builder'; import { IMetadataProvider } from '@itwin/presentation-hierarchy-builder'; import { IPrimitiveValueFormatter } from '@itwin/presentation-hierarchy-builder'; +import { Localization } from '@itwin/core-common'; +import { LocalizationFunction } from '@itwin/presentation-hierarchy-builder'; import { QueryBinder } from '@itwin/core-common'; import { QueryOptions } from '@itwin/core-common'; import { SchemaContext } from '@itwin/ecschema-metadata'; @@ -17,6 +19,9 @@ import { UnitSystemKey } from '@itwin/core-quantity'; // @beta export function createECSqlQueryExecutor(imodel: IECSqlReaderFactory): IECSqlQueryExecutor; +// @beta +export function createLocalizationFunction(localization: Localization): Promise; + // @beta export function createLogger(): ILogger; diff --git a/packages/core-interop/src/core-interop/Localization.ts b/packages/core-interop/src/core-interop/Localization.ts new file mode 100644 index 000000000..5d7e80ab5 --- /dev/null +++ b/packages/core-interop/src/core-interop/Localization.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { Localization } from "@itwin/core-common"; +import { LOCALIZATION_NAMESPACE, LocalizationFunction } from "@itwin/presentation-hierarchy-builder"; + +/** + * Create a `LocalizationFunction` that uses [Localization]($core-common) API to register the namespace and create a localized string. + * @beta + */ +export async function createLocalizationFunction(localization: Localization): Promise { + await localization.registerNamespace(LOCALIZATION_NAMESPACE); + return (input) => localization.getLocalizedString(input); +} diff --git a/packages/core-interop/src/presentation-core-interop.ts b/packages/core-interop/src/presentation-core-interop.ts index 2a95d3f28..d2495e6a0 100644 --- a/packages/core-interop/src/presentation-core-interop.ts +++ b/packages/core-interop/src/presentation-core-interop.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ export * from "./core-interop/Formatting"; +export * from "./core-interop/Localization"; export * from "./core-interop/Logging"; export * from "./core-interop/Metadata"; export * from "./core-interop/QueryExecutor"; diff --git a/packages/core-interop/src/test/Localization.test.ts b/packages/core-interop/src/test/Localization.test.ts new file mode 100644 index 000000000..08a161bfe --- /dev/null +++ b/packages/core-interop/src/test/Localization.test.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from "chai"; +import sinon from "sinon"; +import { Localization } from "@itwin/core-common"; +import { LOCALIZATION_NAMESPACE } from "@itwin/presentation-hierarchy-builder"; +import { createLocalizationFunction } from "../core-interop/Localization"; + +describe("createTranslator", () => { + it("creates a localization function using provided core `Localization` object", async () => { + const registerNamespaceSpy = sinon.spy(); + const localization = { + getLocalizedString: (input: string) => `${input}_localized`, + registerNamespace: registerNamespaceSpy, + } as unknown as Localization; + const translator = await createLocalizationFunction(localization); + expect(registerNamespaceSpy).to.be.calledWith(LOCALIZATION_NAMESPACE); + const result = translator("Test"); + expect(result).to.be.eq("Test_localized"); + }); +}); diff --git a/packages/full-stack-tests/src/IntegrationTests.ts b/packages/full-stack-tests/src/IntegrationTests.ts index a21165fd0..86ceaace9 100644 --- a/packages/full-stack-tests/src/IntegrationTests.ts +++ b/packages/full-stack-tests/src/IntegrationTests.ts @@ -7,20 +7,20 @@ import * as fs from "fs"; import Backend from "i18next-http-backend"; import * as path from "path"; import { Guid, Logger, LogLevel } from "@itwin/core-bentley"; +import { IModelReadRpcInterface, SnapshotIModelRpcInterface } from "@itwin/core-common"; import { IModelApp, IModelAppOptions, NoRenderApp } from "@itwin/core-frontend"; import { ITwinLocalization } from "@itwin/core-i18n"; +import { ECSchemaRpcInterface } from "@itwin/ecschema-rpcinterface-common"; +import { ECSchemaRpcImpl } from "@itwin/ecschema-rpcinterface-impl"; import { HierarchyCacheMode, Presentation as PresentationBackend, PresentationBackendNativeLoggerCategory, PresentationProps as PresentationBackendProps, } from "@itwin/presentation-backend"; +import { PresentationRpcInterface } from "@itwin/presentation-common"; import { PresentationProps as PresentationFrontendProps } from "@itwin/presentation-frontend"; import { initialize as initializePresentation, PresentationTestingInitProps, terminate as terminatePresentation } from "@itwin/presentation-testing"; -import { ECSchemaRpcInterface } from "@itwin/ecschema-rpcinterface-common"; -import { ECSchemaRpcImpl } from "@itwin/ecschema-rpcinterface-impl"; -import { IModelReadRpcInterface, SnapshotIModelRpcInterface } from "@itwin/core-common"; -import { PresentationRpcInterface } from "@itwin/presentation-common"; class IntegrationTestsApp extends NoRenderApp { public static override async startup(opts?: IModelAppOptions): Promise { diff --git a/packages/full-stack-tests/src/hierarchy-builder/Localization.test.ts b/packages/full-stack-tests/src/hierarchy-builder/Localization.test.ts new file mode 100644 index 000000000..31f777272 --- /dev/null +++ b/packages/full-stack-tests/src/hierarchy-builder/Localization.test.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from "chai"; +import { IModelApp } from "@itwin/core-frontend"; +import { createLocalizationFunction } from "@itwin/presentation-core-interop"; +import { setLocalizationFunction } from "@itwin/presentation-hierarchy-builder"; +import { translate } from "@itwin/presentation-hierarchy-builder/lib/cjs/hierarchy-builder/Localization"; +import { initialize, terminate } from "../IntegrationTests"; + +describe("Stateless hierarchy builder", () => { + describe("Localization", () => { + beforeEach(async () => { + await initialize(); + }); + + afterEach(async () => { + await terminate(); + }); + + it("translates strings using `IModelApp.localization`", async function () { + const localizationFunction = await createLocalizationFunction(IModelApp.localization); + setLocalizationFunction(localizationFunction); + const result = translate("grouping.other-label"); + expect(result).to.be.eq("Òthér"); + }); + }); +}); diff --git a/packages/hierarchy-builder/.npmignore b/packages/hierarchy-builder/.npmignore new file mode 100644 index 000000000..06f3449b0 --- /dev/null +++ b/packages/hierarchy-builder/.npmignore @@ -0,0 +1,14 @@ +# start off ignoring everything +* +# then add back only the files we want +!*.md + +!lib/**/*.d.ts +!lib/**/*.d.ts.map +!lib/**/*.js +!lib/**/*.js.map +!lib/**/public/**/* + +# then ignore some stuff again +lib/**/test/** +lib/**/public/locales/en-PSEUDO/** diff --git a/packages/hierarchy-builder/api/presentation-hierarchy-builder.api.md b/packages/hierarchy-builder/api/presentation-hierarchy-builder.api.md index 0a2414818..bd2a000da 100644 --- a/packages/hierarchy-builder/api/presentation-hierarchy-builder.api.md +++ b/packages/hierarchy-builder/api/presentation-hierarchy-builder.api.md @@ -653,6 +653,12 @@ export interface LabelGroupingNodeKey { type: "label-grouping"; } +// @beta +export const LOCALIZATION_NAMESPACE = "PresentationHierarchyBuilder"; + +// @beta +export type LocalizationFunction = (input: string) => string; + // @beta (undocumented) export type LogFunction = (category: string, message: string) => void; @@ -805,6 +811,9 @@ export interface PropertyValueSelectClauseProps { specialType?: SpecialPropertyType; } +// @beta +export function setLocalizationFunction(localizationFunction?: LocalizationFunction): void; + // @beta export function setLogger(logger: ILogger | undefined): void; diff --git a/packages/hierarchy-builder/package.json b/packages/hierarchy-builder/package.json index f37067a29..99972c807 100644 --- a/packages/hierarchy-builder/package.json +++ b/packages/hierarchy-builder/package.json @@ -23,10 +23,11 @@ "module": "lib/esm/presentation-hierarchy-builder.js", "types": "lib/cjs/presentation-hierarchy-builder.d.ts", "scripts": { - "build": "npm run -s build:cjs && npm run -s build:esm", + "build": "npm run -s copy:locale && npm run -s build:cjs && npm run -s build:esm", "build:cjs": "tsc -p tsconfig.cjs.json", "build:esm": "tsc -p tsconfig.esm.json", - "build:watch": "npm run -s build:cjs -- -w", + "build:watch": "npm run -s copy:locale && npm run -s build:cjs -- -w", + "copy:locale": "cpx \"./public/**/*\" ./lib/public", "clean": "rimraf lib", "cover": "nyc npm -s test", "lint": "eslint ./src/**/*.ts", diff --git a/packages/hierarchy-builder/public/locales/en/PresentationHierarchyBuilder.json b/packages/hierarchy-builder/public/locales/en/PresentationHierarchyBuilder.json new file mode 100644 index 000000000..b2acd54e6 --- /dev/null +++ b/packages/hierarchy-builder/public/locales/en/PresentationHierarchyBuilder.json @@ -0,0 +1,6 @@ +{ + "grouping": { + "other-label": "Other", + "unspecified-label": "Not specified" + } +} diff --git a/packages/hierarchy-builder/src/hierarchy-builder/Localization.ts b/packages/hierarchy-builder/src/hierarchy-builder/Localization.ts new file mode 100644 index 000000000..5038a5efd --- /dev/null +++ b/packages/hierarchy-builder/src/hierarchy-builder/Localization.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +/** A localization function implementation returns the same input. */ +const NOOP_LOCALIZATION_FUNCTION = (input: string) => input; + +// eslint-disable-next-line @typescript-eslint/naming-convention +let g_localizationFunction = NOOP_LOCALIZATION_FUNCTION; + +/** + * An type for a localization function used by this package. + * @beta + */ +export type LocalizationFunction = (input: string) => string; + +/** + * A namespace that is used for localization. + * @beta + */ +export const LOCALIZATION_NAMESPACE = "PresentationHierarchyBuilder"; + +/** + * Set localization function to use by this package. By default the package uses a no-op localization function. + * @beta + */ +export function setLocalizationFunction(localizationFunction?: LocalizationFunction) { + g_localizationFunction = localizationFunction || NOOP_LOCALIZATION_FUNCTION; +} + +/** + * Use localization function that is set in this package. + * @internal + */ +export function translate(input: string) { + return g_localizationFunction(`${LOCALIZATION_NAMESPACE}:${input}`); +} diff --git a/packages/hierarchy-builder/src/presentation-hierarchy-builder.ts b/packages/hierarchy-builder/src/presentation-hierarchy-builder.ts index cd719ed9c..b5ad7d641 100644 --- a/packages/hierarchy-builder/src/presentation-hierarchy-builder.ts +++ b/packages/hierarchy-builder/src/presentation-hierarchy-builder.ts @@ -6,6 +6,7 @@ export * from "./hierarchy-builder/HierarchyDefinition"; export * from "./hierarchy-builder/HierarchyNode"; export * from "./hierarchy-builder/HierarchyProvider"; +export { LOCALIZATION_NAMESPACE, LocalizationFunction, setLocalizationFunction } from "./hierarchy-builder/Localization"; export * from "./hierarchy-builder/Logging"; export * from "./hierarchy-builder/Metadata"; export * from "./hierarchy-builder/queries/ECSql"; diff --git a/packages/hierarchy-builder/src/test/Localization.test.ts b/packages/hierarchy-builder/src/test/Localization.test.ts new file mode 100644 index 000000000..4b12e82af --- /dev/null +++ b/packages/hierarchy-builder/src/test/Localization.test.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from "chai"; +import { LOCALIZATION_NAMESPACE, setLocalizationFunction, translate } from "../hierarchy-builder/Localization"; + +describe("translate", () => { + it("returns same input with namespace appended when localizationFunction isn't set", () => { + expect(translate("Test")).to.eq(`${LOCALIZATION_NAMESPACE}:Test`); + }); + + it("returns same input with namespace appended when localizationFunction is set to undefined", () => { + setLocalizationFunction(); + expect(translate("Test")).to.eq(`${LOCALIZATION_NAMESPACE}:Test`); + }); + + it("returns input modified by custom localizationFunction with namespace appended when localizationFunction is set", () => { + setLocalizationFunction((input) => `${input}_translated`); + expect(translate("Test")).to.eq(`${LOCALIZATION_NAMESPACE}:Test_translated`); + }); +});