From cf579c28bf2be7aeed54971f61a70be4215b2551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Marie=20De=20Mey?= Date: Wed, 15 May 2024 11:49:50 +0300 Subject: [PATCH] packaging --- README.md | 4 ++- docs/README.md | 26 ++++++++++++-- docs/db.md | 79 +++++++++++++++++++++++------------------- package-lock.json | 40 +++++---------------- package.json | 24 ++++++------- rollup.config.js | 35 ++++++++++--------- src/db/fileDb.ts | 2 +- src/db/memDb.ts | 8 +++-- src/types.ts | 40 ++++++++++++--------- test/specifics.test.ts | 3 ++ test/static.test.ts | 8 ++--- tsconfig.json | 4 +-- 12 files changed, 144 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index ffd10a4..c700a71 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Generic i18n library managing the fullstack interaction in a CI/CD pace. The dic - 1.0.x - ~~alpha~~ - 1.1.x - [beta](https://www.youtube.com/watch?v=1gSZfX91zYk) +🎉 1.1.5 🥳 The library finally has well-set entry points and export bundles + [![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) [![Github repo dependents](https://badgen.net/github/dependents-repo/emedware/omni18n)](https://github.com/emedware/omni18n/network/dependents?dependent_type=REPOSITORY) @@ -29,7 +31,7 @@ The server: The client part is a [`I18nClient`](./docs/client.md) that will remember a locale and manage the queries to a server and language changes. This client will produce `Translator`s who are described in typescript by the type `any`, or you can specify yours for your dictionary structure. -> :warning: The library has 2 entry points: `omni18n/server` and `omni18n/client`. Please only load the latter in the browser. (The `server` contains indeed everything) +> :warning: The library has 2 entry points: `omni18n` and `omni18n/client`. Only load the latter in the browser. ### Server side diff --git a/docs/README.md b/docs/README.md index fcd1956..b110635 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,8 +2,6 @@ The first document presents an [overview](../README.md), here is a more detailed description -> :warning: The library has 2 entry points: `omni18n/server` and `omni18n/client`. Please only load the latter in the browser. (The `server` contains indeed everything) - Projects using OmnI18n use it in 4 layers 1. [The `client`](./client.md): The client manages the cache and download along with providing [`Translator`s](./translator.md) that will [interpolate](./interpolation.md) @@ -11,7 +9,29 @@ Projects using OmnI18n use it in 4 layers 3. [The `server`](./server.md): The server exposes functions to interact with the languages 4. [The `database`](./db.md): A class implementing some interface that interacts directly with a database -## Bonus - flags +## Entry points + +The library has 2x2 entry points: + +client/server: The server functionalities are not needed **and** harmful on client-side (try to ask chrome to import `node:fs` ...) + +- The complete library `omni18n` +- The client part `omni18n/client` + +bundled/source: The sources (TypeScript) are provided so that you can use your favorite bundler/debugger + +- The bundled `omni18n` +- The source `omni18n/src` + +And of course `omni18n/src/client` for the 2x2... + +### umd + +On the client side, it is also possible to reference the file `lib/omni18n.js` statically in the HTML code, every functionality will be in the `OmnI18n` global variable. + +## Bonus + +### Flags ```js import { localeFlags, flagCodeExceptions } diff --git a/docs/db.md b/docs/db.md index 3d4ec7f..0bdf82d 100644 --- a/docs/db.md +++ b/docs/db.md @@ -12,7 +12,7 @@ In mongo or json-oriented DB, a key object could directly give a list of transla The database interface is the one provided to the server. The first one is fairly simple: ```ts -type RawDictionary = Record +export type RawDictionary = Record interface DB { list(locales: Locale[], zone: Zone): Promise @@ -25,32 +25,34 @@ That `list` function is a glorified `SELECT` who gives all keys given in a zone The DB role is purely to deal with a database. The [`server`](./server.md) will often mimic functions and their signature (`modify`, `reKey`, ...) and while the `server` role is to propagate information to both the client and the DB, a DB's role is purely the one of an adapter toward a database. -## InteractiveDB +### Glorified strings -A kind of API has been designed for the server to be able to _modify_ the content of the DB. +`Locale`, `Zone`, `TextKey`, `Translation` ... are basically strings. -### Infos +## TranslatableDB -Here, we get already in the realm where we can specify `KeyInfos` and `TextInfos`. The former is given by developers, in english or some common language if text is needed - and appear in the `keys` database - and the `TextInfo`, more often used/edited by the translators and appearing in the `translations` database. +Translation occur with simple "select/upsert" operations. There is _no key management_ here, no structure management, just content edition -If a database implementation is meant to be generic, it should store the `...Infos` as json I guess or something, but an application can specify both these generic arguments _and_ the database adapter to deal with it. +### ...Infos + +Here, we get already in the realm where we can specify `KeyInfos` and `TextInfos`. + +The former is given by developers, in english or some common language if comments are needed, it might contain the type (text/html/md/...) for the translation interface, &c. - and appear in the `keys` database -The `KeyInfo` might store information like notes from the dev, a flag to know if the text is pure, html, md, ... Whatever concerns development. +The latter more often used/edited by the translators and appearing in the `translations` database. (comment, "Keep the default value"="Do not translate" tag, &c.) -The `Textinfo` might store translation notes I guess, a link to a discussion with chatGPT, I really don't know - in case of doubt, let the default `{}` +If a database implementation is meant to be generic, it should store the `...Infos` as json I guess or something, but an application can specify both these generic arguments _and_ the database adapter to deal with it. ```ts ...Infos extends {} = {} ``` -### Specific getters - -#### Work list +### Work list ```ts type WorkDictionaryText = { - text: string - infos: TextInfos + text?: Translation + infos?: TextInfos } type WorkDictionaryEntry = { texts: { [locale: Locale]: WorkDictionaryText } @@ -58,53 +60,56 @@ type WorkDictionaryEntry = { infos: KeyInfos } type WorkDictionary = Record + workList(locales: Locale[]): Promise ``` Given a list of locales, find all their translations -> No `zone` fuss, and it's not "the first translation", it's all of them. +> No `zone` fuss: this is read-only at this level, and it's not "the first translation", it's all of them. This function is indeed used to populate translator's list for working on it ... working list. -#### Get a single key, check whether a translation is specified +### Translate -```ts -get(key: string): Promise> -getZone(key: TextKey, locales?: Locale[]): Promise -``` - -The first one retrieves the list of translations for a key, the second the key's zone IF some of the locales have a translation - -### Setters +Sets the text/`TextInfo` for a given text-key/locale pair. -#### Translate +Returns the zone of the text-key if modified, `false` if the text-key was not found > Write in the texts table, read in the keys table ```ts modify( - key: string, + key: TextKey, locale: Locale, - text: string, + text: Translation, textInfos?: Partial -): Promise +): Promise ``` -Sets the text/`TextInfo` for a given text-key/locale pair. - -Returns the zone of the text-key if modified, `false` if the text-key was not found +## EditableDB -#### Key management +Provides edition for the developer. (note: the querying goes through `workList` of `TranslatableDB`) ```ts key(key: string, zone: string, keyInfos?: Partial): Promise -reKey(key: string, newKey?: string): Promise<{ zone: string; locales: Locale[] }> +reKey(key: string, newKey?: string): Promise<{ zone: Zone; locales: Locale[] }> ``` `key` just upsert a key and its relative information. -`reKey` renames a key - into oblivion if no `newKey` (in the later case, remove also the translations) +`reKey` renames a key - into oblivion if no `newKey` (in the later case, removes also the translations) + +## InteractiveDB + +The last one has some little query functions used in interactive mode (ie. when the text changes should be populated to all clients when done) + +```ts +get(key: string): Promise> +getZone(key: TextKey, locales?: Locale[]): Promise +``` + +The first one retrieves the list of translations for a key, the second the key's zone IF some of the locales have a translation ## Provided providers @@ -121,9 +126,11 @@ This allows: - All the translations to simply be gathered under a file under source control (backup-able) - The development activities (adding/removing/removing/rezoning a key) to be made and applied on commit/merge, and the "translation" (text-change) activities to still be available through the UI in real time +> :warning: The file should be in UTF-16 LE in strict `LF` mode + #### Recovering a file to export to a database -An `FileDB.analyze` function is exposed who takes the string to analyze and 2/3 callbacks +A `FileDB.analyze` function is exposed who takes the string to analyze and 2/3 callbacks - `onKey` called when a new key is discovered - `onText` called when a translation is discovered @@ -156,11 +163,11 @@ A line beginning with no tabs is a key specification A line beginning with one tab is a locale specification for the key "en cours" ``` - [locale]: + [locale]:Some fancy translation ``` ``` - [locale][{ SomeTextInfos: 'hjson format' }]: + [locale][{ SomeTextInfos: 'hjson format' }]:Some fancy translation ``` ##### 2-tabs diff --git a/package-lock.json b/package-lock.json index 8981832..b0d015a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omni18n", - "version": "1.1.2", + "version": "1.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omni18n", - "version": "1.1.2", + "version": "1.1.5", "license": "ISC", "dependencies": { "hjson": "^3.2.2" @@ -23,7 +23,7 @@ "prettier": "^3.2.5", "rollup": "^4.16.4", "rollup-plugin-dts": "^6.1.0", - "rollup-plugin-typescript": "^1.0.1", + "rollup-plugin-shim": "^1.0.0", "rollup-plugin-typescript2": "^0.36.0", "ts-jest": "^29.1.2", "tslib": "^2.6.2", @@ -5345,20 +5345,11 @@ "typescript": "^4.5 || ^5.0" } }, - "node_modules/rollup-plugin-typescript": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-typescript/-/rollup-plugin-typescript-1.0.1.tgz", - "integrity": "sha512-rwJDNn9jv/NsKZuyBb/h0jsclP4CJ58qbvZt2Q9zDIGILF2LtdtvCqMOL+Gq9IVq5MTrTlHZNrn8h7VjQgd8tw==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-typescript.", - "dev": true, - "dependencies": { - "resolve": "^1.10.0", - "rollup-pluginutils": "^2.5.0" - }, - "peerDependencies": { - "tslib": "*", - "typescript": ">=2.1.0" - } + "node_modules/rollup-plugin-shim": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-shim/-/rollup-plugin-shim-1.0.0.tgz", + "integrity": "sha512-rZqFD43y4U9nSqVq3iyWBiDwmBQJY8Txi04yI9jTKD3xcl7CbFjh1qRpQshUB3sONLubDzm7vJiwB+1MEGv67w==", + "dev": true }, "node_modules/rollup-plugin-typescript2": { "version": "0.36.0", @@ -5390,21 +5381,6 @@ "node": ">= 8.0.0" } }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1" - } - }, - "node_modules/rollup-pluginutils/node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index 27d0df3..8ff8d93 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,28 @@ { "name": "omni18n", - "version": "1.1.4", + "version": "1.1.5", "exports": { "./client": { - "import": "./lib/client.esm.js", - "require": "./lib/client.js", - "types": "./lib/client.d.ts" + "import": "./lib/esm/client.js", + "require": "./lib/cjs/client.js", + "types": "./lib/src/client.d.ts" }, - "./server": { - "import": "./lib/server.esm.js", - "require": "./lib/server.js", - "types": "./lib/index.d.ts" + ".": { + "import": "./lib/esm/index.js", + "require": "./lib/cjs/index.js", + "types": "./lib/src/index.d.ts" }, "./src/client": { "import": "./src/client.ts", - "require": "./src/client.ts", "types": "./src/client.ts" }, - "./src/server": { + "./src": { "import": "./src/index.ts", - "require": "./src/index.ts", "types": "./src/index.ts" } }, "description": "", - "types": "./lib/index.d.ts", + "types": "./lib/src/index.d.ts", "type": "module", "scripts": { "test": "jest", @@ -60,7 +58,7 @@ "prettier": "^3.2.5", "rollup": "^4.16.4", "rollup-plugin-dts": "^6.1.0", - "rollup-plugin-typescript": "^1.0.1", + "rollup-plugin-shim": "^1.0.0", "rollup-plugin-typescript2": "^0.36.0", "ts-jest": "^29.1.2", "tslib": "^2.6.2", diff --git a/rollup.config.js b/rollup.config.js index be1a72f..9229708 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,6 +1,8 @@ import resolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import typescript from 'rollup-plugin-typescript2' +import pluginDts from 'rollup-plugin-dts' +import shim from 'rollup-plugin-shim' import { rm } from 'node:fs/promises' // clean out the destination folder @@ -8,12 +10,9 @@ await rm('lib', { recursive: true, force: true }) export default [ { - input: './src/client.ts', + input: ['src/index.ts', 'src/client.ts'], output: { - file: 'lib/client.js', - sourcemap: true, - format: 'cjs', - exports: 'named' + dir: 'lib' }, external: ['hjson'], plugins: [ @@ -24,15 +23,16 @@ export default [ include: ['./src'], exclude: ['./node_modules'] } - }) + }), + pluginDts() ] }, { - input: './src/client.ts', + input: ['src/index.ts', 'src/client.ts'], output: { - file: 'lib/client.esm.js', + dir: 'lib/cjs', sourcemap: true, - format: 'esm', + format: 'cjs', exports: 'named' }, external: ['hjson'], @@ -48,11 +48,11 @@ export default [ ] }, { - input: './src/index.ts', + input: ['src/index.ts', 'src/client.ts'], output: { - file: 'lib/server.js', + dir: 'lib/esm', sourcemap: true, - format: 'cjs' + format: 'esm' }, external: ['hjson'], plugins: [ @@ -67,14 +67,17 @@ export default [ ] }, { - input: './src/index.ts', + input: 'src/client.ts', output: { - file: 'lib/server.esm.js', + file: 'lib/omni18n.js', sourcemap: true, - format: 'esm' + format: 'umd', + name: 'OmnI18n' }, - external: ['hjson'], plugins: [ + shim({ + os: 'module.export = {}' + }), resolve(), commonjs(), typescript({ diff --git a/src/db/fileDb.ts b/src/db/fileDb.ts index 66976a4..9893877 100644 --- a/src/db/fileDb.ts +++ b/src/db/fileDb.ts @@ -169,7 +169,7 @@ export default class FileDB extends M let lastIndex = 0 while ((keyFetch = rex.key.exec(data))) { if (keyFetch.index > lastIndex) throw parseError(data, lastIndex, keyFetch.index) - const key = keyFetch[1], + const key = keyFetch[1].trim(), zone = keyFetch[3] as Zone let keyInfos: any if (keyFetch[2]) keyInfos = parse(keyFetch[2].replace(/\u0000/g, '\n')) diff --git a/src/db/memDb.ts b/src/db/memDb.ts index 07fa8c5..b852848 100644 --- a/src/db/memDb.ts +++ b/src/db/memDb.ts @@ -74,7 +74,7 @@ export default class MemDB } async modify(key: TextKey, locale: Locale, value: Translation, textInfos?: Partial) { - if (!this.dictionary[key]) throw new Error(`Key "${key}" not found`) + if (!this.dictionary[key]) return false if (!/^[\w-]*$/g.test(locale)) throw new Error(`Bad locale: ${locale} (only letters, digits, "_" and "-" allowed)`) this.dictionary[key][locale] = value @@ -92,8 +92,10 @@ export default class MemDB async key(key: TextKey, zone: Zone, keyInfos?: Partial) { const entry = this.dictionary[key] || {}, ez = entry['.zone'] - if (!/^[\w\-\+\*\.]*$/g.test(key)) - throw new Error(`Bad key-name: ${key} (only letters, digits, "_+-*." allowed)`) + if (!/^[\w\-\+\*\.]*$/g.test(key) || `.${key}.`.includes('.then.')) + throw new Error( + `Bad key-name: ${key} (only letters, digits, "_+-*." allowed) (and no "then" part)` + ) this.dictionary[key] = >{ ...entry, ...((entry['.keyInfos'] || keyInfos) && { diff --git a/src/types.ts b/src/types.ts index 8241434..dd475c5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,8 +20,8 @@ export type OnModification = (keys?: TextKey[]) => void */ export type RawDictionary = Record export type WorkDictionaryText = { - text: Translation - infos: TextInfos + text?: Translation + infos?: TextInfos } export type WorkDictionaryEntry = { texts: { [locale: Locale]: WorkDictionaryText } @@ -43,26 +43,13 @@ export interface DB { list(locales: Locale[], zone: Zone): Promise } -export interface InteractiveDB extends DB { +export interface TranslatableDB extends DB { /** * Retrieves all the values for certain locales, in order for translators to work on it * @param locales */ workList(locales: Locale[]): Promise - /** - * Retrieves all the translations given for a certain key - * @param key The key to search for - */ - get(key: TextKey): Promise> - - /** - * Checks if a key is specified in a certain locale - * @param key The key to search for - * @param locales The locales to search in - */ - getZone(key: TextKey, locales?: Locale[]): Promise - /** * Modifies/add the value for the key [key, locale] * Note: checks that the key exists @@ -77,12 +64,15 @@ export interface InteractiveDB ): Promise +} +export interface EditableDB + extends TranslatableDB { /** * Creates or modifies a key * @param key The key to manipulate * @param zone The zone to create the key in, or undefined to delete it - * @returns Wether there was a change + * @returns Wether the zone was changed */ key(key: TextKey, zone: Zone, keyInfos?: Partial): Promise @@ -94,3 +84,19 @@ export interface InteractiveDB }> } + +export interface InteractiveDB + extends EditableDB { + /** + * Retrieves all the translations given for a certain key + * @param key The key to search for + */ + get(key: TextKey): Promise> + + /** + * Checks if a key is specified in a certain locale + * @param key The key to search for + * @param locales The locales to search in + */ + getZone(key: TextKey, locales?: Locale[]): Promise +} diff --git a/test/specifics.test.ts b/test/specifics.test.ts index aee71f5..8143af8 100644 --- a/test/specifics.test.ts +++ b/test/specifics.test.ts @@ -114,6 +114,9 @@ describe('specifics', () => { expect('' + T.fld.inexistent).toBe('[no]') expect(misses).toHaveBeenCalledWith('fld.inexistent') }) +}) + +describe('fileDB', () => { test('serialize', () => { const content: MemDBDictionary = { 'serializations.nl1': { diff --git a/test/static.test.ts b/test/static.test.ts index 66290cd..5f90ece 100644 --- a/test/static.test.ts +++ b/test/static.test.ts @@ -218,11 +218,9 @@ describe('formatting', () => { }) describe('parameters', () => { test('zones', async () => { - /* We cannot predict the order - expect(loads).toEqual([ - { locale: 'en-US', zones: ['', 'adm'] }, - { locale: 'fr-BE', zones: [''] } - ])*/ + expect(loads.length).toBe(2) + expect(loads).toContainEqual({ locales: ['en-US'], zones: ['adm', ''] }) + expect(loads).toContainEqual({ locales: ['fr-BE'], zones: [''] }) expect(T.en.cmd.ban()).toBe('Ban user') expect(T.be.cmd.ban()).toBe('[cmd.ban]') loads = [] diff --git a/tsconfig.json b/tsconfig.json index d23bc0d..c302d11 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,8 @@ "moduleResolution": "Node", "target": "es2015", "module": "ESNext", - "outDir": "./lib", - "declaration": true, + "outDir": "docs/lib", + "declaration": false, "strict": true, "esModuleInterop": true, "skipLibCheck": true,