diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..ecef790 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,28 @@ +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18, 20, 21] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm install + - run: npm run build + - run: npm run test diff --git a/.gitignore b/.gitignore index a7c5098..df105c0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ node_modules /dist /.svelte-kit -/semantic .env.* !.env.example .vercel @@ -12,5 +11,6 @@ node_modules /static/modules /raw /temp +/coverage .vercel .vscode/settings.json \ No newline at end of file diff --git a/.npmignore b/.npmignore index e920a36..eed20eb 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,9 @@ -node_modules -test +/node_modules +/test +/coverage +/docs .* *.config.js -*.config.json \ No newline at end of file +*.config.json +.gitignore +/.svelte-kit \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 579bdd0..e83a92d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,10 +12,7 @@ "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/tsx", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "skipFiles": [ - "/**", - "${workspaceFolder}/node_modules/**", - ], + "skipFiles": ["/**", "${workspaceFolder}/node_modules/**"] } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 4022932..d942cd7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,19 @@ +[![view on npm](https://badgen.net/npm/v/omni18n)](https://www.npmjs.org/package/omni18n) +[![npm module downloads](https://badgen.net/npm/dt/omni18n)](https://www.npmjs.org/package/omni18n) +[![Gihub repo dependents](https://badgen.net/github/dependents-repo/emedware/omni18n)](https://github.com/emedware/omni18n/network/dependents?dependent_type=REPOSITORY) +[![Gihub package dependents](https://badgen.net/github/dependents-pkg/emedware/omni18n)](https://github.com/emedware/omni18n/network/dependents?dependent_type=PACKAGE) +[![Node.js CI](https://github.com/emedware/omni18n/actions/workflows/node.js.yml/badge.svg)](https://github.com/emedware/omni18n/actions/workflows/node.js.yml) + + + # omni18n Generic i18n library managing the fullstack interaction in a CI/CD pace. The fact the dictionaries are stored in a DB edited by the translators through a(/the same) web application - managing translation errors, missing keys, ... It can even manage update of all (concerned) clients when a translation is modified +The main documentation on [GitHub pages](https://emedware.github.io/omni18n/) or in [the repository](./docs/README.md) + ## General structure The library is composed of a server part and a client part. @@ -34,6 +44,8 @@ console.log(T('msg.hello')) The full-stack case will insert the http protocol between `client` and `server`. The `condense` function takes few arguments and return a (promise of) json-able object so can go through an http request. +The "Omni" part is that it can be integrated for various asynchronous scenarios and in many frameworks. + ### Interactive mode In interactive mode (using `InteractiveServer`), the DB interface contains modification functions and the server exposes modification function, that will modify the DB but also raise events. In this case, an `InteractiveServer` instance has to be created for every client, with an interface toward the DB and a callback for event raising. @@ -43,6 +55,7 @@ In interactive mode (using `InteractiveServer`), the DB interface contains modif Two interfaces allow to implement an interface to any database: `OmnI18n.DB` (who basically just has a `list`) and `OmnI18n.InteractiveDB` who has some modification access Two are provided: a `MemDB` who is basically an "in-memory database" and its descendant, a `FileDB` who allows: + - reading from a file - maintaining the files when changes are brought @@ -246,13 +259,17 @@ import { reports, type TContext } from "omni18n"; client: I18nClient }*/ -reports.missing = ({key, client}: TContext) { - if (client.loading) return `...` // `onModification` callback has been provided - return `[${key}]` +reports.loading = ({ key, client }: TContext): string { + // report if not expected + return '...' +} +reports.missing = ({ key, client }: TContext, fallback?: string): string { + // report + return fallback ?? `[${key}]` } reports.error = (context: TContext, error: string, spec: object) { - if (client.loading) return `...` // `onModification` callback has been provided - return `[!${error}]` + // report + return `[!${error}]` } ``` @@ -263,4 +280,4 @@ The function might do as much logging as they wish, the returned string will be ## TODOs - testing the error system -- detailed documentation on each part \ No newline at end of file +- detailed documentation on each part diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..01991cc --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +# OmnI18n + +[Overview](../README.md) + +> :warning: **Work in progress!** + +Projects using OmnI18n use it in 4 layers + +1. [The `client`](./client.md): The client manages the cache and download along with text retrieval and interpolation +2. (optional) The HTTP or any other layer. This part is implemented by the user +3. The `server`: The server exposes functions to interact with the languages +4. The `database`: A class implementing some interface that interacts directly with a database diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..5197212 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-leap-day diff --git a/docs/client.md b/docs/client.md new file mode 100644 index 0000000..0548022 --- /dev/null +++ b/docs/client.md @@ -0,0 +1,82 @@ +# Client part + +The client part `I18nClient` is usually instantiated once per client. + +For instance, in the browser for an SPA is is instantiated once for the whole web-application lifetime, while on the server-side for SSR, it might be instantiated once per request or per session. + +## Interactions and configurations + +### With the server + +```ts +I18nClient(locales: OmnI18n.Locale[], condense: OmnI18n.Condense, onModification?: OmnI18n.OnModification) +``` + +- `locales`: A list of locales: from preferred to fallback +- `condense`: A function that will query the server for the condensed dictionary +- `onModification`: A function that will be called when the dictionary is modified + +```ts +const client = new I18nClient(['fr', 'en'], server.condense, frontend.refreshTexts) +``` + +### Global settings + +These are variables you can import and modify: + +```ts +import { reports, formats, processors } from 'omni18n' +``` + +#### `reports` + +Reporting mechanism in case of problem. They both take an argument of type `TContext` describing mainly the client and the key where the problem occurred + +```ts +export interface TContext { + key: string + zones: string[] + client: I18nClient +} +``` + +> If texts might be displayed before loading is complete, make sure `onModification` has been specified as it will be called when the translations will be provided + +These reports will: + +- have any side effect, like logging or making a request that will log +- return a string that will be used instead of the expected translation + +`reports` contain: + +- A missing key report + +```ts +reports.missing = ({ key, client }: TContext, fallback?: string) => { + // report + return fallback ?? `[${key}]` +} +``` + +- A "missing key while loading" report + This one is called only when the client is in a loading state. If `onModification` was specified, it will be called once loaded. If not, the client will automatically check all the keys that went through this error to check them again. + +```ts +reports.loading = ({ client }: TContext) => '...' +``` + +- An interpolation error + When interpolating, an error calls this report with a textual description and some specifications depending on the error. + +> The specification is json-able _except_ in the case of `error: "Error in processor"`, in which case `spec.error` is whatever had been thrown and might be an `Error` or `Exception` + +```ts +reports.error = ({ key, client }: TContext, error: string, spec: object) => { + // report + return '[!error!]' +} +``` + +#### `formats` + +#### `processors` diff --git a/docs/part2.md b/docs/part2.md new file mode 100644 index 0000000..bb0c94c --- /dev/null +++ b/docs/part2.md @@ -0,0 +1,7 @@ +# Part 2 + +[root](/) + +[local root](./) + +[index](README.md) diff --git a/jest.config.json b/jest.config.json index 5c419b4..666096c 100644 --- a/jest.config.json +++ b/jest.config.json @@ -3,5 +3,6 @@ "^.+\\.(ts|tsx)$": "ts-jest" }, "testEnvironment": "node", - "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"] + "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], + "coverageReporters": ["lcov"] } diff --git a/package-lock.json b/package-lock.json index 49c4087..41c76f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@types/jest": "^29.5.12", "eslint": "^9.1.1", "jest": "^29.7.0", + "jsdoc-to-markdown": "^8.0.1", "prettier": "^3.2.5", "rollup": "^4.16.4", "rollup-plugin-typescript2": "^0.36.0", @@ -1092,18 +1093,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.1.1.tgz", @@ -1510,6 +1499,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1823,6 +1824,28 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "dev": true + }, "node_modules/@types/node": { "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", @@ -1896,6 +1919,27 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escape-sequences": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz", + "integrity": "sha512-dzW9kHxH011uBsidTXd14JXgzye/YLb2LzeKZ4bsgl/Knwx8AtbSFkkGxagdNOoh0DlqHCmfiEjWKBaqjOanVw==", + "dev": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ansi-escape-sequences/node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1963,6 +2007,15 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "dev": true, + "engines": { + "node": ">=12.17" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2085,6 +2138,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2178,6 +2237,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cache-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cache-point/-/cache-point-2.0.0.tgz", + "integrity": "sha512-4gkeHlFpSKgm3vm2gJN5sPqfmijYRFYCQ6tv5cLw0xVmT6r1z1vd4FNnpuOREco3cBs1G709sZ72LdgddKvL5w==", + "dev": true, + "dependencies": { + "array-back": "^4.0.1", + "fs-then-native": "^2.0.0", + "mkdirp2": "^1.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cache-point/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2216,6 +2298,18 @@ } ] }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2286,6 +2380,19 @@ "node": ">= 0.12.0" } }, + "node_modules/collect-all": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/collect-all/-/collect-all-1.0.4.tgz", + "integrity": "sha512-RKZhRwJtJEP5FWul+gkSMEnaK6H3AGPTTWOiRimCcs+rc/OmQE3Yhy1Q7A7KsdkG3ZXVdZq68Y6ONSdvkeEcKA==", + "dev": true, + "dependencies": { + "stream-connect": "^1.0.2", + "stream-via": "^1.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -2310,6 +2417,103 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-args/node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/command-line-args/node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-tool": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/command-line-tool/-/command-line-tool-0.8.0.tgz", + "integrity": "sha512-Xw18HVx/QzQV3Sc5k1vy3kgtOeGmsKIqwtFFoyjI4bbcpSgnw2CWVULvtakyw4s6fhyAdI6soQQhXc2OzJy62g==", + "dev": true, + "dependencies": { + "ansi-escape-sequences": "^4.0.0", + "array-back": "^2.0.0", + "command-line-args": "^5.0.0", + "command-line-usage": "^4.1.0", + "typical": "^2.6.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-tool/node_modules/array-back": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", + "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "dev": true, + "dependencies": { + "typical": "^2.6.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-4.1.0.tgz", + "integrity": "sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g==", + "dev": true, + "dependencies": { + "ansi-escape-sequences": "^4.0.0", + "array-back": "^2.0.0", + "table-layout": "^0.4.2", + "typical": "^2.6.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", + "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "dev": true, + "dependencies": { + "typical": "^2.6.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/common-sequence": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/common-sequence/-/common-sequence-2.0.2.tgz", + "integrity": "sha512-jAg09gkdkrDO9EWTdXfv80WWH3yeZl5oT69fGfedBNS9pXUKYInVJ1bJ+/ht2+Moeei48TmSbQDYMc8EOx9G0g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -2322,6 +2526,24 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/config-master": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/config-master/-/config-master-3.1.0.tgz", + "integrity": "sha512-n7LBL1zBzYdTpF1mx5DNcZnZn05CWIdsdvtPL4MosvqbBUK3Rq6VWEtGUuF3Y0s9/CIhMejezqlSkP6TnCJ/9g==", + "dev": true, + "dependencies": { + "walk-back": "^2.0.1" + } + }, + "node_modules/config-master/node_modules/walk-back": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-2.0.1.tgz", + "integrity": "sha512-Nb6GvBR8UWX1D+Le+xUq0+Q1kFmRBIWVrfLnQAOmcpEzA9oAxwJ9gIr36t9TWYfzvWRvuMtjHiVsJYEkXWaTAQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2394,6 +2616,15 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2427,6 +2658,29 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dmd": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dmd/-/dmd-6.2.0.tgz", + "integrity": "sha512-uXWxLF1H7TkUAuoHK59/h/ts5cKavm2LnhrIgJWisip4BVzPoXavlwyoprFFn2CzcahKYgvkfaebS6oxzgflkg==", + "dev": true, + "dependencies": { + "array-back": "^6.2.2", + "cache-point": "^2.0.0", + "common-sequence": "^2.0.2", + "file-set": "^4.0.2", + "handlebars": "^4.7.7", + "marked": "^4.2.3", + "object-get": "^2.1.1", + "reduce-flatten": "^3.0.1", + "reduce-unique": "^2.0.1", + "reduce-without": "^1.0.1", + "test-value": "^3.0.0", + "walk-back": "^5.1.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.746", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.746.tgz", @@ -2451,6 +2705,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2827,6 +3090,28 @@ "node": ">=16.0.0" } }, + "node_modules/file-set": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/file-set/-/file-set-4.0.2.tgz", + "integrity": "sha512-fuxEgzk4L8waGXaAkd8cMr73Pm0FxOVkn8hztzUW7BAHhOGH90viQNXbiOsnecCWmfInqU6YmAMwxRMdKETceQ==", + "dev": true, + "dependencies": { + "array-back": "^5.0.0", + "glob": "^7.1.6" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/file-set/node_modules/array-back": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-5.0.0.tgz", + "integrity": "sha512-kgVWwJReZWmVuWOQKEOohXKJX+nD02JAZ54D1RRWlv8L0NebauKAaFxACKzB74RTclt1+WNz5KHaLRDAPZbDEw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2880,6 +3165,27 @@ "semver": "bin/semver.js" } }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-replace/node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2926,6 +3232,15 @@ "node": ">=12" } }, + "node_modules/fs-then-native": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fs-then-native/-/fs-then-native-2.0.0.tgz", + "integrity": "sha512-X712jAOaWXkemQCAmWeg5rOT2i+KOpWz1Z/txk/cW0qlOu2oQ9H61vc5w3X/iyuUEfq/OyaFJ78/cZAQD1/bgA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3053,6 +3368,27 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3513,18 +3849,6 @@ } } }, - "node_modules/jest-config/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -3950,6 +4274,101 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc-api": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-8.0.0.tgz", + "integrity": "sha512-Rnhor0suB1Ds1abjmFkFfKeD+kSMRN9oHMTMZoJVUrmtCGDwXty+sWMA9sa4xbe4UyxuPjhC7tavZ40mDKK6QQ==", + "dev": true, + "dependencies": { + "array-back": "^6.2.2", + "cache-point": "^2.0.0", + "collect-all": "^1.0.4", + "file-set": "^4.0.2", + "fs-then-native": "^2.0.0", + "jsdoc": "^4.0.0", + "object-to-spawn-args": "^2.0.1", + "temp-path": "^1.0.0", + "walk-back": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/jsdoc-parse": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsdoc-parse/-/jsdoc-parse-6.2.1.tgz", + "integrity": "sha512-9viGRUUtWOk/G4V0+nQ6rfLucz5plxh5I74WbNSNm9h9NWugCDVX4jbG8hZP9QqKGpdTPDE+qJXzaYNos3wqTA==", + "dev": true, + "dependencies": { + "array-back": "^6.2.2", + "lodash.omit": "^4.5.0", + "reduce-extract": "^1.0.0", + "sort-array": "^4.1.5", + "test-value": "^3.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdoc-to-markdown": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/jsdoc-to-markdown/-/jsdoc-to-markdown-8.0.1.tgz", + "integrity": "sha512-qJfNJhkq2C26UYoOdj8L1yheTJlk1veCsxwRejRmj07XZKCn7oSkuPErx6+JoNi8afCaUKdIM5oUu0uF2/T8iw==", + "dev": true, + "dependencies": { + "array-back": "^6.2.2", + "command-line-tool": "^0.8.0", + "config-master": "^3.1.0", + "dmd": "^6.2.0", + "jsdoc-api": "^8.0.0", + "jsdoc-parse": "^6.2.1", + "walk-back": "^5.1.0" + }, + "bin": { + "jsdoc2md": "bin/cli.js" + }, + "engines": { + "node": ">=12.17" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -4019,6 +4438,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4056,6 +4484,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -4068,6 +4505,18 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -4080,6 +4529,18 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", + "dev": true + }, + "node_modules/lodash.padend": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", + "integrity": "sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==", + "dev": true + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4131,6 +4592,56 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4171,6 +4682,33 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp2": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/mkdirp2/-/mkdirp2-1.0.5.tgz", + "integrity": "sha512-xOE9xbICroUDmG1ye2h4bZ8WBie9EGmACaco8K8cx6RlkJJrxGIqjGqztAI+NMhexXBcdGbSEzI6N3EJPevxZw==", + "dev": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4183,6 +4721,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4216,6 +4760,21 @@ "node": ">=8" } }, + "node_modules/object-get": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-get/-/object-get-2.1.1.tgz", + "integrity": "sha512-7n4IpLMzGGcLEMiQKsNR7vCe+N5E9LORFrtNUVy4sO3dj9a3HedZCxEL2T7QuLhcHN1NBuBsMOKaOsAYI9IIvg==", + "dev": true + }, + "node_modules/object-to-spawn-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", + "integrity": "sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4524,6 +5083,98 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/reduce-extract": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/reduce-extract/-/reduce-extract-1.0.0.tgz", + "integrity": "sha512-QF8vjWx3wnRSL5uFMyCjDeDc5EBMiryoT9tz94VvgjKfzecHAVnqmXAwQDcr7X4JmLc2cjkjFGCVzhMqDjgR9g==", + "dev": true, + "dependencies": { + "test-value": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reduce-extract/node_modules/array-back": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw==", + "dev": true, + "dependencies": { + "typical": "^2.6.0" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/reduce-extract/node_modules/test-value": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/test-value/-/test-value-1.1.0.tgz", + "integrity": "sha512-wrsbRo7qP+2Je8x8DsK8ovCGyxe3sYfQwOraIY/09A2gFXU9DYKiTF14W4ki/01AEh56kMzAmlj9CaHGDDUBJA==", + "dev": true, + "dependencies": { + "array-back": "^1.0.2", + "typical": "^2.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reduce-flatten": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-3.0.1.tgz", + "integrity": "sha512-bYo+97BmUUOzg09XwfkwALt4PQH1M5L0wzKerBt6WLm3Fhdd43mMS89HiT1B9pJIqko/6lWx3OnV4J9f2Kqp5Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/reduce-unique": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/reduce-unique/-/reduce-unique-2.0.1.tgz", + "integrity": "sha512-x4jH/8L1eyZGR785WY+ePtyMNhycl1N2XOLxhCbzZFaqF4AXjLzqSxa2UHgJ2ZVR/HHyPOvl1L7xRnW8ye5MdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/reduce-without": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/reduce-without/-/reduce-without-1.0.1.tgz", + "integrity": "sha512-zQv5y/cf85sxvdrKPlfcRzlDn/OqKFThNimYmsS3flmkioKvkUGn2Qg9cJVoQiEvdxFGLE0MQER/9fZ9sUqdxg==", + "dev": true, + "dependencies": { + "test-value": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reduce-without/node_modules/array-back": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw==", + "dev": true, + "dependencies": { + "typical": "^2.6.0" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/reduce-without/node_modules/test-value": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", + "integrity": "sha512-+1epbAxtKeXttkGFMTX9H42oqzOTufR1ceCF+GYA5aOmvaPq9wd4PUS8329fn2RRLGNeUkgRLnVpycjx8DsO2w==", + "dev": true, + "dependencies": { + "array-back": "^1.0.3", + "typical": "^2.6.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4533,6 +5184,15 @@ "node": ">=0.10.0" } }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4744,6 +5404,37 @@ "node": ">=8" } }, + "node_modules/sort-array": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-4.1.5.tgz", + "integrity": "sha512-Ya4peoS1fgFN42RN1REk2FgdNOeLIEMKFGJvs7VTP3OklF8+kl2SkpVliZ4tk/PurWsrWRsdNdU+tgyOBkB9sA==", + "dev": true, + "dependencies": { + "array-back": "^5.0.0", + "typical": "^6.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sort-array/node_modules/array-back": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-5.0.0.tgz", + "integrity": "sha512-kgVWwJReZWmVuWOQKEOohXKJX+nD02JAZ54D1RRWlv8L0NebauKAaFxACKzB74RTclt1+WNz5KHaLRDAPZbDEw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/sort-array/node_modules/typical": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/typical/-/typical-6.0.1.tgz", + "integrity": "sha512-+g3NEp7fJLe9DPa1TArHm9QAA7YciZmWnfAqEaFrBihQ7epOv9i99rjtgb6Iz0wh3WuQDjsCTDfgRoGnmHN81A==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4775,6 +5466,39 @@ "node": ">=10" } }, + "node_modules/stream-connect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-connect/-/stream-connect-1.0.2.tgz", + "integrity": "sha512-68Kl+79cE0RGKemKkhxTSg8+6AGrqBt+cbZAXevg2iJ6Y3zX4JhA/sZeGzLpxW9cXhmqAcE7KnJCisUmIUfnFQ==", + "dev": true, + "dependencies": { + "array-back": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-connect/node_modules/array-back": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw==", + "dev": true, + "dependencies": { + "typical": "^2.6.0" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/stream-via": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-via/-/stream-via-1.0.4.tgz", + "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -4832,6 +5556,18 @@ "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4856,6 +5592,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/table-layout": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.5.tgz", + "integrity": "sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==", + "dev": true, + "dependencies": { + "array-back": "^2.0.0", + "deep-extend": "~0.6.0", + "lodash.padend": "^4.6.1", + "typical": "^2.6.1", + "wordwrapjs": "^3.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", + "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "dev": true, + "dependencies": { + "typical": "^2.6.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/temp-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-path/-/temp-path-1.0.0.tgz", + "integrity": "sha512-TvmyH7kC6ZVTYkqCODjJIbgvu0FKiwQpZ4D1aknE7xpcDf/qEOB8KZEK5ef2pfbVoiBhNWs3yx4y+ESMtNYmlg==", + "dev": true + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -4870,6 +5640,31 @@ "node": ">=8" } }, + "node_modules/test-value": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/test-value/-/test-value-3.0.0.tgz", + "integrity": "sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ==", + "dev": true, + "dependencies": { + "array-back": "^2.0.0", + "typical": "^2.6.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/test-value/node_modules/array-back": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", + "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "dev": true, + "dependencies": { + "typical": "^2.6.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5017,6 +5812,37 @@ "node": ">=14.17" } }, + "node_modules/typical": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", + "integrity": "sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg==", + "dev": true + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -5085,6 +5911,15 @@ "node": ">=10.12.0" } }, + "node_modules/walk-back": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-5.1.0.tgz", + "integrity": "sha512-Uhxps5yZcVNbLEAnb+xaEEMdgTXl9qAQDzKYejG2AZ7qPwRQ81lozY9ECDbjLPNWm7YsO1IK5rsP1KoQzXAcGA==", + "dev": true, + "engines": { + "node": ">=12.17" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -5109,6 +5944,34 @@ "node": ">= 8" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wordwrapjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-3.0.0.tgz", + "integrity": "sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==", + "dev": true, + "dependencies": { + "reduce-flatten": "^1.0.1", + "typical": "^2.6.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/reduce-flatten": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", + "integrity": "sha512-j5WfFJfc9CoXv/WbwVLHq74i/hdTUpy+iNC534LxczMRP67vJeK3V9JOdnL0N1cIRbn9mYhE2yVjvvKXDxvNXQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5145,6 +6008,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 136c35f..2dc1597 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,15 @@ "type": "module", "scripts": { "test": "jest", + "test:cover": "jest --coverage", "prettier": "prettier --write .", - "build": "rollup -c" + "build": "rollup -c", + "jsdoc": "jsdoc", + "prepare": "npm run prettier && npm run build" }, "repository": { "type": "git", - "url": "https://github.com/eddow/omni18n.git" + "url": "git+https://github.com/emedware/omni18n.git" }, "keywords": [ "omni18n", @@ -32,6 +35,7 @@ "@types/jest": "^29.5.12", "eslint": "^9.1.1", "jest": "^29.7.0", + "jsdoc-to-markdown": "^8.0.1", "prettier": "^3.2.5", "rollup": "^4.16.4", "rollup-plugin-typescript2": "^0.36.0", diff --git a/rollup.config.js b/rollup.config.js index 1caed25..86fd04f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -10,8 +10,15 @@ export default { format: 'cjs' }, external: ['hjson'], - plugins: [resolve(), commonjs(), typescript({tsconfigOverride: { - include: ['./src'], - exclude: ['./node_modules'] - }}), json()] + plugins: [ + resolve(), + commonjs(), + typescript({ + tsconfigOverride: { + include: ['./src'], + exclude: ['./node_modules'] + } + }), + json() + ] } diff --git a/src/client/client.ts b/src/client/client.ts index bdc198e..3473b0e 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -4,9 +4,19 @@ */ import '../polyfill' import Defer from '../defer' -import { ClientDictionary, OmnI18nClient, Internals, TContext, text, zone } from './types' +import { + ClientDictionary, + OmnI18nClient, + Internals, + TContext as RootContext, + text, + zone, + fallback +} from './types' import { interpolate } from './interpolation' -import { longKeyList, parseInternals, recurExtend, translator } from './helpers' +import { longKeyList, parseInternals, recurExtend, reports, translator } from './helpers' + +export type TContext = RootContext export default class I18nClient implements OmnI18nClient { readonly ordinalRules: Intl.PluralRules @@ -18,9 +28,18 @@ export default class I18nClient implements OmnI18nClient { private loadDefer = new Defer() public loaded: Promise = Promise.resolve() + public checkOnLoad = new Set() public timeZone?: string + public currency?: string + /** + * + * @param locales A list of locales: from preferred to fallback + * @param condense A function that will query the server for the condensed dictionary + * @param onModification A function that will be called when the dictionary is modified + * @example new I18nClient(['fr', 'en'], server.condense, frontend.refreshTexts) + */ constructor( public locales: OmnI18n.Locale[], // On the server side, this is `server.condensed`. From the client-side this is an http request of some sort @@ -66,6 +85,25 @@ export default class I18nClient implements OmnI18nClient { this.internals = parseInternals(this.dictionary.internals) this.onModification?.(condensed.map(longKeyList).flat()) + for (const key of this.checkOnLoad) { + const keys = key.split('.') + let current = this.dictionary + let value = false, + fallenBack: string | undefined + for (const key of keys) { + if (!current[key]) break + if (current[key][text]) { + if (current[key][fallback]) fallenBack = current[key][text] + else { + value = true + break + } + } + current = current[key] + } + if (!value) reports.missing({ key, client: this, zones }, fallenBack) + } + this.checkOnLoad = new Set() } private async download(zones: string[]) { diff --git a/src/client/helpers.ts b/src/client/helpers.ts index 95582f6..284ae72 100644 --- a/src/client/helpers.ts +++ b/src/client/helpers.ts @@ -1,17 +1,46 @@ import { parse } from 'hjson' -import { ClientDictionary, TContext, TranslationError, Translator, text, zone } from './types' +import { + ClientDictionary, + TContext, + TranslationError, + Translator, + text, + zone, + fallback +} from './types' -function entry(t: string, z: string): ClientDictionary { - return { [text]: t, [zone]: z } +function entry(t: string, z: string, isFallback?: boolean): ClientDictionary { + return { [text]: t, [zone]: z, ...(isFallback ? { [fallback]: true } : {}) } +} + +export function reportMissing(context: TContext, fallback?: string): string { + if (!context.client.loading) return reports.missing(context, fallback) + if (!context.client.onModification) context.client.checkOnLoad.add(context.key) + return reports.loading(context) } export const reports = { - missing({ key, client }: TContext): string { - if (client.loading) return `...` // `onModification` callback has been provided - return `[${key}]` + loading({ client }: TContext): string { + return '...' // `onModification` callback has been provided }, - error({ client }: TContext, error: string, spec: object): string { - if (client.loading) return `...` // `onModification` callback has been provided + /** + * Report a missing translation + * @param key The key that is missing + * @param client The client that is missing the translation. The expected locale is in `client.locales[0]` + * @param fallback A fallback from another language if any + * @returns The string to display instead of the expected translation + */ + missing({ key, client }: TContext, fallback?: string): string { + return fallback ?? `[${key}]` + }, + /** + * Report a missing translation + * @param key The key that is missing + * @param client The client that is missing the translation. The expected locale is in `client.locales[0]` + * @param fallback A fallback from another language if any + * @returns The string to display instead of the expected translation + */ + error({ key, client }: TContext, error: string, spec: object): string { return `[!${error}]` } } @@ -20,19 +49,22 @@ export function translate(context: TContext, args: any[]): string { const { client, key } = context, keys = key.split('.') let current = client.dictionary, - value: [string, string] | undefined + value: [string, string, true | undefined] | undefined for (const k of keys) { if (!current[k]) break else { const next = current[k] as ClientDictionary - if (text in next) value = [next[text]!, next[zone]!] + if (text in next) value = [next[text]!, next[zone]!, next[fallback]] current = next } } - // This case can happen for example in role-zoning, when roles are entered separately - //if (value && !context.zones.includes(value[1])) reports.missing(context, value[1]) - return value ? client.interpolate(context, value[0], args) : reports.missing(context) + + return value?.[2] + ? client.interpolate(context, reportMissing(context, value[0]), args) + : value + ? client.interpolate(context, value[0], args) + : reportMissing(context) } export function translator(context: TContext): Translator { @@ -74,9 +106,10 @@ function condensed2dictionary( condensed: OmnI18n.CondensedDictionary, zone: OmnI18n.Zone ): ClientDictionary { - const dictionary: ClientDictionary = '' in condensed ? entry(condensed['']!, zone) : {} + const dictionary: ClientDictionary = + '' in condensed ? entry(condensed['']!, zone, !!condensed['.']) : {} for (const key in condensed) - if (key) { + if (!['', '.'].includes(key)) { const value = condensed[key] if (typeof value === 'string') dictionary[key] = entry(value, zone) else dictionary[key] = condensed2dictionary(value, zone) @@ -89,21 +122,23 @@ export function recurExtend( src: OmnI18n.CondensedDictionary, zone: OmnI18n.Zone ) { - for (const key in src) { - if (!dst[key]) - dst[key] = - typeof src[key] === 'string' - ? entry(src[key], zone) - : condensed2dictionary(src[key], zone) - else { - if (typeof src[key] === 'string') - dst[key] = { - ...dst[key], - ...entry(src[key], zone) - } - else recurExtend(dst[key], src[key], zone) + for (const key in src) + if (key === '') Object.assign(dst, entry(src[key]!, zone, !!src['.'])) + else if (key !== '.') { + if (!dst[key]) + dst[key] = + typeof src[key] === 'string' + ? entry(src[key], zone) + : condensed2dictionary(src[key], zone) + else { + if (typeof src[key] === 'string') + dst[key] = { + ...dst[key], + ...entry(src[key], zone) + } + else recurExtend(dst[key], src[key], zone) + } } - } } export function longKeyList(condensed: OmnI18n.CondensedDictionary) { diff --git a/src/client/index.ts b/src/client/index.ts index e724ee3..1812ff4 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,4 +1,4 @@ -export { default as I18nClient } from './client' -export { type ClientDictionary, type TContext, TranslationError, type Translator } from './types' +export { default as I18nClient, type TContext } from './client' +export { type ClientDictionary, TranslationError, type Translator } from './types' export { translator, reports } from './helpers' -export { formats, globals, processors } from './interpolation' +export { formats, processors } from './interpolation' diff --git a/src/client/interpolation.ts b/src/client/interpolation.ts index 827fc78..05ab3c4 100644 --- a/src/client/interpolation.ts +++ b/src/client/interpolation.ts @@ -1,8 +1,6 @@ -import { reports, translate } from './helpers' +import { reportMissing, reports, translate } from './helpers' import { TContext, TranslationError } from './types' -export const globals: { currency?: string } = {} - export const formats: Record<'date' | 'number' | 'relative', Record> = { date: { date: { @@ -43,7 +41,7 @@ export const processors: Record string> = { }, ordinal(this: TContext, str: string) { const { client } = this - if (!client.internals.ordinals) return reports.missing({ ...this, key: 'internals.ordinals' }) + if (!client.internals.ordinals) return reportMissing({ ...this, key: 'internals.ordinals' }) const num = parseInt(str) if (isNaN(num)) return reports.error(this, 'NaN', { str }) return client.internals.ordinals[client.ordinalRules.select(num)].replace('$', str) @@ -58,7 +56,7 @@ export const processors: Record string> = { : designation if (typeof rules === 'string') { - if (!client.internals.plurals) return reports.missing({ ...this, key: 'internals.plurals' }) + if (!client.internals.plurals) return reportMissing({ ...this, key: 'internals.plurals' }) if (!client.internals.plurals[rule]) return reports.error(this, 'Missing rule in plurals', { rule }) return client.internals.plurals[rule].replace('$', designation) @@ -76,10 +74,11 @@ export const processors: Record string> = { return reports.error(this, 'Invalid number options', { options }) options = formats.number[options] } - options = { - currency: globals.currency, - ...options - } + if (this.client.currency) + options = { + currency: this.client.currency, + ...options + } return num.toLocaleString(client.locales, options) }, date(this: TContext, str: string, options?: any) { @@ -92,10 +91,11 @@ export const processors: Record string> = { return reports.error(this, 'Invalid date options', { options }) options = formats.date[options] } - options = { - timeZone: client.timeZone, - ...options - } + if (client.timeZone) + options = { + timeZone: client.timeZone, + ...options + } return date.toLocaleString(client.locales, options) }, relative(this: TContext, str: string, options?: any) { diff --git a/src/client/types.ts b/src/client/types.ts index df58b9a..7a90d41 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -4,12 +4,14 @@ export interface Internals { } export const zone = Symbol('Zone'), - text = Symbol('Text') + text = Symbol('Text'), + fallback = Symbol('Fallback') export type ClientDictionary = { [key: string]: ClientDictionary [zone]?: string [text]?: OmnI18n.Zone + [fallback]?: true } export interface OmnI18nClient { @@ -19,14 +21,17 @@ export interface OmnI18nClient { readonly cardinalRules: Intl.PluralRules locales: OmnI18n.Locale[] timeZone?: string + currency?: string interpolate(context: TContext, text: string, args: any[]): string readonly loading: boolean + readonly checkOnLoad: Set + onModification?: OmnI18n.OnModification } -export interface TContext { +export interface TContext { key: string zones: string[] - client: OmnI18nClient + client: Client } export class TranslationError extends Error { diff --git a/src/db/README.md b/src/db/README.md index e08c8d7..e76ae40 100644 --- a/src/db/README.md +++ b/src/db/README.md @@ -10,4 +10,4 @@ In-memory "DB", initialized with a structured `MemDictionary` dictionary A `MemDB` that uses a file as a source and persists its change to the file system. -Note, a human-readable format is used using mostly indent (tabs) to group keys and texts \ No newline at end of file +Note, a human-readable format is used using mostly indent (tabs) to group keys and texts diff --git a/src/db/fileDb.ts b/src/db/fileDb.ts index bbd1bf6..d7efcbe 100644 --- a/src/db/fileDb.ts +++ b/src/db/fileDb.ts @@ -3,12 +3,12 @@ import { readFile, writeFile, stat } from 'node:fs/promises' import { parse, stringify } from 'hjson' import Defer from '../defer' -function rexCount(str: string, position: number, rex: RegExp = /\u0000/g) { +function parseError(str: string, position: number, end: number = position + 100) { let count = 0, fetch: RegExpExecArray | null - while ((fetch = rex.exec(str)) && fetch.index < position) count++ - return count + while ((fetch = /\u0000/g.exec(str)) && fetch.index < position) count++ + return new Error(`Unparsable data at line ${count}: ${str.slice(position, end)}`) } export default class FileDB extends MemDB< @@ -88,9 +88,6 @@ export default class FileDB extends M multiline: 'std', space: '\t' }) - /*stringified.length < 80 - ? stringified.replace(/[\n\t]/g, '') - :*/ return preTabs ? stringified.replace(/\n/g, '\n' + '\t'.repeat(preTabs)) : stringified } let rv = '' @@ -134,10 +131,7 @@ export default class FileDB extends M let keyFetch: RegExpExecArray | null let lastIndex = 0 while ((keyFetch = rex.key.exec(data))) { - if (keyFetch.index > lastIndex) - throw new Error( - `Unparsable data at line ${rexCount(data, rex.key.lastIndex)}: ${data.slice(lastIndex, keyFetch.index)}` - ) + if (keyFetch.index > lastIndex) throw parseError(data, lastIndex, keyFetch.index) const key = keyFetch[1], zone = keyFetch[3] as OmnI18n.Zone let keyInfos: any, @@ -163,10 +157,7 @@ export default class FileDB extends M if (Object.keys(textInfos).length) entry['.textInfos'] = textInfos dictionary[key] = entry } - if (rex.key.lastIndex > 0 || !rex.key.test(data)) - throw new Error( - `Unparsable data at line ${rexCount(data, rex.key.lastIndex)}: ${data.slice(rex.key.lastIndex, 100)}` - ) + if (rex.key.lastIndex > 0 || !rex.key.test(data)) throw parseError(data, rex.key.lastIndex) return dictionary } diff --git a/src/defer.ts b/src/defer.ts index 7ee466a..757bd96 100644 --- a/src/defer.ts +++ b/src/defer.ts @@ -35,7 +35,7 @@ export default class Defer { cancel() { if (!this.timeout) return clearTimeout(this.timeout) - this.rejecter!() + this.rejecter!('`Defer`red action canceled') this.timeout = undefined } diff --git a/src/server/server.ts b/src/server/server.ts index 448ba0f..c3684af 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -18,7 +18,7 @@ export function localeTree(locale: OmnI18n.Locale) { * Server class that should be instantiated once and used to interact with the database */ export default class I18nServer { - constructor(protected db: OmnI18n.DB) { + constructor(protected db: OmnI18n.DB) { this.condense = this.condense.bind(this) } @@ -52,9 +52,16 @@ export default class I18nServer{ '': current[k] } current = current[k] as CDic } - if (!hasValue || locales[0].startsWith(value[0])) { - if (current[lastKey] && typeof current[lastKey] !== 'string') - (current[lastKey])[''] = value[1] + const fallback = !locales[0].startsWith(value[0]), + clk = current[lastKey] + if (!hasValue || !fallback) { + if (fallback) + current[lastKey] = { + ...(typeof clk === 'object' ? clk : {}), + '': value[1], + '.': '.' + } + else if (clk && typeof clk !== 'string') (current[lastKey])[''] = value[1] else current[lastKey] = value[1] hasValue = true } diff --git a/src/types.d.ts b/src/types.d.ts index a3f002b..129502b 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -4,8 +4,9 @@ declare namespace OmnI18n { type Zone = string type CondensedDictionary = { - [key: Exclude]: CondensedDictionary | string + [key: Exclude]: CondensedDictionary | string ''?: string + '.'?: '.' // fallback marker } type Condense = (locales: Locale[], zones: Zone[]) => Promise @@ -25,7 +26,7 @@ declare namespace OmnI18n { infos: KeyInfos } /** - * Used for translator-related operations + * Dictionary used for translator-related operations */ type WorkDictionary = Record diff --git a/test/specifics.test.ts b/test/specifics.test.ts index adead1e..098b450 100644 --- a/test/specifics.test.ts +++ b/test/specifics.test.ts @@ -4,16 +4,24 @@ import { I18nServer, InteractiveServer, MemDB, - MemDictionary + MemDictionary, + TContext, + reports } from '../src/index' import { WaitingDB } from './db' import { readFile, writeFile, unlink, cp } from 'node:fs/promises' -describe('fallback', () => { +describe('specifics', () => { test('errors', async () => { // TODO test errors }) test('fallbacks', async () => { + const misses = jest.fn() + reports.missing = ({ key }: TContext, fallback?: string) => { + misses(key) + return fallback ?? '[no]' + } + const server = new I18nServer( new WaitingDB( new MemDB({ @@ -25,9 +33,18 @@ describe('fallback', () => { ), client = new I18nClient(['fr', 'en'], server.condense), T = client.enter() + expect('' + T.fld.name).toBe('...') + expect('' + T.fld.inexistent).toBe('...') await client.loaded + expect(misses).toHaveBeenCalledWith('fld.inexistent') + misses.mockClear() expect('' + T.fld.name).toBe('Name') + expect(misses).toHaveBeenCalledWith('fld.name') + misses.mockClear() expect('' + T.fld.bday.short).toBe('Anniversaire') + expect(misses).not.toHaveBeenCalled() + expect('' + T.fld.inexistent).toBe('[no]') + expect(misses).toHaveBeenCalledWith('fld.inexistent') }) test('serialize', () => { const content: MemDictionary = { diff --git a/test/static.test.ts b/test/static.test.ts index ad39eae..83bc154 100644 --- a/test/static.test.ts +++ b/test/static.test.ts @@ -76,6 +76,7 @@ beforeAll(async () => { clients = { en: new I18nClient(['en-US'], condense), be: new I18nClient(['fr-BE'], condense) } clients.en.enter('adm') clients.be.timeZone = 'Europe/Brussels' + clients.en.timeZone = 'Greenwich' T = Object.fromEntries(Object.entries(clients).map(([key, value]) => [key, value.enter()])) await Promise.all(Object.values(clients).map((client) => client.loaded)) }) @@ -172,11 +173,11 @@ describe('formatting', () => { const date = new Date('2021-05-01T12:34:56.789Z') expect(T.en.format.date(date)).toBe('5/1/21') expect(T.be.format.date(date)).toBe('1/05/21') - expect(T.en.format.dateTime(date)).toBe('5/1/2021, 3:34:56 PM') + expect(T.en.format.dateTime(date)).toBe('5/1/2021, 12:34:56 PM') expect(T.be.format.dateTime(date)).toBe('01/05/2021 14:34:56') expect(T.en.format.medium(date)).toBe('May 1, 2021') expect(T.be.format.medium(date)).toBe('1 mai 2021') - expect(T.en.format.time(date)).toBe('3:34 PM') + expect(T.en.format.time(date)).toBe('12:34 PM') expect(T.be.format.time(date)).toBe('14:34') }) test('relative', () => { diff --git a/tsconfig.json b/tsconfig.json index 5d2c78f..726882d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { - "tslib" : ["./node_modules/tslib/tslib.d.ts"] + "tslib": ["./node_modules/tslib/tslib.d.ts"] } } }