From 299528b0545a3ce0eafec72465c98a99eb8b9372 Mon Sep 17 00:00:00 2001 From: Isaac Date: Wed, 17 Apr 2024 21:14:08 +0100 Subject: [PATCH] Fallback, optimisations, types, vite/sveltekit demo --- packages/cif/index.d.ts | 2 +- packages/cif/package.json | 2 +- packages/cif/src/ctom.ts | 2 +- packages/cif/src/mtoc.ts | 2 +- packages/cif/test/test.cif | Bin 1533 -> 1610 bytes packages/cif/test/test.i18n.json | 46 +- packages/i18n/README.md | 171 +--- packages/i18n/package.json | 2 +- packages/i18n/src/I18n.ts | 15 +- packages/i18n/test/with-fallback.js | 3 +- packages/i18n/types/I18n.d.ts | 2 +- packages/vite-plugin-i18n/README.md | 4 +- packages/vite-plugin-i18n/index.d.ts | 23 +- packages/vite-plugin-i18n/package.json | 2 +- packages/vite-plugin-i18n/src/I18n.ts | 17 - packages/vite-plugin-i18n/src/I18nPlugin.ts | 33 +- packages/vite-plugin-i18n/src/importCIF.ts | 11 + packages/vite-plugin-i18n/src/importJSON.ts | 10 + packages/vite-plugin-i18n/src/index.ts | 8 +- packages/vite-plugin-i18n/src/types.ts | 15 +- packages/vite-sveltekit-demo/package.json | 37 +- .../src/lib/locales/en-GB/_footer.json | 3 + .../src/lib/locales/en-GB/common.json | 5 + .../src/lib/locales/en-GB/home.json | 3 + .../src/routes/+page.svelte | 14 +- .../vite-sveltekit-demo/src/routes/+page.ts | 17 + packages/vite-sveltekit-demo/vite.config.ts | 14 +- pnpm-lock.yaml | 910 +++++++++++++++--- 28 files changed, 979 insertions(+), 394 deletions(-) delete mode 100644 packages/vite-plugin-i18n/src/I18n.ts create mode 100644 packages/vite-plugin-i18n/src/importCIF.ts create mode 100644 packages/vite-plugin-i18n/src/importJSON.ts create mode 100644 packages/vite-sveltekit-demo/src/lib/locales/en-GB/_footer.json create mode 100644 packages/vite-sveltekit-demo/src/lib/locales/en-GB/common.json create mode 100644 packages/vite-sveltekit-demo/src/lib/locales/en-GB/home.json diff --git a/packages/cif/index.d.ts b/packages/cif/index.d.ts index 9759d7a..7039479 100644 --- a/packages/cif/index.d.ts +++ b/packages/cif/index.d.ts @@ -1,4 +1,4 @@ -import type { ParsedMessages } from '@eartharoid/i18n'; +import type { ParsedMessages } from '@eartharoid/i18n/types/types'; export function ctom(cif: string): ParsedMessages; diff --git a/packages/cif/package.json b/packages/cif/package.json index 7c8a887..3a2ff9f 100644 --- a/packages/cif/package.json +++ b/packages/cif/package.json @@ -1,6 +1,6 @@ { "name": "@eartharoid/cif", - "version": "1.0.0", + "version": "1.0.0-alpha.1", "description": "Convert to and from an efficient i18n message file format", "main": "dist/index.js", "type": "module", diff --git a/packages/cif/src/ctom.ts b/packages/cif/src/ctom.ts index 668776d..fe8c6cf 100644 --- a/packages/cif/src/ctom.ts +++ b/packages/cif/src/ctom.ts @@ -1,7 +1,7 @@ import type { ParsedMessage, ParsedMessages -} from '@eartharoid/i18n'; +} from '@eartharoid/i18n/types/types.js'; import control from './control.js'; export default function ctom(cif: string): ParsedMessages { diff --git a/packages/cif/src/mtoc.ts b/packages/cif/src/mtoc.ts index 769cb0a..e72c668 100644 --- a/packages/cif/src/mtoc.ts +++ b/packages/cif/src/mtoc.ts @@ -1,4 +1,4 @@ -import type { ExtractedMessageObject, ParsedMessages } from '@eartharoid/i18n'; +import type { ExtractedMessageObject, ParsedMessages } from '@eartharoid/i18n/types/types.js'; import control from './control.js'; export default function mtoc(messages: ParsedMessages): string { diff --git a/packages/cif/test/test.cif b/packages/cif/test/test.cif index 92cfce48bbd0b56444ec156250d07f4f5c47075c..d6b354cbd61a6a2da4f311681b30b91715f6584f 100644 GIT binary patch delta 149 zcmey%eTrv-iET+{ZYrmt5hsY256MVX05KFYixuSZimh@oi%U2SEjU56yh~<^LS=rb zLP=3#S!#|#Ql*0ZL<1A8qSTzklFYKy_<}@`Vlz&dfV`(dVo9n(az=h~K2S-j!p4Me kOp>++@{vG8%2SI{6_WCc@)U|vlT-6baw<1ZWX@p%0OhwcqyPW_ delta 78 zcmX@b^Ot*q$;48Fjh8<$Sy!bN<;zFrmnxK}7NsgA https://www.youtube.com/watch?v=bwnksI2ZoJI - -// this code does exactly the same -console.log(i18n.getMessage('russian', 'example')); -``` - -### Placeholders - -i18n supports both positional and named placeholders. - -```js -{ // a locale object - "positional": { - "strings": "I like %s", - "numbers": "%d %s %d = %d" - }, - "named": { - "example1": "Hi, I'm {name} and I am from {location}", - "example2": "Hi, I'm {person.name} and I am from {person.location}" - } -} -``` - -> Also note that messages and named placeholders can be nested - -```js -messages('positional.strings', 'chocolate'); // I like chocolate - -messages('positional.numbers', 5, '+', 5, 10); // 5 + 5 = 10 - -messages('named.example1', { - name: 'Someone', - location: 'Somewhere' -}); // Hi, I'm Someone and I am from Somewhere - -messages('named.example2', { - person: { - name: 'Someone', - location: 'Somewhere' - } -}); // Hi, I'm Someone and I am from Somewhere -``` - -### Pluralisation - -i18n supports basic pluralisation. If the message is an array, **the first placeholder value will be "eaten" (consumed)** and the correct message will be returned. - -```js -[ - "1", - "anything else" -] -``` - -or - -```js -[ - "0", - "1", - "anything else" -] -``` - -```js -{ // a locale object - "example1": [ - "You only have one %s", - "You have %d %ss" - ], - "example2": [ - "You don't have any {item}s", - "You only have one {item}", - "You have {number} {item}s" - ] -} -``` - -```js -messages('example1', 1, 1, 'item') -messages('example2', 0, { - number: 0, - item: 'car' -}) -``` - -### API - -#### `new I18n(default_locale, locales)` - -> Create a new I18n instance - -- `default_locale` - the name of the default locale -- `locales` - an object of localised messages - -#### `I18n#default_locale` - -> The name of the default locale - -#### `I18n#locales` - -> An array of the names of the locales you created - -#### `I18n#getAllMessages(message, ...args)` - -> Get a message in all locales - -- `message` - dot notation string for the message -- `...args` - placeholders/pluralisation - -#### `I18n#getLocale(locale)` - -> Get a locale - -- `locale` - locale name - -Returns a function which calls [`I18n#getMessage`](#i18ngetmessagelocale-message-args) using the given locale name (or the default). - -#### `I18n#getMessage(locale, message, ...args)` - -> Get a message from a specific locale - -- `locale` - locale name -- `message` - dot notation string for the message -- `...args` - placeholders/pluralisation - -#### `I18n#getMessages(locales, message, ...args)` - -> Get a message from multiple locales - -- `locales` - array of locale names -- `message` - dot notation string for the message -- `...args` - placeholders/pluralisation - -## Support - -[![Discord support server](https://discordapp.com/api/guilds/451745464480432129/widget.png?style=banner4)](https://lnk.earth/discord) - -## Donate - -[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/eartharoid) - -## License - -[MIT license](https://github.com/eartharoid/i18n/blob/master/LICENSE). - -© 2022 Isaac Saunders +Super small and incredibly fast localisation *with no documentation*. \ No newline at end of file diff --git a/packages/i18n/package.json b/packages/i18n/package.json index b03c516..6d8a5db 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@eartharoid/i18n", - "version": "2.0.0", + "version": "2.0.0-alpha.1", "description": "Simple and lightweight message localisation", "main": "dist/index.js", "type": "module", diff --git a/packages/i18n/src/I18n.ts b/packages/i18n/src/I18n.ts index 72abecc..e7805b3 100644 --- a/packages/i18n/src/I18n.ts +++ b/packages/i18n/src/I18n.ts @@ -65,15 +65,14 @@ export default class I18n extends I18nLite { /** * Resolve missing translations - * @param {string} default_locale_id * @param {Record} [fallback_map] * @returns {Fallen} */ - public fallback(default_locale_id: string, fallback_map?: Record): Fallen { + public fallback(fallback_map?: Record): Fallen { + if (!this.default_locale_id) throw new Error('No default locale is set'); let ordered_ids: string[]; - const default_locale = this.locales.get(default_locale_id); + const default_locale = this.locales.get(this.default_locale_id); const locale_ids = Array.from(this.locales.keys()); - // const fallen: Fallen = locale_ids.reduce((obj, id) => (obj[id] = [], obj), {}); const fallen: Fallen = {}; if (fallback_map) { @@ -90,14 +89,12 @@ export default class I18n extends I18nLite { if (fallback_map) { fallback_order = [ ...(fallback_map[locale_id] || []), - default_locale_id + this.default_locale_id ]; } else { - // const idx = locale_id.indexOf('-'); - // const base_language = idx === -1 ? locale_id : locale_id.substring(0, idx); // locale_id.split('-')[0] const base_language = new Intl.Locale(locale_id).language; - if (base_language !== locale_id && this.locales.has(base_language)) fallback_order = [base_language, default_locale_id]; - else fallback_order = [default_locale_id]; + if (base_language !== locale_id && this.locales.has(base_language)) fallback_order = [base_language, this.default_locale_id]; + else fallback_order = [this.default_locale_id]; } const locale = this.locales.get(locale_id); for (const [key] of default_locale) { diff --git a/packages/i18n/test/with-fallback.js b/packages/i18n/test/with-fallback.js index e5ff50b..fc08f97 100644 --- a/packages/i18n/test/with-fallback.js +++ b/packages/i18n/test/with-fallback.js @@ -14,10 +14,11 @@ fs.readdirSync('test/locales') }); const i18n = new I18n({ + default_locale_id: 'en', defer_extraction: false, }); for (const [k, v] of Object.entries(locales)) i18n.load(k, v); -i18n.fallback('en'); +i18n.fallback(); test('not missing', t => { const expected = 'Dette er så enkelt som det blir'; diff --git a/packages/i18n/types/I18n.d.ts b/packages/i18n/types/I18n.d.ts index 090e00f..87a1eb3 100644 --- a/packages/i18n/types/I18n.d.ts +++ b/packages/i18n/types/I18n.d.ts @@ -6,7 +6,7 @@ export default class I18n extends I18nLite { placeholder_regex: RegExp; constructor(options?: Partial); extract(message: string): ExtractedMessageObject; - fallback(default_locale_id: string, fallback_map?: Record): Fallen; + fallback(fallback_map?: Record): Fallen; load(locale_id: string, messages: RawMessages, namespace?: string): Locale; parse(messages: RawMessages, namespace?: string): ParsedMessages; } diff --git a/packages/vite-plugin-i18n/README.md b/packages/vite-plugin-i18n/README.md index b8950e0..2136cdc 100644 --- a/packages/vite-plugin-i18n/README.md +++ b/packages/vite-plugin-i18n/README.md @@ -89,7 +89,7 @@ export default { ## Usage -```ts + \ No newline at end of file diff --git a/packages/vite-plugin-i18n/index.d.ts b/packages/vite-plugin-i18n/index.d.ts index 737e7d3..dd2aeda 100644 --- a/packages/vite-plugin-i18n/index.d.ts +++ b/packages/vite-plugin-i18n/index.d.ts @@ -1,8 +1,13 @@ -import type { Locale, I18nLiteOptions } from '@eartharoid/i18n/index.d.ts'; +import type { Plugin } from 'vite'; +import type { ParsedMessages } from '@eartharoid/i18n/types/types'; export interface CIFModule { - cif?: string, - json?: ParsedMessages, + cif: string, + locale_id: string, +} + +export interface JSONModule { + json: ParsedMessages, locale_id: string, } @@ -14,15 +19,15 @@ export interface I18nPluginOptions { parser?(src: string): string, } -export interface I18nPlugin { +export interface I18nVitePlugin { enforce: string, name: string, transform(code: string, id: string): unknown, } -export class I18n { - constructor(options: Partial); - public load(module: CIFModule): Locale -} -export function I18nPlugin(options: I18nPluginOptions): I18nPlugin; \ No newline at end of file +export function importCIF(...modules: CIFModule[]): [string, ParsedMessages] + +export function importJSON(...modules: JSONModule[]): [string, ParsedMessages] + +export function I18nPlugin(options: I18nPluginOptions): Plugin; \ No newline at end of file diff --git a/packages/vite-plugin-i18n/package.json b/packages/vite-plugin-i18n/package.json index 4e20e0e..8c6ffd9 100644 --- a/packages/vite-plugin-i18n/package.json +++ b/packages/vite-plugin-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@eartharoid/vite-plugin-i18n", - "version": "1.0.0", + "version": "1.0.0-alpha.1", "description": "A Vite plugin for I18n and CIF", "main": "dist/index.js", "type": "module", diff --git a/packages/vite-plugin-i18n/src/I18n.ts b/packages/vite-plugin-i18n/src/I18n.ts deleted file mode 100644 index 530f390..0000000 --- a/packages/vite-plugin-i18n/src/I18n.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { I18nLiteOptions, Locale } from '@eartharoid/i18n'; -import type { CIFModule } from './types'; -import { I18nLite } from '@eartharoid/i18n'; -// @ts-ignore -import { ctom } from '@eartharoid/cif'; - -export default class I18n extends I18nLite { - constructor(options?: Partial) { - super(options); - } - - public load(module: CIFModule | CIFModule[]): Locale { - // TODO: ignore locale of all but first - const { cif, json, locale_id } = module; - return this.loadParsed(locale_id, cif ? ctom(cif) : json); - } -} \ No newline at end of file diff --git a/packages/vite-plugin-i18n/src/I18nPlugin.ts b/packages/vite-plugin-i18n/src/I18nPlugin.ts index eaa76be..46a9c85 100644 --- a/packages/vite-plugin-i18n/src/I18nPlugin.ts +++ b/packages/vite-plugin-i18n/src/I18nPlugin.ts @@ -1,41 +1,40 @@ -import type { I18nPlugin, I18nPluginOptions } from './types'; -import type { ParsedMessages, RawMessages } from '@eartharoid/i18n'; +import type { Plugin } from 'vite'; +import type { I18nPluginOptions, I18nVitePlugin } from './types'; +import type { ParsedMessages, RawMessages } from '@eartharoid/i18n/types/types'; import { I18n } from '@eartharoid/i18n'; // @ts-ignore !? import { mtoc } from '@eartharoid/cif'; import { createFilter, - normalizePath + normalizePath, } from 'vite'; -function parse(messages: RawMessages): ParsedMessages { +function parse(messages: RawMessages, namespace?: string): ParsedMessages { const i18n = new I18n({ defer_extraction: false }); - return i18n.parse(messages); + return i18n.parse(messages, namespace); } -function encode(parsed: ParsedMessages): string { - return mtoc(parsed); -} - -export default function I18nPlugin(options: I18nPluginOptions): I18nPlugin { +export default function I18nPlugin(options: I18nPluginOptions): Plugin { return { enforce: 'pre', // must run before vite:json name: 'i18n', transform(src, id) { const filter = createFilter(options.include, options.exclude); - if (filter(id)) { + const [normalised, qs] = normalizePath(id).split('?'); + if (filter(normalised)) { // TODO: preprocess for i18next/weblate - // TODO: ./[locale].json | ./[locale]/[namespace].json (ignore namespace, use ?namespace on import) - + const query = new URLSearchParams(qs ?? ''); const id_regex = options.id_regex || /(?[a-z0-9-_]+)\.[a-z]+/i; - const locale_id = id_regex.exec(normalizePath(id))?.groups?.id; + // eslint-disable-next-line prefer-const + let { locale, namespace } = id_regex.exec(normalised)?.groups || {}; + namespace = query.get('namespace') || namespace; const messages = options.parser ? options.parser(src) : JSON.parse(src); - const parsed = parse(messages); - const cif = encode(parsed); + const parsed = parse(messages, namespace); + const cif = mtoc(parsed); return { code: JSON.stringify({ ...options.compact ? { cif } : { json: parsed }, - locale_id + locale_id: locale }), map: { mappings: '' }, // TODO: generate a sourcemap }; diff --git a/packages/vite-plugin-i18n/src/importCIF.ts b/packages/vite-plugin-i18n/src/importCIF.ts new file mode 100644 index 0000000..df0eec2 --- /dev/null +++ b/packages/vite-plugin-i18n/src/importCIF.ts @@ -0,0 +1,11 @@ +import type { ParsedMessages } from '@eartharoid/i18n/types/types'; +import type { CIFModule } from './types'; +import { ctom } from '@eartharoid/cif'; + +export default function importCIF(...modules: CIFModule[]): [string, ParsedMessages] { + if (modules.length === 1) { + return [modules[0].locale_id, ctom(modules[0].cif)]; + } else { + return [modules[0].locale_id, [].concat(...modules.map(mod => mod.cif))]; + } +} \ No newline at end of file diff --git a/packages/vite-plugin-i18n/src/importJSON.ts b/packages/vite-plugin-i18n/src/importJSON.ts new file mode 100644 index 0000000..5f35650 --- /dev/null +++ b/packages/vite-plugin-i18n/src/importJSON.ts @@ -0,0 +1,10 @@ +import { ParsedMessages } from '@eartharoid/i18n/types/types'; +import type { JSONModule } from './types'; + +export default function importJSON(...modules: JSONModule[]): [string, ParsedMessages] { + if (modules.length === 1) { + return [modules[0].locale_id, modules[0].json]; + } else { + return [modules[0].locale_id, [].concat(...modules.map(mod => mod.json))]; + } +} \ No newline at end of file diff --git a/packages/vite-plugin-i18n/src/index.ts b/packages/vite-plugin-i18n/src/index.ts index cc8a12a..23c0deb 100644 --- a/packages/vite-plugin-i18n/src/index.ts +++ b/packages/vite-plugin-i18n/src/index.ts @@ -7,10 +7,12 @@ 'use strict'; -import I18n from './I18n.js'; import I18nPlugin from './I18nPlugin.js'; +import importCIF from './importCIF.js'; +import importJSON from './importJSON.js'; export { - I18n, - I18nPlugin + I18nPlugin, + importCIF, + importJSON, }; \ No newline at end of file diff --git a/packages/vite-plugin-i18n/src/types.ts b/packages/vite-plugin-i18n/src/types.ts index f0226f8..89ed527 100644 --- a/packages/vite-plugin-i18n/src/types.ts +++ b/packages/vite-plugin-i18n/src/types.ts @@ -1,7 +1,12 @@ -import { ParsedMessages } from '@eartharoid/i18n'; +import { ParsedMessages } from '@eartharoid/i18n/types/types'; + export interface CIFModule { - cif?: string, - json?: ParsedMessages, + cif: string, + locale_id: string, +} + +export interface JSONModule { + json: ParsedMessages, locale_id: string, } @@ -13,8 +18,10 @@ export interface I18nPluginOptions { parser?(src: string): string, } -export interface I18nPlugin { +export interface I18nVitePlugin { enforce: string, name: string, + // resolveId(id: string): string | void, + // load(id: string): Promise, transform(code: string, id: string): unknown, } diff --git a/packages/vite-sveltekit-demo/package.json b/packages/vite-sveltekit-demo/package.json index 0a2a6e3..2e1d511 100644 --- a/packages/vite-sveltekit-demo/package.json +++ b/packages/vite-sveltekit-demo/package.json @@ -13,22 +13,27 @@ "devDependencies": { "@fontsource/fira-mono": "^4.5.10", "@neoconfetti/svelte": "^1.0.0", - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "@types/eslint": "^8.56.0", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0", + "@sveltejs/adapter-auto": "^3.2.0", + "@sveltejs/kit": "^2.5.5", + "@sveltejs/vite-plugin-svelte": "^3.1.0", + "@types/eslint": "^8.56.9", + "@typescript-eslint/eslint-plugin": "^7.6.0", + "@typescript-eslint/parser": "^7.6.0", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.35.1", - "prettier": "^3.1.1", - "prettier-plugin-svelte": "^3.1.2", - "svelte": "^4.2.7", - "svelte-check": "^3.6.0", - "tslib": "^2.4.1", - "typescript": "^5.0.0", - "vite": "^5.0.3" + "eslint-plugin-svelte": "^2.37.0", + "prettier": "^3.2.5", + "prettier-plugin-svelte": "^3.2.3", + "svelte": "^4.2.14", + "svelte-check": "^3.6.9", + "tslib": "^2.6.2", + "typescript": "^5.4.5", + "vite": "^5.2.8" }, - "type": "module" + "type": "module", + "dependencies": { + "@eartharoid/i18n": "workspace:^", + "@eartharoid/vite-plugin-i18n": "workspace:^", + "vite-plugin-inspect": "^0.8.3" + } } diff --git a/packages/vite-sveltekit-demo/src/lib/locales/en-GB/_footer.json b/packages/vite-sveltekit-demo/src/lib/locales/en-GB/_footer.json new file mode 100644 index 0000000..1943d4c --- /dev/null +++ b/packages/vite-sveltekit-demo/src/lib/locales/en-GB/_footer.json @@ -0,0 +1,3 @@ +{ + "footer": "Footer you" +} \ No newline at end of file diff --git a/packages/vite-sveltekit-demo/src/lib/locales/en-GB/common.json b/packages/vite-sveltekit-demo/src/lib/locales/en-GB/common.json new file mode 100644 index 0000000..7619be7 --- /dev/null +++ b/packages/vite-sveltekit-demo/src/lib/locales/en-GB/common.json @@ -0,0 +1,5 @@ +{ + "buttons": { + "search": "Search the {thing}" + } +} \ No newline at end of file diff --git a/packages/vite-sveltekit-demo/src/lib/locales/en-GB/home.json b/packages/vite-sveltekit-demo/src/lib/locales/en-GB/home.json new file mode 100644 index 0000000..85d2752 --- /dev/null +++ b/packages/vite-sveltekit-demo/src/lib/locales/en-GB/home.json @@ -0,0 +1,3 @@ +{ + "hello": "Hello world" +} \ No newline at end of file diff --git a/packages/vite-sveltekit-demo/src/routes/+page.svelte b/packages/vite-sveltekit-demo/src/routes/+page.svelte index 97201ef..f2d427f 100644 --- a/packages/vite-sveltekit-demo/src/routes/+page.svelte +++ b/packages/vite-sveltekit-demo/src/routes/+page.svelte @@ -1,7 +1,16 @@ - @@ -26,6 +35,9 @@ +

{t('hello')}

+

{t('common:buttons.search', { thing: 'database' })}

+

{t('footer:footer')}