diff --git a/README.md b/README.md index 2da132f..ff4b5a5 100644 --- a/README.md +++ b/README.md @@ -5,41 +5,7 @@ Live example on [StackBlitz](https://stackblitz.com/edit/qwik-speak) -## Speak context -```mermaid -stateDiagram-v2 - State1: SpeakState - State2: SpeakLocale - State3: Translation - State4: SpeakConfig - State5: TranslateFn - State1 --> State2 - State1 --> State3 - State1 --> State4 - State1 --> State5 - note right of State2 - - lang - - extension (Intl) - - currency - - timezone - - unit - end note - note right of State3 - key-value pairs - of translation data - end note - note right of State4: Configuration - note right of State5 - Custom APIs: - - loadTranslation$ - - resolveLocale$ - - storeLocale$ - - handleMissingTranslation$ - end note -``` - ## Usage -### Getting started ```shell npm install qwik-speak --save-dev ``` @@ -50,8 +16,9 @@ import { $translate as t, plural as p } from 'qwik-speak'; export default component$(() => { return ( <> -

{t('app.title', { name: 'Qwik Speak' })}

{/* I'm Qwik Speak */} -

{p(1, 'app.devs')}

{/* 1 software developer */} +

{t('app.title')}

{/* Qwik Speak */} +

{t('home.greeting', { name: 'Qwik Speak' })}

{/* Hi! I am Qwik Speak */} +

{p(state.count, 'runtime.devs')}

{/* 1 software developer, 2 software developers */} ); }); @@ -64,7 +31,7 @@ export default component$(() => { return ( <>

{fd(Date.now(), { dateStyle: 'full', timeStyle: 'short' })}

{/* Wednesday, July 20, 2022 at 7:09 AM */} -

{rt(-1, 'day')}

{/* 1 day ago */} +

{rt(-1, 'second')}

{/* 1 second ago */}

{fn(1000000, { style: 'currency' })}

{/* $1,000,000.00 */} ); @@ -85,19 +52,16 @@ export const config: SpeakConfig = { ] }; ``` -Assets will be loaded through the implementation of `loadTranslation$` function below. You can load _json_ files or call an _endpoint_ to return a `Translation` object for each language: +Assets will be loaded through the implementation of `loadTranslation$` function below. You can load _json_ files or call an _endpoint_ to return a `Translation` object of key-value pairs for each language: + ```json { "app": { - "title": "I'm {{name}}", - "devs": { - "one": "{{value}} software developer", - "other": "{{value}} software developers" - } + "title": "Qwik Speak" } } ``` -### Custom APIs +#### Custom APIs ```typescript import { $ } from '@builder.io/qwik'; @@ -153,7 +117,19 @@ export default component$(() => { ); }); ``` -### Lazy loading of translation data +### Scoped translations +```mermaid +C4Container + Container_Boundary(a, "App") { + Component(a0, "QwikSpeak", "", "Uses Speak context") + Container_Boundary(b1, "Home") { + Component(a10, "Speak", "", "Adds its own translation data to the context") + } + Container_Boundary(b2, "Page") { + Component(a20, "Speak", "", "Adds its own translation data to the context") + } + } +``` Create a different translation data file (asset) for each page and use `Speak` component to add translation data to the context: ```jsx import { Speak } from 'qwik-speak'; @@ -161,7 +137,7 @@ import { Speak } from 'qwik-speak'; export default component$(() => { return ( /** - * Add Home translation (only available in child components) + * Add Home translations (only available in child components) */ @@ -183,17 +159,7 @@ export default component$(() => { ``` The translation data of the additional languages are preloaded along with the current language. They can be used as a fallback for missing values by implementing `handleMissingTranslation$`, or for multilingual pages. -## Production -You have three solutions: -- **Build as is** Translation happens _at runtime_: translations are loaded during SSR or on client, and the lookup also happens at runtime as in development mode -- **Build using Qwik Speak Inline Vite plugin** Translation happens _at compile-time_: translations are loaded and inlined during the buid (both in server file and in chunks sent to the browser) -- **Build using Qwik Speak Inline Vite plugin & runtime** Translation happens _at compile-time_ or _at runtime_ as needed: static translations are loaded and inlined during the buid, while dynamic translations occur at runtime - -See [Qwik Speak Inline Vite plugin](./tools/inline.md) for more information on how it works and how to use it. - -### Static Site Generation (SSG) -Using SSG offered by Qwik City, you can prerender the pages for each language. - +### Localized routing What you need: - A `lang` parameter in the root, like: ``` @@ -206,9 +172,21 @@ What you need: index.html ``` - Handle the localized routing in `resolveLocale$` and `storeLocale$` -- Qwik City Static Site Generation config and dynamic routes -The [sample app](./src/app) in this project uses _Qwik Speak Inline Vite plugin & runtime_ solution and implements SSG. +## Extraction of translations +To extract translations directly from the components, a command is available that automatically generates the files with the keys and default values. + +See [Qwik Speak Extract](./tools/extract.md) for more information on how to use it. + +## Production +You have three solutions: +- **Build as is** Translation happens _at runtime_: translations are loaded during SSR or on client, and the lookup also happens at runtime as in development mode +- **Build using Qwik Speak Inline Vite plugin** Translation happens _at compile-time_: translations are loaded and inlined during the build (both in server file and in chunks sent to the browser) +- **Build using Qwik Speak Inline Vite plugin & runtime** Translation happens _at compile-time_ or _at runtime_ as needed: static translations are loaded and inlined during the build, while dynamic translations occur at runtime + +See [Qwik Speak Inline Vite plugin](./tools/inline.md) for more information on how it works and how to use it. + +The [sample app](./src/app) in this project uses a localized routing, _Qwik Speak Inline Vite plugin & runtime_ solution and implements SSG. ## Speak config - `defaultLocale` @@ -226,7 +204,7 @@ Separator of nested keys. Default is `.` - `keyValueSeparator` Key-value separator. Default is `@@` - The default value of a key can be passed directly into the string: `t("app.title@@I'm {{name}}")` + The default value of a key can be passed directly into the string: `t('app.title@@Qwik Speak')` The `SpeakLocale` object contains the `lang`, in the format `language[-script][-region]`, where: - `language`: ISO 639 two-letter or three-letter code @@ -258,9 +236,42 @@ Formats a number - `changeLocale(newLocale: SpeakLocale, ctx: SpeakState)` Changes locale at runtime: loads translation data and rerenders components that uses translations + ### Speak context +```mermaid +stateDiagram-v2 + State1: SpeakState + State2: SpeakLocale + State3: Translation + State4: SpeakConfig + State5: TranslateFn + State1 --> State2 + State1 --> State3 + State1 --> State4 + State1 --> State5 + note right of State2 + - lang + - extension (Intl) + - currency + - timezone + - unit + end note + note right of State3 + key-value pairs + of translation data + end note + note right of State4: Configuration + note right of State5 + Custom APIs: + - loadTranslation$ + - resolveLocale$ + - storeLocale$ + - handleMissingTranslation$ + end note +``` + - `useSpeakContext()` -Returns the Speak context +Returns the Speak state - `useSpeakLocale()` Returns the locale in Speak context @@ -305,8 +316,9 @@ npm run serve.ssg ``` ## What's new -> Released v0.1.0 +> Released v0.2.0 +- Extract translations: [Qwik Speak Extract](./tools/extract.md) - Inline translation data at compile time: [Qwik Speak Inline Vite plugin](./tools/inline.md) ## License diff --git a/adaptors/express/vite.config.ts b/adaptors/express/vite.config.ts index b08fb39..e09c790 100644 --- a/adaptors/express/vite.config.ts +++ b/adaptors/express/vite.config.ts @@ -11,9 +11,6 @@ export default extendConfig(baseConfig, () => { }, }, plugins: [ - expressAdaptor({ - staticGenerate: true, - }), ], }; }); diff --git a/banner.txt b/banner.txt new file mode 100644 index 0000000..9e4e43f --- /dev/null +++ b/banner.txt @@ -0,0 +1,6 @@ +/** + * @license + * Qwik Speak + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/robisim74/qwik-speak/blob/main/LICENSE + */ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bc2ca77..2fee357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,17 +12,17 @@ "qwik-speak-extract": "extract/cli.js" }, "devDependencies": { - "@builder.io/qwik": "0.12.1", - "@builder.io/qwik-city": "0.0.118", + "@builder.io/qwik": "0.13.3", + "@builder.io/qwik-city": "0.0.122", "@microsoft/api-extractor": "^7.32.0", "@playwright/test": "^1.24.0", "@types/compression": "^1.7.2", - "@types/eslint": "8.4.8", + "@types/eslint": "8.4.9", "@types/express": "4.17.13", "@types/jest": "latest", "@types/node": "latest", - "@typescript-eslint/eslint-plugin": "5.41.0", - "@typescript-eslint/parser": "5.41.0", + "@typescript-eslint/eslint-plugin": "5.42.0", + "@typescript-eslint/parser": "5.42.0", "compression": "^1.7.4", "eslint": "8.26.0", "eslint-plugin-qwik": "latest", @@ -30,17 +30,17 @@ "jest": "^29.1.1", "node-fetch": "3.2.10", "np": "7.6.2", + "rollup-plugin-add-shebang": "^0.3.1", "ts-jest": "^29.0.3", "typescript": "4.8.4", - "vite": "3.2.0", - "vite-plugin-static-copy": "^0.9.0", + "vite": "3.2.2", "vite-tsconfig-paths": "3.5.0" }, "engines": { "node": ">=16" }, "peerDependencies": { - "@builder.io/qwik": ">=0.12.1" + "@builder.io/qwik": ">=0.13.3" } }, "node_modules/@ampproject/remapping": { @@ -649,9 +649,9 @@ "dev": true }, "node_modules/@builder.io/qwik": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@builder.io/qwik/-/qwik-0.12.1.tgz", - "integrity": "sha512-ePxm9tr6qa+cNySuIhXOElozkkqdRfyPvEPEV659QJSq704C031/VC0S2lqqvKsc9+ZogBlejGuYWLmtRrsT6w==", + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@builder.io/qwik/-/qwik-0.13.3.tgz", + "integrity": "sha512-revtQDBww5lYM6oEsmdRk0n+Z38sFghxjbpyNZFjN3h20FHoym7fXwxSUag+0wdeU9c3CRWDKoLJFtnngcLfCg==", "dev": true, "bin": { "qwik": "qwik.cjs" @@ -661,9 +661,9 @@ } }, "node_modules/@builder.io/qwik-city": { - "version": "0.0.118", - "resolved": "https://registry.npmjs.org/@builder.io/qwik-city/-/qwik-city-0.0.118.tgz", - "integrity": "sha512-vkc20xK3HUr6rviVpDeOwbzCfo6QTjZWBL9oyGYcYLfqqC1gvvuc+zf548APAwbTQa+4/7pzJCNMVixvDyD/4g==", + "version": "0.0.122", + "resolved": "https://registry.npmjs.org/@builder.io/qwik-city/-/qwik-city-0.0.122.tgz", + "integrity": "sha512-p1fMVvzREKjEnq2Mf2F4D272OaguA4uSj9qxmCXHZmvG5T355QFRGlq6tTgC7nJAqwlTAB8thhB/3kykfBO/Ng==", "dev": true, "dependencies": { "@mdx-js/mdx": "2.1.5", @@ -1648,9 +1648,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.4.8", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.8.tgz", - "integrity": "sha512-zUCKQI1bUCTi+0kQs5ZQzQ/XILWRLIlh15FXWNykJ+NG3TMKMVvwwC6GP3DR1Ylga15fB7iAExSzc4PNlR5i3w==", + "version": "8.4.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.9.tgz", + "integrity": "sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==", "dev": true, "dependencies": { "@types/estree": "*", @@ -1896,16 +1896,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.41.0.tgz", - "integrity": "sha512-DXUS22Y57/LAFSg3x7Vi6RNAuLpTXwxB9S2nIA7msBb/Zt8p7XqMwdpdc1IU7CkOQUPgAqR5fWvxuKCbneKGmA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz", + "integrity": "sha512-5TJh2AgL6+wpL8H/GTSjNb4WrjKoR2rqvFxR/DDTqYNk6uXn8BJMEcncLSpMbf/XV1aS0jAjYwn98uvVCiAywQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/type-utils": "5.41.0", - "@typescript-eslint/utils": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/type-utils": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" @@ -1928,14 +1929,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.41.0.tgz", - "integrity": "sha512-HQVfix4+RL5YRWZboMD1pUfFN8MpRH4laziWkkAzyO1fvNOY/uinZcvo3QiFJVS/siNHupV8E5+xSwQZrl6PZA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.0.tgz", + "integrity": "sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/typescript-estree": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "debug": "^4.3.4" }, "engines": { @@ -1955,13 +1956,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.41.0.tgz", - "integrity": "sha512-xOxPJCnuktUkY2xoEZBKXO5DBCugFzjrVndKdUnyQr3+9aDWZReKq9MhaoVnbL+maVwWJu/N0SEtrtEUNb62QQ==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz", + "integrity": "sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/visitor-keys": "5.41.0" + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1972,13 +1973,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.41.0.tgz", - "integrity": "sha512-L30HNvIG6A1Q0R58e4hu4h+fZqaO909UcnnPbwKiN6Rc3BUEx6ez2wgN7aC0cBfcAjZfwkzE+E2PQQ9nEuoqfA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.0.tgz", + "integrity": "sha512-HW14TXC45dFVZxnVW8rnUGnvYyRC0E/vxXShFCthcC9VhVTmjqOmtqj6H5rm9Zxv+ORxKA/1aLGD7vmlLsdlOg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.41.0", - "@typescript-eslint/utils": "5.41.0", + "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -1999,9 +2000,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.41.0.tgz", - "integrity": "sha512-5BejraMXMC+2UjefDvrH0Fo/eLwZRV6859SXRg+FgbhA0R0l6lDqDGAQYhKbXhPN2ofk2kY5sgGyLNL907UXpA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz", + "integrity": "sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2012,13 +2013,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.41.0.tgz", - "integrity": "sha512-SlzFYRwFSvswzDSQ/zPkIWcHv8O5y42YUskko9c4ki+fV6HATsTODUPbRbcGDFYP86gaJL5xohUEytvyNNcXWg==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz", + "integrity": "sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/visitor-keys": "5.41.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2039,16 +2040,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.41.0.tgz", - "integrity": "sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz", + "integrity": "sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/typescript-estree": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -2065,12 +2066,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.41.0.tgz", - "integrity": "sha512-vilqeHj267v8uzzakbm13HkPMl7cbYpKVjgFWZPIOHIJHZtinvypUhJ5xBXfWYg4eFKqztbMMpOgFpT9Gfx4fw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz", + "integrity": "sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/types": "5.42.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2401,15 +2402,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", @@ -2767,45 +2759,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/ci-info": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.4.0.tgz", @@ -5605,18 +5558,6 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", @@ -7624,6 +7565,15 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -8672,6 +8622,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -9997,18 +9953,6 @@ "node": ">=8" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/recrawl-sync": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recrawl-sync/-/recrawl-sync-2.2.2.tgz", @@ -10254,6 +10198,31 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-add-shebang": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-add-shebang/-/rollup-plugin-add-shebang-0.3.1.tgz", + "integrity": "sha512-tKONSgKoVw9Om1cp1CnAlPQ9nsHBzu8fInKObX3zT5KZVoAJtslD1aBL84lJuKLeh+L28dB26CBBeYT+doTMLg==", + "dev": true, + "dependencies": { + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "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-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -10521,6 +10490,12 @@ "node": ">=0.10.0" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, "node_modules/space-separated-tokens": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz", @@ -11494,9 +11469,9 @@ } }, "node_modules/vite": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.0.tgz", - "integrity": "sha512-Ovj7+cqIdM1I0LPCk2CWxzgADXMix3NLXpUT6g7P7zg/a9grk/TaC3qn9YMg7w7M0POIVCBOp1aBANJW+RH7oA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz", + "integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==", "dev": true, "dependencies": { "esbuild": "^0.15.9", @@ -11538,59 +11513,6 @@ } } }, - "node_modules/vite-plugin-static-copy": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-0.9.0.tgz", - "integrity": "sha512-0h8esPoZn6zdTK8KoDbiZPJum7+Nw2t4oLTpu2i2haP2HmgysRH+Xy6FaE6lLyCpgODYR5RiRjbLH0UKUZpIPw==", - "dev": true, - "dependencies": { - "chokidar": "^3.5.3", - "fast-glob": "^3.2.11", - "fs-extra": "^10.1.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^3.0.0" - } - }, - "node_modules/vite-plugin-static-copy/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-plugin-static-copy/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/vite-plugin-static-copy/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/vite-tsconfig-paths": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-3.5.0.tgz", @@ -12307,15 +12229,15 @@ "dev": true }, "@builder.io/qwik": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@builder.io/qwik/-/qwik-0.12.1.tgz", - "integrity": "sha512-ePxm9tr6qa+cNySuIhXOElozkkqdRfyPvEPEV659QJSq704C031/VC0S2lqqvKsc9+ZogBlejGuYWLmtRrsT6w==", + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@builder.io/qwik/-/qwik-0.13.3.tgz", + "integrity": "sha512-revtQDBww5lYM6oEsmdRk0n+Z38sFghxjbpyNZFjN3h20FHoym7fXwxSUag+0wdeU9c3CRWDKoLJFtnngcLfCg==", "dev": true }, "@builder.io/qwik-city": { - "version": "0.0.118", - "resolved": "https://registry.npmjs.org/@builder.io/qwik-city/-/qwik-city-0.0.118.tgz", - "integrity": "sha512-vkc20xK3HUr6rviVpDeOwbzCfo6QTjZWBL9oyGYcYLfqqC1gvvuc+zf548APAwbTQa+4/7pzJCNMVixvDyD/4g==", + "version": "0.0.122", + "resolved": "https://registry.npmjs.org/@builder.io/qwik-city/-/qwik-city-0.0.122.tgz", + "integrity": "sha512-p1fMVvzREKjEnq2Mf2F4D272OaguA4uSj9qxmCXHZmvG5T355QFRGlq6tTgC7nJAqwlTAB8thhB/3kykfBO/Ng==", "dev": true, "requires": { "@mdx-js/mdx": "2.1.5", @@ -13126,9 +13048,9 @@ } }, "@types/eslint": { - "version": "8.4.8", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.8.tgz", - "integrity": "sha512-zUCKQI1bUCTi+0kQs5ZQzQ/XILWRLIlh15FXWNykJ+NG3TMKMVvwwC6GP3DR1Ylga15fB7iAExSzc4PNlR5i3w==", + "version": "8.4.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.9.tgz", + "integrity": "sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==", "dev": true, "requires": { "@types/estree": "*", @@ -13374,69 +13296,70 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.41.0.tgz", - "integrity": "sha512-DXUS22Y57/LAFSg3x7Vi6RNAuLpTXwxB9S2nIA7msBb/Zt8p7XqMwdpdc1IU7CkOQUPgAqR5fWvxuKCbneKGmA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz", + "integrity": "sha512-5TJh2AgL6+wpL8H/GTSjNb4WrjKoR2rqvFxR/DDTqYNk6uXn8BJMEcncLSpMbf/XV1aS0jAjYwn98uvVCiAywQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/type-utils": "5.41.0", - "@typescript-eslint/utils": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/type-utils": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "@typescript-eslint/parser": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.41.0.tgz", - "integrity": "sha512-HQVfix4+RL5YRWZboMD1pUfFN8MpRH4laziWkkAzyO1fvNOY/uinZcvo3QiFJVS/siNHupV8E5+xSwQZrl6PZA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.0.tgz", + "integrity": "sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/typescript-estree": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.41.0.tgz", - "integrity": "sha512-xOxPJCnuktUkY2xoEZBKXO5DBCugFzjrVndKdUnyQr3+9aDWZReKq9MhaoVnbL+maVwWJu/N0SEtrtEUNb62QQ==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz", + "integrity": "sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==", "dev": true, "requires": { - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/visitor-keys": "5.41.0" + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0" } }, "@typescript-eslint/type-utils": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.41.0.tgz", - "integrity": "sha512-L30HNvIG6A1Q0R58e4hu4h+fZqaO909UcnnPbwKiN6Rc3BUEx6ez2wgN7aC0cBfcAjZfwkzE+E2PQQ9nEuoqfA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.0.tgz", + "integrity": "sha512-HW14TXC45dFVZxnVW8rnUGnvYyRC0E/vxXShFCthcC9VhVTmjqOmtqj6H5rm9Zxv+ORxKA/1aLGD7vmlLsdlOg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.41.0", - "@typescript-eslint/utils": "5.41.0", + "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.41.0.tgz", - "integrity": "sha512-5BejraMXMC+2UjefDvrH0Fo/eLwZRV6859SXRg+FgbhA0R0l6lDqDGAQYhKbXhPN2ofk2kY5sgGyLNL907UXpA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz", + "integrity": "sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.41.0.tgz", - "integrity": "sha512-SlzFYRwFSvswzDSQ/zPkIWcHv8O5y42YUskko9c4ki+fV6HATsTODUPbRbcGDFYP86gaJL5xohUEytvyNNcXWg==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz", + "integrity": "sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/visitor-keys": "5.41.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -13445,28 +13368,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.41.0.tgz", - "integrity": "sha512-QlvfwaN9jaMga9EBazQ+5DDx/4sAdqDkcs05AsQHMaopluVCUyu1bTRUVKzXbgjDlrRAQrYVoi/sXJ9fmG+KLQ==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz", + "integrity": "sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.41.0", - "@typescript-eslint/types": "5.41.0", - "@typescript-eslint/typescript-estree": "5.41.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.41.0.tgz", - "integrity": "sha512-vilqeHj267v8uzzakbm13HkPMl7cbYpKVjgFWZPIOHIJHZtinvypUhJ5xBXfWYg4eFKqztbMMpOgFpT9Gfx4fw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz", + "integrity": "sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.41.0", + "@typescript-eslint/types": "5.42.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -13698,12 +13621,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, "body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", @@ -13958,33 +13875,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, "ci-info": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.4.0.tgz", @@ -16011,15 +15901,6 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, "is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", @@ -17525,6 +17406,15 @@ "yallist": "^4.0.0" } }, + "magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -18201,6 +18091,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -19165,15 +19061,6 @@ } } }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, "recrawl-sync": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recrawl-sync/-/recrawl-sync-2.2.2.tgz", @@ -19350,6 +19237,33 @@ "fsevents": "~2.3.2" } }, + "rollup-plugin-add-shebang": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-add-shebang/-/rollup-plugin-add-shebang-0.3.1.tgz", + "integrity": "sha512-tKONSgKoVw9Om1cp1CnAlPQ9nsHBzu8fInKObX3zT5KZVoAJtslD1aBL84lJuKLeh+L28dB26CBBeYT+doTMLg==", + "dev": true, + "requires": { + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + }, + "dependencies": { + "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 + } + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -19560,6 +19474,12 @@ } } }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, "space-separated-tokens": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz", @@ -20253,9 +20173,9 @@ } }, "vite": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.0.tgz", - "integrity": "sha512-Ovj7+cqIdM1I0LPCk2CWxzgADXMix3NLXpUT6g7P7zg/a9grk/TaC3qn9YMg7w7M0POIVCBOp1aBANJW+RH7oA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz", + "integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==", "dev": true, "requires": { "esbuild": "^0.15.9", @@ -20265,47 +20185,6 @@ "rollup": "^2.79.1" } }, - "vite-plugin-static-copy": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-0.9.0.tgz", - "integrity": "sha512-0h8esPoZn6zdTK8KoDbiZPJum7+Nw2t4oLTpu2i2haP2HmgysRH+Xy6FaE6lLyCpgODYR5RiRjbLH0UKUZpIPw==", - "dev": true, - "requires": { - "chokidar": "^3.5.3", - "fast-glob": "^3.2.11", - "fs-extra": "^10.1.0", - "picocolors": "^1.0.0" - }, - "dependencies": { - "fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, "vite-tsconfig-paths": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-3.5.0.tgz", diff --git a/package.json b/package.json index 2e12e7d..9410add 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build.app": "qwik build", "build.client": "vite build", "build.server": "vite build -c adaptors/express/vite.config.ts", - "build.types": "tsc --incremental --noEmit", + "build.types": "tsc --noEmit", "dev": "vite --mode ssr", "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", "lint": "eslint src/**/*.ts* tools/**/*.ts*", @@ -20,26 +20,27 @@ "test": "jest ./src/tests ./tools/tests", "test.e2e": "playwright test", "test.watch": "jest ./src/tests ./tools/tests --watch", - "qwik": "qwik" + "qwik": "qwik", + "qwik-speak-extract": "qwik-speak-extract --supportedLangs=en-US,it-IT --sourceFilesPath=src/app" }, "bin": { "qwik-speak-extract": "./extract/cli.js" }, "peerDependencies": { - "@builder.io/qwik": ">=0.12.1" + "@builder.io/qwik": ">=0.13.3" }, "devDependencies": { - "@builder.io/qwik": "0.12.1", - "@builder.io/qwik-city": "0.0.118", + "@builder.io/qwik": "0.13.3", + "@builder.io/qwik-city": "0.0.122", "@microsoft/api-extractor": "^7.32.0", "@playwright/test": "^1.24.0", "@types/compression": "^1.7.2", - "@types/eslint": "8.4.8", + "@types/eslint": "8.4.9", "@types/express": "4.17.13", "@types/jest": "latest", "@types/node": "latest", - "@typescript-eslint/eslint-plugin": "5.41.0", - "@typescript-eslint/parser": "5.41.0", + "@typescript-eslint/eslint-plugin": "5.42.0", + "@typescript-eslint/parser": "5.42.0", "compression": "^1.7.4", "eslint": "8.26.0", "eslint-plugin-qwik": "latest", @@ -49,8 +50,8 @@ "np": "7.6.2", "ts-jest": "^29.0.3", "typescript": "4.8.4", - "vite-plugin-static-copy": "^0.9.0", - "vite": "3.2.0", + "rollup-plugin-add-shebang": "^0.3.1", + "vite": "3.2.2", "vite-tsconfig-paths": "3.5.0" }, "engines": { @@ -96,4 +97,4 @@ "qwik": "./lib/index.qwik.mjs", "type": "module", "types": "./lib/index.d.ts" -} +} \ No newline at end of file diff --git a/public/i18n/en-US/app.json b/public/i18n/en-US/app.json index 4c45ba8..05a534e 100644 --- a/public/i18n/en-US/app.json +++ b/public/i18n/en-US/app.json @@ -1,11 +1,11 @@ { "app": { - "title": "Qwik Speak", - "subtitle": "Translate your Qwik apps into any language", "changeLocale": "Change locale", "nav": { "home": "Home", "page": "Page" - } + }, + "subtitle": "Translate your Qwik apps into any language", + "title": "Qwik Speak" } } \ No newline at end of file diff --git a/public/i18n/en-US/home.json b/public/i18n/en-US/home.json index 2ef084b..c1c0801 100644 --- a/public/i18n/en-US/home.json +++ b/public/i18n/en-US/home.json @@ -1,12 +1,12 @@ { "home": { + "dates": "Dates & relative time", "greeting": "Hi! I am {{name}}", - "text": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps", + "increment": "Increment", + "numbers": "Numbers & currencies", "params": "Parameters", - "tags": "Html tags", "plural": "Plural", - "dates": "Dates & relative time", - "numbers": "Numbers & currencies", - "increment": "Increment" + "tags": "Html tags", + "text": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps" } } \ No newline at end of file diff --git a/public/i18n/en-US/runtime.json b/public/i18n/en-US/runtime.json index 9cfa3ce..2fd7114 100644 --- a/public/i18n/en-US/runtime.json +++ b/public/i18n/en-US/runtime.json @@ -1,16 +1,18 @@ { - "head": { - "home": { - "title": "{{name}}", - "description": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps" + "runtime": { + "devs": { + "one": "{{ value }} software developer", + "other": "{{value}} software developers" }, - "page": { - "title": "Page - {{name}}", - "description": "I'm another page" + "head": { + "home": { + "description": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps", + "title": "{{name}}" + }, + "page": { + "description": "I'm another page", + "title": "Page - {{name}}" + } } - }, - "devs": { - "one": "{{ value }} software developer", - "other": "{{value}} software developers" } } \ No newline at end of file diff --git a/public/i18n/it-IT/app.json b/public/i18n/it-IT/app.json index 92fbe3a..d609ead 100644 --- a/public/i18n/it-IT/app.json +++ b/public/i18n/it-IT/app.json @@ -1,11 +1,11 @@ { "app": { - "title": "Qwik Speak", - "subtitle": "Traduci le tue app Qwik in qualsiasi lingua", "changeLocale": "Cambia località", "nav": { "home": "Home", "page": "Pagina" - } + }, + "subtitle": "Traduci le tue app Qwik in qualsiasi lingua", + "title": "Qwik Speak" } } \ No newline at end of file diff --git a/public/i18n/it-IT/home.json b/public/i18n/it-IT/home.json index fb61379..232cec6 100644 --- a/public/i18n/it-IT/home.json +++ b/public/i18n/it-IT/home.json @@ -1,12 +1,12 @@ { "home": { + "dates": "Date e tempo relativo", "greeting": "Ciao! Sono {{name}}", - "text": "Libreria di internazionalizzazione (i18n) per tradurre testi, date e numeri nelle app Qwik", + "increment": "Incrementa", + "numbers": "Numeri e valute", "params": "Parametri", - "tags": "Tag Html", "plural": "Plurale", - "dates": "Date e tempo relativo", - "numbers": "Numeri e valute", - "increment": "Incrementa" + "tags": "Tag Html", + "text": "Libreria di internazionalizzazione (i18n) per tradurre testi, date e numeri nelle app Qwik" } } \ No newline at end of file diff --git a/public/i18n/it-IT/page.json b/public/i18n/it-IT/page.json index 8350440..b765c29 100644 --- a/public/i18n/it-IT/page.json +++ b/public/i18n/it-IT/page.json @@ -1,3 +1,5 @@ { - "page": {} + "page": { + "text": "I'm a default value" + } } \ No newline at end of file diff --git a/public/i18n/it-IT/runtime.json b/public/i18n/it-IT/runtime.json index 69231ed..8b8c51b 100644 --- a/public/i18n/it-IT/runtime.json +++ b/public/i18n/it-IT/runtime.json @@ -1,16 +1,18 @@ { - "head": { - "home": { - "title": "{{name}}", - "description": "Libreria di internazionalizzazione (i18n) per tradurre testi, date e numeri nelle app Qwik" + "runtime": { + "devs": { + "one": "{{ value }} sviluppatore software", + "other": "{{ value }} sviluppatori software" }, - "page": { - "title": "Pagina - {{name}}", - "description": "Io sono un'altra pagina" + "head": { + "home": { + "description": "Libreria di internazionalizzazione (i18n) per tradurre testi, date e numeri nelle app Qwik", + "title": "{{name}}" + }, + "page": { + "description": "Io sono un'altra pagina", + "title": "Pagina - {{name}}" + } } - }, - "devs": { - "one": "{{ value }} sviluppatore software", - "other": "{{ value }} sviluppatori software" } } \ No newline at end of file diff --git a/src/app/routes/[...lang]/index.tsx b/src/app/routes/[...lang]/index.tsx index 8329423..54258d0 100644 --- a/src/app/routes/[...lang]/index.tsx +++ b/src/app/routes/[...lang]/index.tsx @@ -30,7 +30,7 @@ export const Home = component$(() => {

{t('home.plural')}

-

{p(state.count, 'devs')}

+

{p(state.count, 'runtime.devs')}

{t('home.dates')}

{fd(Date.now(), { dateStyle: 'full', timeStyle: 'short' })}

@@ -47,7 +47,7 @@ export const Home = component$(() => { export default component$(() => { return ( /** - * Add Home translation (only available in child components) + * Add Home translations (only available in child components) */ @@ -56,8 +56,8 @@ export default component$(() => { }); export const head: DocumentHead = { - title: 'head.home.title', - meta: [{ name: 'description', content: 'head.home.description' }] + title: 'runtime.head.home.title', + meta: [{ name: 'description', content: 'runtime.head.home.description' }] }; // E.g. SSG diff --git a/src/app/routes/[...lang]/page/index.tsx b/src/app/routes/[...lang]/page/index.tsx index 689724e..17d48db 100644 --- a/src/app/routes/[...lang]/page/index.tsx +++ b/src/app/routes/[...lang]/page/index.tsx @@ -18,7 +18,7 @@ export const Page = component$(() => { export default component$(() => { return ( /** - * Add Page translation (only available in child components) + * Add Page translations (only available in child components) */ @@ -27,8 +27,8 @@ export default component$(() => { }); export const head: DocumentHead = { - title: 'head.page.title', - meta: [{ name: 'description', content: 'head.page.description' }] + title: 'runtime.head.page.title', + meta: [{ name: 'description', content: 'runtime.head.page.description' }] }; // E.g. SSG diff --git a/src/app/speak-config.ts b/src/app/speak-config.ts index 646ac8d..7f13b81 100644 --- a/src/app/speak-config.ts +++ b/src/app/speak-config.ts @@ -76,7 +76,8 @@ export const storeLocale$: StoreLocaleFn = $((locale: SpeakLocale) => { url.pathname = `/${locale.lang}${url.pathname}`; } - window.history.pushState({}, '', url); + // E.g. Just replace the state: no back or forward on language change + window.history.replaceState({}, '', url); } }); diff --git a/tools/core/cli-parser.ts b/tools/core/cli-parser.ts new file mode 100644 index 0000000..48c6726 --- /dev/null +++ b/tools/core/cli-parser.ts @@ -0,0 +1,12 @@ +/** + * Parse a cli argument to { key: value } + */ +export function parseArgument(arg: string): { key: string, value: any } { + const property = arg.split('='); + if (property.length === 2 && property[0].startsWith('--')) { + const key = property[0].slice(2); + const value = /,/.test(property[1]) ? property[1].split(',') : property[1]; + return { key, value }; + } + return { key: 'error', value: `- wrong option: "${property[0]}"` }; +} diff --git a/tools/core/format.ts b/tools/core/format.ts new file mode 100644 index 0000000..493e137 --- /dev/null +++ b/tools/core/format.ts @@ -0,0 +1,28 @@ +export function toJsonString(target: { [key: string]: any }): string { + return JSON.stringify(target, replacer, 2); +} + +export function minDepth(target: { [key: string]: any }): number { + return typeof target === 'object' && Object.keys(target).length > 0 ? + 1 + Math.min(1, ...Object.values(target).map(o => minDepth(o))) + : 0 +} + +export function sortTarget(target: { [key: string]: any }) { + return Object.keys(target).sort().reduce( + (out: any, key: string) => { + if (typeof target[key] === 'object') + out[key] = sortTarget(target[key]); + else + out[key] = target[key]; + return out; + }, {} + ); +} + +/** + * Remove escaped sequences + */ +function replacer(key: string, value: any) { + return typeof value === 'string' ? value.replace(/\\/g, '') : value; +} diff --git a/tools/core/merge.ts b/tools/core/merge.ts new file mode 100644 index 0000000..ee5ce70 --- /dev/null +++ b/tools/core/merge.ts @@ -0,0 +1,24 @@ +/** + * https://github.com/lukeed/dset + */ +export function deepSet(target: { [key: string]: any }, keys: string[], val: string) { + let i = 0; + const len = keys.length; + while (i < len) { + const key = keys[i++]; + target[key] = (i === len) ? val : typeof target[key] === 'object' ? target[key] : {}; + target = target[key]; + } +} + +export function deepMerge(target: { [key: string]: any }, source: { [key: string]: any }) { + if (typeof target === 'object' && typeof source === 'object') { + for (const key of Object.keys(source)) { + if (!target[key] || typeof source[key] !== 'object') + target[key] = source[key]; + else + deepMerge(target[key], source[key]); + } + } + return target; +} diff --git a/tools/core/parser.ts b/tools/core/parser.ts index e93f812..3cedf9d 100644 --- a/tools/core/parser.ts +++ b/tools/core/parser.ts @@ -310,3 +310,18 @@ export function parseSequenceExpressions(code: string, alias: string): CallExpre return sequenceExpressions; } + +/** + * Get $translate alias + */ +export function getTranslateAlias(code: string): string { + let translateAlias = code.match(/(?<=\$translate as).*?(?=,|\})/s)?.[0]?.trim() || '$translate'; + // Escape special characters / Assert position at a word boundary + translateAlias = translateAlias.startsWith('$') ? `\\${translateAlias}` : `\\b${translateAlias}`; + return translateAlias; +} + +export function parseJson(target: { [key: string]: any }, source: string): { [key: string]: any } { + target = { ...target, ...JSON.parse(source) }; + return target; +} diff --git a/tools/extract.md b/tools/extract.md new file mode 100644 index 0000000..343e424 --- /dev/null +++ b/tools/extract.md @@ -0,0 +1,68 @@ +# Qwik Speak Extract + +> Extract translations directly from the components + +## Usage +### Command +#### Get the code ready +Optionally, you can use a default value for the keys. The syntax is `key@@[default value]`: +```html +

{t('app.title@@Qwik Speak'}

+

{t('home.greeting@@Hi! I am {{name}}', { name: 'Qwik Speak' })}

+ +``` +When you use a default value, it will be used as initial value for the key in every translation. + +> Note. A key will not be extracted when a function argument is a variable (dynamic). + +#### Naming conventions +If you use scoped translations, the first property will be used as filename: +```html +

{t('app.text)}

+

{t('home.greeting)}

+``` +will generate two files for each language: +``` +public/i18n +│ +└───en-US + app.json + home.json +``` +But if you don't always use scoped translations, only one file for each language will be generated, called `app.json` + +#### Configuration +Add the command in `package.json`, and provide at least the supported languages: +```json +"scripts": { + "qwik-speak-extract": "qwik-speak-extract --supportedLangs=en-US,it-IT" +} +``` +Available options: +- `basePath` The base path. Default to `'./'` +- `sourceFilesPath` Path to files to search for translations. Default to `'src'` +- `assetsPath` Path to translation files: `[basePath]/[assetsPath]/[lang]/*.json`. Default to `'public/i18n'` +- `format` The format of the translation files. Default to `'json'` +- `supportedLangs` Supported langs. Required +- `keySeparator` Separator of nested keys. Default is `'.'` +- `keyValueSeparator` Key-value separator. Default is `'@@'` + +> Note. Currently, only `json` is supported as format + +#### Running +```shell +npm run qwik-speak-extract +``` + +#### Updating +If you add new translations in the components, or a new language, they will be merged into the existing files without losing the translations already made. + +### Using it programmatically +Rather than using the command, you can invoke `qwikSpeakExtract` function: +```typescript +import { qwikSpeakExtract } from 'qwik-speak/extract'; + +await qwikSpeakExtract({ + supportedLangs: ['en-US', 'it-IT'] +}); +``` diff --git a/tools/extract/cli.js b/tools/extract/cli.js deleted file mode 100644 index da8a453..0000000 --- a/tools/extract/cli.js +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node - -import { extract } from './index.mjs'; - -extract(); diff --git a/tools/extract/cli.ts b/tools/extract/cli.ts new file mode 100644 index 0000000..c6d2042 --- /dev/null +++ b/tools/extract/cli.ts @@ -0,0 +1,78 @@ +import type { QwikSpeakExtractOptions } from './types'; +import { parseArgument } from '../core/cli-parser'; +import { qwikSpeakExtract } from './index'; + +const assertType = (value: any, type: string): boolean => { + if (type === value) return true; + if (type === 'array' && Array.isArray(value)) return true; + if (type === 'string' && typeof (value) === 'string') return true; + return false; +}; + +const wrongOption = (key: string, value: any): string => `- option "${key}": wrong value ${JSON.stringify(value)}`; +const missingOption = (name: string): string => `- missing option: "${name}"`; + +const args = process.argv.slice(2); + +const options: Partial = {}; + +const errors: string[] = []; + +// Parse arguments +for (const arg of args) { + const { key, value } = parseArgument(arg); + switch (key) { + case 'basePath': + if (assertType(value, 'string')) options.basePath = value; + else errors.push(wrongOption(key, value)); + break; + case 'sourceFilesPath': + if (assertType(value, 'string')) options.sourceFilesPath = value; + else errors.push(wrongOption(key, value)); + break; + case 'assetsPath': + if (assertType(value, 'string')) options.assetsPath = value; + else errors.push(wrongOption(key, value)); + break; + case 'format': + if (assertType(value, 'json')) options.format = value; + else errors.push(wrongOption(key, value)); + break; + case 'supportedLangs': + if (assertType(value, 'array')) options.supportedLangs = value; + else errors.push(wrongOption(key, value)); + break; + case 'keySeparator': + if (assertType(value, 'string')) options.keySeparator = value; + else errors.push(wrongOption(key, value)); + break; + case 'keyValueSeparator': + if (assertType(value, 'string')) options.keyValueSeparator = value; + else errors.push(wrongOption(key, value)); + break; + case 'error': + errors.push(value); + break; + default: + errors.push(`- unknown option: "${key}"`); + } +} + +// Required options +if (!options.supportedLangs) errors.push(missingOption('supportedLangs')); + +// Log errors +if (errors.length > 0) { + console.log('\x1b[36m%s\x1b[0m', 'Qwik Speak Extract options errors:'); + for (const error of errors) { + console.log('\x1b[33m%s\x1b[0m', error); + } + + process.exitCode = 1; // Exit process +} + +// Process +console.log('\x1b[36m%s\x1b[0m', 'Qwik Speak Extract'); +console.log('\x1b[32m%s\x1b[0m', 'extracting translation...'); + +qwikSpeakExtract(options as QwikSpeakExtractOptions); diff --git a/tools/extract/extract.ts b/tools/extract/extract.ts deleted file mode 100644 index 0fc61bb..0000000 --- a/tools/extract/extract.ts +++ /dev/null @@ -1,5 +0,0 @@ -export async function extract() { - const args = process.argv; - - // TODO -} diff --git a/tools/extract/index.ts b/tools/extract/index.ts index b96ed90..5f15e3e 100644 --- a/tools/extract/index.ts +++ b/tools/extract/index.ts @@ -1 +1,215 @@ -export { extract } from './extract'; +import { readdir, readFile, writeFile } from 'fs/promises'; +import { existsSync, mkdirSync } from 'fs'; +import { extname, join, normalize } from 'path'; + +import type { QwikSpeakExtractOptions, Translation } from './types'; +import { getTranslateAlias, parseJson, parseSequenceExpressions } from '../core/parser'; +import { deepMerge, deepSet } from '../core/merge'; +import { minDepth, sortTarget, toJsonString } from '../core/format'; + +/** + * Extract translations from source files + */ +export async function qwikSpeakExtract(options: QwikSpeakExtractOptions) { + // Resolve options + const resolvedOptions: Required = { + ...options, + basePath: options.basePath ?? './', + sourceFilesPath: options.sourceFilesPath ?? 'src', + assetsPath: options.assetsPath ?? 'public/i18n', + format: options.format ?? 'json', + keySeparator: options.keySeparator ?? '.', + keyValueSeparator: options.keyValueSeparator ?? '@@', + } + + // Logs + const stats = new Map(); + + const baseSources = normalize(`${resolvedOptions.basePath}/${resolvedOptions.sourceFilesPath}`); + + // Source files + const sourceFiles: string[] = []; + // Translation data + const translation: Translation = Object.fromEntries(resolvedOptions.supportedLangs.map(value => [value, {}])); + + /** + * Read source files recursively + */ + const readSourceFiles = async (sourceFilesPath: string) => { + const files = await readdir(sourceFilesPath, { withFileTypes: true }); + for (const file of files) { + const filePath = join(sourceFilesPath, file.name); + const ext = extname(file.name); + if (file.isDirectory()) { + await readSourceFiles(filePath); + } else if (/\.js|\.ts|\.jsx|\.tsx/.test(ext)) { + sourceFiles.push(filePath); + } + } + }; + + /** + * Parse source file to return keys + */ + const parseSourceFile = async (file: string): Promise => { + const keys: string[] = []; + + const code = await readFile(normalize(`${resolvedOptions.basePath}/${file}`), 'utf8'); + + if (/\$translate/.test(code)) { + const alias = getTranslateAlias(code); + // Parse sequence + const sequence = parseSequenceExpressions(code, alias); + for (const expr of sequence) { + const args = expr.arguments; + + if (args?.[0]?.value) { + if (args[0].type === 'Identifier') { + stats.set('dynamic', (stats.get('dynamic') ?? 0) + 1); + continue; + } + if (args[0].type === 'Literal') { + if (args[0].value !== 'key' && /\${.*}/.test(args[0].value)) { + stats.set('dynamic', (stats.get('dynamic') ?? 0) + 1); + continue; + } + } + if (args[1]?.type === 'Identifier' || args[1]?.type === 'CallExpression' || + args[2]?.type === 'Identifier' || args[2]?.type === 'CallExpression' || + args[3]?.type === 'Identifier' || args[3]?.type === 'CallExpression') { + stats.set('dynamic', (stats.get('dynamic') ?? 0) + 1); + continue; + } + + keys.push(args[0].value); + stats.set('keys', (stats.get('keys') ?? 0) + 1); + } + } + } + + return keys; + }; + + /** + * Read, deep merge & sort translation data + */ + const readAssets = async () => { + await Promise.all(resolvedOptions.supportedLangs.map(async lang => { + const baseAssets = normalize(`${resolvedOptions.basePath}/${resolvedOptions.assetsPath}/${lang}`); + + if (!existsSync(baseAssets)) return; + + const files = await readdir(baseAssets); + + if (files.length > 0) { + const ext = extname(files[0]); + let data: Translation = {}; + + const tasks = files.map(filename => readFile(`${baseAssets}/${filename}`, 'utf8')); + const sources = await Promise.all(tasks); + + for (const source of sources) { + if (source) { + switch (ext) { + case '.json': + data = parseJson(data, source); + break; + } + } + } + + deepMerge(translation[lang], data); + + // Sort by key + translation[lang] = sortTarget(translation[lang]); + } + })); + }; + + /** + * Write translation data + * + * Naming convention of keys: + * min depth > 1: filenames = each top-level property name + * min depth = 1: filename = 'app' + */ + const writeAssets = async () => { + for (const lang of resolvedOptions.supportedLangs) { + const baseAssets = normalize(`${resolvedOptions.basePath}/${resolvedOptions.assetsPath}/${lang}`); + + if (!existsSync(baseAssets)) { + mkdirSync(baseAssets, { recursive: true }); + } + + if (minDepth(translation[lang]) > 1) { + for (const topLevelProperty of Object.keys(translation[lang])) { + let data: string; + switch (resolvedOptions.format) { + case 'json': + // Computed property name + data = toJsonString({ [topLevelProperty]: translation[lang][topLevelProperty] }); + break; + } + const file = normalize(`${baseAssets}/${topLevelProperty}.${resolvedOptions.format}`); + await writeFile(file, data); + console.log(file); + } + } else { + let data: string; + switch (resolvedOptions.format) { + case 'json': + data = toJsonString(translation[lang]); + break; + } + + const file = normalize(`${baseAssets}/app.${resolvedOptions.format}`); + await writeFile(file, data); + console.log(file); + } + } + }; + + /** + * Start pipeline + */ + await readSourceFiles(baseSources); + + const tasks = sourceFiles.map(file => parseSourceFile(file)); + const sources = await Promise.all(tasks); + + let keys: string[] = []; + for (const source of sources) { + keys = keys.concat(source); + } + + // Deep set + for (let key of keys) { + let defaultValue: string | undefined = undefined; + + [key, defaultValue] = key.split(resolvedOptions.keyValueSeparator); + + for (const lang of resolvedOptions.supportedLangs) { + deepSet(translation[lang], key.split(resolvedOptions.keySeparator), defaultValue || ''); + } + } + + // Read, deep merge & sort + await readAssets(); + + // Write + await writeAssets(); + + // Log + for (const [key, value] of stats) { + switch (key) { + case 'keys': + console.log('\x1b[32m%s\x1b[0m', `extracted keys: ${value}`); + break; + case 'dynamic': + console.log('\x1b[32m%s\x1b[0m', `skipped keys due to dynamic params: ${value}`); + break; + } + } +} + +export type { QwikSpeakExtractOptions }; diff --git a/tools/extract/types.ts b/tools/extract/types.ts new file mode 100644 index 0000000..aafac70 --- /dev/null +++ b/tools/extract/types.ts @@ -0,0 +1,38 @@ +/** + * Qwik Speak Extract Options + */ +export interface QwikSpeakExtractOptions { + /** + * The base path. Default to './' + */ + basePath?: string; + /** + * Path to files to search for translations. Default to 'src' + */ + sourceFilesPath?: string; + /** + * Path to translation files: [basePath]/[assetsPath]/[lang]/*.json. Default to 'public/i18n' + */ + assetsPath?: string; + /** + * The format of the translation files. Default to 'json' + */ + format?: 'json'; + /** + * Supported langs. Required + */ + supportedLangs: string[]; + /** + * Separator of nested keys. Default is '.' + */ + keySeparator?: string; + /** + * Key-value separator. Default is '@@' + */ + keyValueSeparator?: string; +} + +/** + * Translation data + */ +export type Translation = { [key: string]: any }; diff --git a/tools/inline.md b/tools/inline.md index bbede58..e753178 100644 --- a/tools/inline.md +++ b/tools/inline.md @@ -25,7 +25,7 @@ export default defineConfig(() => { qwikVite(), qwikSpeakInline({ basePath: './', - assetsPath: './public/i18n', + assetsPath: 'public/i18n', supportedLangs: ['en-US', 'it-IT'], defaultLang: 'en-US' }), @@ -63,7 +63,7 @@ When there are translations with dynamic keys or params, you can manage them at } }); ``` -Likewise, you can also create lazy loaded runtime files for the different pages. +Likewise, you can also create scoped runtime files for the different pages. > Note. The `plural` function must be handled as a dynamic translation diff --git a/tools/inline/plugin.ts b/tools/inline/plugin.ts index 0628b2c..edb3bcc 100644 --- a/tools/inline/plugin.ts +++ b/tools/inline/plugin.ts @@ -1,10 +1,10 @@ import type { Plugin } from 'vite'; import { readFile, readdir } from 'fs/promises'; import { createWriteStream } from 'fs'; -import path from 'path'; +import { extname, normalize } from 'path'; import type { QwikSpeakInlineOptions, Translation } from './types'; -import type { Argument, Property } from '../core/parser'; +import { Argument, getTranslateAlias, parseJson, Property } from '../core/parser'; import { parseSequenceExpressions } from '../core/parser'; // Logs @@ -59,12 +59,12 @@ export function qwikSpeakInline(options: QwikSpeakInlineOptions): Plugin { async buildStart() { // For all langs await Promise.all(resolvedOptions.supportedLangs.map(async lang => { - const baseDir = path.normalize(`${resolvedOptions.basePath}${resolvedOptions.assetsPath}/${lang}`); + const baseDir = normalize(`${resolvedOptions.basePath}/${resolvedOptions.assetsPath}/${lang}`); // For all files const files = await readdir(baseDir); if (files.length > 0) { - const ext = path.extname(files[0]); + const ext = extname(files[0]); let data: Translation = {}; const tasks = files.map(filename => readFile(`${baseDir}/${filename}`, 'utf8')); @@ -74,7 +74,7 @@ export function qwikSpeakInline(options: QwikSpeakInlineOptions): Plugin { if (source) { switch (ext) { case '.json': - data = await parseJson(data, source); + data = parseJson(data, source); break; } } @@ -117,17 +117,12 @@ export function qwikSpeakInline(options: QwikSpeakInlineOptions): Plugin { return plugin; } -export async function parseJson(target: Translation, source: string): Promise { - target = { ...target, ...JSON.parse(source) }; - return target; -} - export function inline( code: string, translation: Translation, opts: Required ): string | null { - const alias = getAlias(code); + const alias = getTranslateAlias(code); // Parse sequence const sequence = parseSequenceExpressions(code, alias); @@ -219,13 +214,6 @@ export function inline( return code; } -export function getAlias(code: string): string { - let translateAlias = code.match(/(?<=\$translate as).*?(?=,|\})/s)?.[0]?.trim() || '$translate'; - // Escape special characters / Assert position at a word boundary - translateAlias = translateAlias.startsWith('$') ? `\\${translateAlias}` : `\\b${translateAlias}`; - return translateAlias; -} - export function multilingual(lang: string | undefined, supportedLangs: string[]): string | undefined { if (!lang) return undefined; return supportedLangs.find(x => x === lang); diff --git a/tools/tests/extract.test.ts b/tools/tests/extract.test.ts new file mode 100644 index 0000000..b32831b --- /dev/null +++ b/tools/tests/extract.test.ts @@ -0,0 +1,48 @@ +import fs from 'fs/promises'; +import { normalize } from 'path'; + +import { qwikSpeakExtract } from '../extract/index'; +import { mockAsset, mockSource } from './mock'; + +jest.mock('fs/promises'); + +describe('extract', () => { + beforeEach(() => { + // Reset mocks + jest.resetAllMocks(); + }); + + test('extract json', async () => { + fs.readdir = jest.fn() + .mockImplementationOnce(() => [{ name: 'home.tsx', isDirectory: () => false }]) + .mockImplementationOnce(() => ['home.json']); + fs.readFile = jest.fn() + .mockImplementationOnce(() => mockSource) + .mockImplementationOnce(() => mockAsset); + fs.writeFile = jest.fn(); + + await qwikSpeakExtract({ + supportedLangs: ['en-US'] + }); + + expect(fs.writeFile).toHaveBeenCalledTimes(2); + expect(fs.writeFile).toHaveBeenNthCalledWith(1, normalize('public/i18n/en-US/app.json'), `{ + "app": { + "subtitle": "Translate your Qwik apps into any language", + "title": "Qwik Speak" + } +}`); + expect(fs.writeFile).toHaveBeenNthCalledWith(2, normalize('public/i18n/en-US/home.json'), `{ + "home": { + "dates": "Dates & relative time", + "greeting": "Hi! I am {{name}}", + "increment": "Increment", + "numbers": "Numbers & currencies", + "params": "", + "plural": "Plural", + "tags": "Html tags", + "text": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps" + } +}`); + }); +}); diff --git a/tools/tests/format.test.ts b/tools/tests/format.test.ts new file mode 100644 index 0000000..1979b17 --- /dev/null +++ b/tools/tests/format.test.ts @@ -0,0 +1,20 @@ +import { minDepth, sortTarget } from '../core/format'; + +describe('format', () => { + test('minDepth', () => { + let target = {}; + let depth = minDepth(target); + expect(depth).toBe(0); + target = { key1: { subkey1: 'Subkey1' }, key2: 'Key2' }; + depth = minDepth(target); + expect(depth).toBe(1); + target = { key1: { subkey1: 'Subkey1' }, key2: { subkey2: 'Subkey2' } }; + depth = minDepth(target); + expect(depth).toBe(2); + }); + test('sortTarget', () => { + let target = { b: { b: 'B', a: 'A' }, a: 'A' }; + target = sortTarget(target); + expect(target).toEqual({ a: 'A', b: { a: 'A', b: 'B' } }); + }); +}); diff --git a/tools/tests/inline.test.ts b/tools/tests/inline.test.ts index 5d835fa..76921cc 100644 --- a/tools/tests/inline.test.ts +++ b/tools/tests/inline.test.ts @@ -1,23 +1,7 @@ -import { getKey, getValue, qwikSpeakInline, transpileFn, getAlias, addLang } from '../inline/plugin'; +import { getKey, getValue, qwikSpeakInline, transpileFn, addLang } from '../inline/plugin'; import { inlinedCode, mockCode } from './mock'; describe('inline', () => { - test('getAlias', () => { - let alias = getAlias(`import { - $translate as t, - plural as p, - formatDate as fd, - formatNumber as fn, - relativeTime as rt, - Speak, - useSpeakLocale - } from 'qwik-speak';`); - expect(alias).toBe('\\bt'); - alias = getAlias("import { $translate as t } from 'qwik-speak';"); - expect(alias).toBe('\\bt'); - alias = getAlias("import { $translate } from 'qwik-speak';"); - expect(alias).toBe('\\$translate'); - }); test('getKey', () => { let key = getKey('key1', '@@'); expect(key).toBe('key1'); diff --git a/tools/tests/merge.test.ts b/tools/tests/merge.test.ts new file mode 100644 index 0000000..ed07cd4 --- /dev/null +++ b/tools/tests/merge.test.ts @@ -0,0 +1,15 @@ +import { deepMerge, deepSet } from '../core/merge'; + +describe('merge', () => { + test('deepSet', () => { + const target = { key1: { subkey1: 'Subkey1' } }; + deepSet(target, ['key1', 'subkey2'], 'Subkey2'); + expect(target).toEqual({ key1: { subkey1: 'Subkey1', subkey2: 'Subkey2' } }); + }); + test('deepMerge', () => { + const target = { key1: { subkey1: 'Subkey1' } }; + const source = { key1: { subkey1: 'NewSubkey1', subkey2: 'Subkey2' } }; + deepMerge(target, source); + expect(target).toEqual({ key1: { subkey1: 'NewSubkey1', subkey2: 'Subkey2' } }); + }); +}); diff --git a/tools/tests/mock.ts b/tools/tests/mock.ts index b7505a9..42753d0 100644 --- a/tools/tests/mock.ts +++ b/tools/tests/mock.ts @@ -159,3 +159,86 @@ export const s_xJBzwgVGKaQ = ()=>{ ] }); };`; + +export const mockSource = `import { component$, useStore } from '@builder.io/qwik'; +import { DocumentHead, StaticGenerateHandler } from '@builder.io/qwik-city'; +import { + $translate as t, + plural as p, + formatDate as fd, + formatNumber as fn, + relativeTime as rt, + Speak, + useSpeakLocale +} from 'qwik-speak'; + +import { config } from '../../speak-config'; + +export const Home = component$(() => { + const units = useSpeakLocale().units!; + + const state = useStore({ count: 0 }); + + return ( + <> +

{t('app.title@@Qwik Speak')}

+

{t('app.subtitle@@Translate your Qwik apps into any language')}

+ +

{t('home.params')}

+

{t('home.greeting', { name: 'Qwik Speak' })}

+ +

{t('home.tags')}

+

+ +

{t('home.plural')}

+ +

{p(state.count, 'runtime.devs')}

+ +

{t('home.dates')}

+

{fd(Date.now(), { dateStyle: 'full', timeStyle: 'short' })}

+

{rt(-1, 'second')}

+ +

{t('home.numbers')}

+

{fn(1000000)}

+

{fn(1000000, { style: 'currency' })}

+

{fn(1, { style: 'unit', unit: units['length'] })}

+ + ); +}); + +export default component$(() => { + return ( + /** + * Add Home translation (only available in child components) + */ + + + + ); +}); + +export const head: DocumentHead = { + title: 'runtime.head.home.title', + meta: [{ name: 'description', content: 'runtime.head.home.description' }] +}; + +// E.g. SSG +export const onStaticGenerate: StaticGenerateHandler = () => { + return { + params: config.supportedLocales.map(locale => { + return { lang: locale.lang !== config.defaultLocale.lang ? locale.lang : '' }; + }), + }; +};`; + +export const mockAsset = JSON.stringify({ + "home": { + "dates": "Dates & relative time", + "greeting": "Hi! I am {{name}}", + "increment": "Increment", + "numbers": "Numbers & currencies", + "plural": "Plural", + "tags": "Html tags", + "text": "Internationalization (i18n) library to translate texts, dates and numbers in Qwik apps" + } +}, null, 2); diff --git a/tools/tests/parser.test.ts b/tools/tests/parser.test.ts index 2090a4a..1743a41 100644 --- a/tools/tests/parser.test.ts +++ b/tools/tests/parser.test.ts @@ -1,4 +1,4 @@ -import { parse, parseSequenceExpressions, tokenize } from '../core/parser'; +import { getTranslateAlias, parse, parseSequenceExpressions, tokenize } from '../core/parser'; describe('parser: tokenize', () => { test('tokenize', () => { @@ -362,3 +362,22 @@ describe('parser: parseSequenceExpressions', () => { ); }); }); + +describe('alias', () => { + test('getTranslateAlias', () => { + let alias = getTranslateAlias(`import { + $translate as t, + plural as p, + formatDate as fd, + formatNumber as fn, + relativeTime as rt, + Speak, + useSpeakLocale + } from 'qwik-speak';`); + expect(alias).toBe('\\bt'); + alias = getTranslateAlias("import { $translate as t } from 'qwik-speak';"); + expect(alias).toBe('\\bt'); + alias = getTranslateAlias("import { $translate } from 'qwik-speak';"); + expect(alias).toBe('\\$translate'); + }); +}); \ No newline at end of file diff --git a/tools/tsconfig.json b/tools/tsconfig.json index 817d787..6561741 100644 --- a/tools/tsconfig.json +++ b/tools/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2020", "module": "ES2020", "lib": [ - "es2020" + "es2021" ], "strict": true, "declaration": true, diff --git a/tools/vite.config.extract.ts b/tools/vite.config.extract.ts index 6682537..c025d5a 100644 --- a/tools/vite.config.extract.ts +++ b/tools/vite.config.extract.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'vite'; -import { viteStaticCopy } from 'vite-plugin-static-copy' +import shebang from 'rollup-plugin-add-shebang'; +import { readFile } from 'fs/promises'; export default defineConfig(() => { return { @@ -8,27 +9,26 @@ export default defineConfig(() => { outDir: 'extract', target: 'es2020', lib: { - entry: 'tools/extract/index.ts', - formats: ['es', 'cjs'], - fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`, + entry: ['tools/extract/index.ts', 'tools/extract/cli.ts'], + formats: ['es'], + fileName: (format, entryName) => `${entryName}.js`, }, rollupOptions: { + output: { + banner: () => readFile('./banner.txt', 'utf8') + }, external: [ 'fs', 'fs/promises', 'path' + ], + plugins: [ + shebang({ + shebang: '#!/usr/bin/env node', + include: ['./extract/cli.js'] + }) ] } - }, - plugins: [ - viteStaticCopy({ - targets: [ - { - src: 'tools/extract/cli.js', - dest: './' - } - ] - }) - ] + } }; }); diff --git a/tools/vite.config.inline.ts b/tools/vite.config.inline.ts index 27867e0..771413d 100644 --- a/tools/vite.config.inline.ts +++ b/tools/vite.config.inline.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'vite'; +import { readFile } from 'fs/promises'; export default defineConfig(() => { return { @@ -9,9 +10,12 @@ export default defineConfig(() => { lib: { entry: 'tools/inline/index.ts', formats: ['es', 'cjs'], - fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`, + fileName: (format, entryName) => `${entryName}.${format === 'es' ? 'mjs' : 'cjs'}`, }, rollupOptions: { + output: { + banner: () => readFile('./banner.txt', 'utf8') + }, external: [ 'fs', 'fs/promises', diff --git a/tsconfig.json b/tsconfig.json index 2999cf5..d2aee6b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ "qwik-speak": ["./src/index.ts"] } }, - "include": ["src", "tools/**/*.ts"] + "include": ["src", "tools"] } diff --git a/vite.config.lib.ts b/vite.config.lib.ts index 5abbcf9..6b0ee78 100644 --- a/vite.config.lib.ts +++ b/vite.config.lib.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'vite'; import { qwikVite } from '@builder.io/qwik/optimizer'; +import { readFile } from 'fs/promises'; export default defineConfig(() => { return { @@ -11,6 +12,11 @@ export default defineConfig(() => { formats: ['es', 'cjs'], fileName: (format) => `index.qwik.${format === 'es' ? 'mjs' : 'cjs'}`, }, + rollupOptions: { + output: { + banner: () => readFile('./banner.txt', 'utf8') + } + } }, plugins: [ qwikVite(),