diff --git a/package.json b/package.json index d927820..a89bbe1 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,11 @@ "files": [ "README.md", "package.json", - "scripts/*.js" + "scripts/*.js", + "lib/*.js" ], + "main": "lib/index.js", + "types": "lib/index.d.ts", "bin": { "matrix-gen-i18n": "scripts/gen-i18n.js", "matrix-compare-i18n-files": "scripts/compare-file.js", @@ -18,9 +21,10 @@ }, "scripts": { "build:ts": "tsc", + "build:ts-scripts": "tsc -p tsconfig-scripts.json", "build:chmod": "chmod +x scripts/*.js", "build:shebang": "perl -i -pe 'print \"#!/usr/bin/env node\\n\\n\" if $. == 1 && !/^#/; close ARGV if eof' scripts/*.js", - "build": "yarn build:ts && yarn build:chmod && yarn build:shebang", + "build": "yarn build:ts && yarn build:ts-scripts && yarn build:chmod && yarn build:shebang", "lint": "yarn lint:types", "lint:types": "tsc --noEmit", "prepare": "yarn build" diff --git a/scripts/common.ts b/scripts/common.ts index 9cec1e4..5cf491c 100644 --- a/scripts/common.ts +++ b/scripts/common.ts @@ -16,22 +16,14 @@ limitations under the License. import fs from "fs"; import { isBinaryExpression, isStringLiteral, isTemplateLiteral, Node } from "@babel/types"; +import { Translation, Translations } from "../src"; export const NESTING_KEY = process.env["NESTING_KEY"] || "|"; export const INPUT_FILE = process.env["INPUT_FILE"] || 'src/i18n/strings/en_EN.json'; export const OUTPUT_FILE = process.env["OUTPUT_FILE"] || 'src/i18n/strings/en_EN.json'; -export type Translation = string | { - one?: string; - other: string; -}; - -export interface Translations { - [key: string]: Translation | Translations; -} - export function getPath(key: string): string[] { - return key.split(NESTING_KEY); + return key.replace(/\\n/g, "\n").split(NESTING_KEY); } export function getKeys(translations: Translations | Translation, path = ""): string[] { diff --git a/scripts/gen-i18n.ts b/scripts/gen-i18n.ts index 8b87f89..277dd9b 100644 --- a/scripts/gen-i18n.ts +++ b/scripts/gen-i18n.ts @@ -44,9 +44,8 @@ import { getTranslations, OUTPUT_FILE, putTranslations, - Translation, - Translations } from "./common"; +import { Translation, Translations } from "../src"; // Find the package.json for the project we're running gen-18n against const projectPackageJsonPath = path.join(process.cwd(), 'package.json'); diff --git a/scripts/rekey.ts b/scripts/rekey.ts index 88eb7a1..7271a07 100644 --- a/scripts/rekey.ts +++ b/scripts/rekey.ts @@ -16,16 +16,18 @@ limitations under the License. import parseArgs from "minimist"; import _ from "lodash"; -import { getPath, getTranslations, putTranslations, Translations } from "./common"; import fs from "fs"; import path from "path"; +import { getPath, getTranslations, putTranslations } from "./common"; +import { Translations } from "../src"; + const I18NDIR = "src/i18n/strings"; const argv = parseArgs<{ copy: boolean; }>(process.argv.slice(2), { - boolean: ["copy", "move", "case-insensitive", "find-and-replace"], + boolean: ["copy"], }); const [oldPath, newPath] = argv._.map(getPath); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..9e7030b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,18 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export * from "./utils"; +export * from "./types"; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9a6fe92 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,69 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Utility type for string dot notation for accessing nested object properties. + * Based on https://stackoverflow.com/a/58436959 + * @example + * { + * "a": { + * "b": { + * "c": "value" + * }, + * "d": "foobar" + * } + * } + * will yield a type of `"a.b.c" | "a.d"` with Separator="." + * @typeParam Target the target type to generate leaf keys for + * @typeParam Separator the separator to use between key segments when accessing nested objects + * @typeParam LeafType the type which leaves of this object extend, used to determine when to stop recursion + * @typeParam MaxDepth the maximum depth to recurse to + * @returns a union type representing all dot (Separator) string notation keys which can access a Leaf (of LeafType) + */ +export type Leaves = [ + MaxDepth, +] extends [never] + ? never + : Target extends LeafType + ? "" + : { + [K in keyof Target]-?: Join, Separator>; + }[keyof Target]; +type Prev = [never, 0, 1, 2, 3, ...0[]]; +type Join = K extends string | number + ? P extends string | number + ? `${K}${"" extends P ? "" : S}${P}` + : never + : never; + +/** + * Utility type for |-separated keys indexing into a translations file + * @typeParam Translations the target type to generate leaf keys for + * @typeParam Separator the separator to use between key segments when accessing nested objects + * @typeParam LeafType the type which leaves of this object extend, used to determine when to stop recursion + * @typeParam MaxDepth the maximum depth to recurse to + * @returns a union type representing all dot (Separator) string notation keys which can access a Leaf (of LeafType) + */ +export type TranslationKey = Leaves; + +export type Translation = string | { + one?: string; + other: string; +}; + +export interface Translations { + [key: string]: Translation | Translations; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..2da310d --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,53 @@ +/* +Copyright 2017 MTRNord and Cooperative EITA +Copyright 2017 Vector Creations Ltd. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Returns a language string with underscores replaced with + * hyphens, and lower-cased. + * + * @param {string} language The language string to be normalized + * @returns {string} The normalized language string + */ +export function normalizeLanguageKey(language: string): string { + return language.toLowerCase().replace("_", "-"); +} + +/** + * Turns a language string, normalises it, + * (see normalizeLanguageKey) into an array of language strings + * with fallback to generic languages + * (e.g. 'pt-BR' => ['pt-br', 'pt']) + * + * @param language The input language string + * @return a list of normalised languages + */ +export function getNormalizedLanguageKeys(language: string): string[] { + const languageKeys: string[] = []; + const normalizedLanguage = normalizeLanguageKey(language); + const languageParts = normalizedLanguage.split("-"); + if (languageParts.length === 2 && languageParts[0] === languageParts[1]) { + languageKeys.push(languageParts[0]); + } else { + languageKeys.push(normalizedLanguage); + if (languageParts.length === 2) { + languageKeys.push(languageParts[0]); + } + } + return languageKeys; +} diff --git a/tsconfig-scripts.json b/tsconfig-scripts.json new file mode 100644 index 0000000..3de8cf3 --- /dev/null +++ b/tsconfig-scripts.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2016", + "esModuleInterop": true, + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "noUnusedLocals": true + }, + "include": [ + "./scripts/*.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 770645c..66cd437 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,11 +4,14 @@ "esModuleInterop": true, "module": "commonjs", "moduleResolution": "node", - "noImplicitAny": true, "strict": true, - "noUnusedLocals": true + "noUnusedLocals": true, + "outDir": "lib", + "rootDir": "src", + "declaration": true, + "declarationMap": true }, "include": [ - "./scripts/*.ts", + "./src/*.ts" ] }