Skip to content

Commit

Permalink
worklist + list + umd-bugfix
Browse files Browse the repository at this point in the history
  • Loading branch information
eddow committed Jun 5, 2024
1 parent 37a2cbe commit 3d0fe81
Show file tree
Hide file tree
Showing 18 changed files with 125 additions and 70 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import { I18nServer, I18nClient } from 'omni18n'

const server = new I18nServer(myDBinterface)
const client = new I18nClient(['en-US'], server.condense)
const T = await client.enter() // Here is where the actual DB-query occurs
// Here is where the actual DB-query occurs
const T = await client.enter()

// Will all display the entry `msg.hello` for the `en-US` (or `en`) locale
console.log(`${T.msg.hello}, ...`)
Expand All @@ -64,8 +65,10 @@ const fetchCondensed: Condense = async (locales: Locale[], zones: string[])=> {
return result as CondensedDictionary[]
}
const client = new I18nClient(['en-US'], fetchCondensed)
client.usePartial(preloadedData) // With many frameworks, dictionary data might be available on page load
const T = await client.enter() // Here is where the actual download occurs if needed
// With many frameworks, dictionary data might be available on page load
client.usePartial(preloadedData)
// Here is where the actual download occurs if needed
const T = await client.enter()
```

The `usePartial` usage is described [here](./docs/client.md#ssr-between-clients)
Expand Down Expand Up @@ -173,7 +176,9 @@ error(key: string, error: string, spec: object): string

## Integrations

- [SvelteKit](https://github.com/eddow/omni18n-svelte4) (Svelte4)
- [SvelteKit](https://github.com/eddow/omni18n-svelte4) - Svelte4
- [Translator](https://github.com/eddow/omni18n-edit/releases) - To edit `FileDB` dictionaries


## TODO

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Projects using OmnI18n use it in 4 layers
3. [The `server`](./server.md): The server exposes functions to interact with the languages
4. [The `database`](./db.md): A class implementing some interface that interacts directly with a database

The library can be imported in a static website.
The library can be imported in a [static website](umd.md).

## Bonus

Expand Down
4 changes: 3 additions & 1 deletion docs/umd.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,14 @@ node extractLocales -i myFile.db en fr hu
```

The script accept these arguments:

- without flags - `locales`: The locales to extract (will be the ones used by the application)
- `--input`/`-i` - input (**mandatory**): the input file (a serialized [FileDB](db.md#filedb))
- `--output`/`-o` - output directory: if different from the directory where the input is
- `--watch`/`-w`: stay active until killed and extract each time the DB file is modified

Then, 2 possibilities:

- `--pattern`/`-p`: Gives a pattern for each file. This is a filename where `$` is replaced by the locale
- `--grouped`/`-g`: Gives the filename who will contain all the locales
- By default, `pattern` is `$.js`
- By default, `pattern` is `$.js`
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"require": "./lib/cjs/s-a.js",
"types": "./lib/src/s-a.d.ts"
},
"./s-a/ts": "./src/s-a.ts"
"./ts/s-a": "./src/s-a.ts"
},
"bin": {
"extractLocales": "./bin/extractLocales.js"
Expand Down
3 changes: 3 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default [
{
input: ['src/index.ts', 'src/s-a.ts', 'src/client.ts'],
output: {
banner: '/** https://www.npmjs.com/package/omni18n */',
dir: 'lib'
},
external: ['json5'],
Expand All @@ -34,6 +35,7 @@ export default [
{
input: ['src/index.ts', 'src/s-a.ts', 'src/client.ts'],
output: {
banner: '/** https://www.npmjs.com/package/omni18n */',
dir: 'lib/cjs',
sourcemap: true,
format: 'cjs',
Expand All @@ -53,6 +55,7 @@ export default [
{
input: ['src/index.ts', 'src/s-a.ts', 'src/client.ts'],
output: {
banner: '/** https://www.npmjs.com/package/omni18n */',
dir: 'lib/esm',
sourcemap: true,
format: 'esm'
Expand Down
1 change: 1 addition & 0 deletions rollup.extract.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import typescript from 'rollup-plugin-typescript2'
export default {
input: 'src/umd/extractLocales.ts',
output: {
banner: '/** https://www.npmjs.com/package/omni18n */',
file: 'bin/extractLocales.mjs'
},
plugins: [
Expand Down
1 change: 1 addition & 0 deletions rollup.umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import typescript from 'rollup-plugin-typescript2'
export default {
input: 'src/umd/client.ts',
output: {
banner: '/** https://www.npmjs.com/package/omni18n */',
file: 'lib/omni18n.js',
format: 'umd',
name: 'OmnI18n'
Expand Down
3 changes: 2 additions & 1 deletion src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as I18nClient, type TContext, getContext } from './client'
export { default as I18nClient } from './client'
export * from './client'
export { TranslationError, type ClientDictionary, type Translator } from './types'
export { reports, bulkObject, bulkDictionary } from './helpers'
export { formats, processors } from './interpolation'
16 changes: 16 additions & 0 deletions src/client/interpolation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,22 @@ export const processors: Record<string, (...args: any[]) => string> = {
new Intl.DisplayNames(this.client.locales[0], { type: 'currency' }).of(str) ||
reportError(this, 'Invalid currency', { str })
)
},
list(this: TContext, ...args: any[]) {
function makeArray(arg: Record<string, any> | any[] | string) {
if (Array.isArray(arg) || typeof arg !== 'object') return arg
const rv = []
for (const key in arg) rv[parseInt(key)] = arg[key]
return rv
}
let opts = args.pop()
if (typeof opts !== 'object' || Array.isArray(opts)) {
args.push(opts)
opts = {}
}
return new Intl.ListFormat(this.client.locales[0], opts).format(
args.map((arg) => makeArray(arg)).flat()
)
}
}

Expand Down
23 changes: 4 additions & 19 deletions src/db/fileDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default class FileDB<KeyInfos extends {}, TextInfos extends {}> extends M
> {
private saving: Defer
public readonly loaded: Promise<void>
private metadata: any // DbInfos
constructor(
private path: string,
saveDelay = 1e3 // 1 second
Expand All @@ -21,31 +20,17 @@ export default class FileDB<KeyInfos extends {}, TextInfos extends {}> extends M
this.loaded = this.reload()
this.saving = new Defer(async () => {
let data = serialization.serialize(this.dictionary)
if (this.metadata) data = `#${stringify(this.metadata)}\n` + data
await writeFile(this.path, data, 'utf16le')
}, saveDelay)
}

get meta() {
return this.metadata
}

set meta(meta: any) {
this.metadata = meta
this.saving.defer()
}

async reload() {
// In case of too much time, write a "modified" call
const fStat = await stat(this.path)
if (fStat.isFile() && fStat.size > 0) {
const data = await readFile(this.path, 'utf16le'),
mda = /^#(.*?)\n/g.exec(data)
if (mda) {
this.metadata = parse(mda[1])
this.dictionary = serialization.deserialize<KeyInfos, TextInfos>(data.slice(mda.index))
} else this.dictionary = serialization.deserialize<KeyInfos, TextInfos>(data)
}
if (fStat.isFile() && fStat.size > 0)
this.dictionary = serialization.deserialize<KeyInfos, TextInfos>(
await readFile(this.path, 'utf16le')
)
}

async save() {
Expand Down
17 changes: 13 additions & 4 deletions src/db/memDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type MemDBDictionaryEntry<KeyInfos extends {} = {}, TextInfos extends {}

export type MemDBDictionary<KeyInfos extends {} = {}, TextInfos extends {} = {}> = {
[key in TextKey]: MemDBDictionaryEntry<KeyInfos, TextInfos>
} & {
'.dbInfos'?: any
}

export default class MemDB<KeyInfos extends {} = {}, TextInfos extends {} = {}>
Expand All @@ -40,9 +42,10 @@ export default class MemDB<KeyInfos extends {} = {}, TextInfos extends {} = {}>
}

async workList(locales: Locale[]) {
const result: WorkDictionary = {}
const result: WorkDictionary = []
Object.entries(this.dictionary).forEach(([key, value]) => {
const entry: WorkDictionaryEntry<KeyInfos, TextInfos> = {
key,
zone: value['.zone'] || '',
texts: {},
...(value['.keyInfos'] && { infos: value['.keyInfos'] })
Expand All @@ -58,7 +61,7 @@ export default class MemDB<KeyInfos extends {} = {}, TextInfos extends {} = {}>
}
}
}
result[key] = entry
result.push(entry)
})
return result
}
Expand All @@ -72,11 +75,17 @@ export default class MemDB<KeyInfos extends {} = {}, TextInfos extends {} = {}>
: false
}

async modify(key: TextKey, locale: Locale, value: Translation, textInfos?: Partial<TextInfos>) {
async modify(
key: TextKey,
locale: Locale,
value: Translation | undefined,
textInfos?: Partial<TextInfos>
) {
if (!this.dictionary[key]) throw new Error(`Key "${key}" not found`)
if (!/^[\w-]*$/g.test(locale))
throw new Error(`Bad locale: ${locale} (only letters, digits, "_" and "-" allowed)`)
this.dictionary[key][locale] = value
if (value === undefined) delete this.dictionary[key][locale]
else this.dictionary[key][locale] = value
if (textInfos) {
const tis = <Record<Locale, TextInfos>>this.dictionary[key]['.textInfos']
tis[locale] = {
Expand Down
63 changes: 35 additions & 28 deletions src/db/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,46 @@ const serialization = {
const stringified = stringify(obj)
return preTabs ? stringified.replace(/\n/g, '\n' + '\t'.repeat(preTabs)) : stringified
}
let rv = ''
for (const [key, value] of Object.entries(dictionary)) {
const ti = value['.textInfos']
rv +=
key.replace(/:/g, '::') +
(value['.keyInfos'] ? optioned(value['.keyInfos']) : '') +
':' +
value['.zone'] +
'\n' +
Object.entries(value)
.filter(([k]) => !k.startsWith('.'))
.map(
([k, v]) =>
'\t' +
k +
(ti?.[k] ? optioned(ti[k], 1) : '') +
':' +
v!.replace(/\n/g, '\n\t\t') +
'\n'
)
.join('')
if (ti)
rv += Object.entries(ti)
.filter(([k]) => !(k in value))
.map(([k, v]) => '\t' + k + optioned(v, 1) + '\n')
.join('')
}
let rv = dictionary['.dbInfos'] ? `#${stringify(dictionary['.dbInfos'])}\n` : ''
for (const [key, value] of Object.entries(dictionary))
if (key[0] !== '.') {
const ti = value['.textInfos']
rv +=
key.replace(/:/g, '::') +
(value['.keyInfos'] ? optioned(value['.keyInfos']) : '') +
':' +
value['.zone'] +
'\n' +
Object.entries(value)
.filter(([k]) => !k.startsWith('.'))
.map(
([k, v]) =>
'\t' +
k +
(ti?.[k] ? optioned(ti[k], 1) : '') +
':' +
(<string>v!).replace(/\n/g, '\n\t\t') +
'\n'
)
.join('')
if (ti)
rv += Object.entries(ti)
.filter(([k]) => !(k in value))
.map(([k, v]) => '\t' + k + optioned(v, 1) + '\n')
.join('')
}
return rv
},

deserialize<KeyInfos extends {} = {}, TextInfos extends {} = {}>(data: string) {
if (!data.endsWith('\n')) data += '\n'
const dictionary: MemDBDictionary<KeyInfos, TextInfos> = {}
if (!data.endsWith('\n')) data += '\n'
if (data.charCodeAt(0) > 255) data = data.slice(1)
const mda = /^#(.*?)\n/g.exec(data)
if (mda) {
dictionary['.dbInfos'] = parse(mda[1])
data = data.slice(mda[0].length)
}
serialization.analyze<KeyInfos, TextInfos>(
data,
(key, zone, infos) => {
Expand Down
2 changes: 1 addition & 1 deletion src/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface TranslatableDB<TextInfos extends {} = {}> extends DB {
modify(
key: TextKey,
locale: Locale,
text: Translation,
text: Translation | undefined,
textInfos?: Partial<TextInfos>
): Promise<void>
}
Expand Down
8 changes: 6 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
export type Locale = Intl.UnicodeBCP47LocaleIdentifier
export type Zone = string
// No `then` as it would become `thenable` and no async function could return a `Translator`
export type TextKey = Exclude<string, '' | '.' | 'then' | '.zone' | '.textInfos' | '.keyInfos'>
export type TextKey = Exclude<
string,
'' | '.' | 'then' | '.zone' | '.textInfos' | '.keyInfos' | '.dbInfos'
>
export type Translation = string

export type CondensedDictionary = {
Expand All @@ -19,11 +22,12 @@ export type WorkDictionaryText<TextInfos extends {} = {}> = {
infos?: TextInfos
}
export type WorkDictionaryEntry<KeyInfos extends {} = {}, TextInfos extends {} = {}> = {
key: TextKey
texts: { [locale: Locale]: WorkDictionaryText<TextInfos> }
zone: Zone
infos?: KeyInfos
}
/**
* Dictionary used for translator-related operations
*/
export type WorkDictionary = Record<TextKey, WorkDictionaryEntry>
export type WorkDictionary = WorkDictionaryEntry[]
13 changes: 12 additions & 1 deletion src/umd/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,21 @@ export function translatePage() {
const parts = element.getAttribute('i18n')!.split(',')
for (const part of parts) {
const [attr, key] = part.split(':', 2).map((k) => k.trim())
if (key === 'html') element.innerHTML = T[key]()
if (attr === 'html') element.innerHTML = T[key]()
if (key) element.setAttribute(attr, T[key]())
else element.textContent = T[attr]()
}
}
// Translate before text is loaded
// Just empty the translated elements before their rendering to avoid blinking
else
for (const element of document.querySelectorAll('[i18n]')) {
const parts = element.getAttribute('i18n')!.split(',')
for (const part of parts) {
const [attr, key] = part.split(':', 2).map((k) => k.trim())
if (attr === 'html' || !key) element.textContent = ''
}
}
const localesListElm = document.getElementById('locales-list')
if (localesListElm) {
const selectionList: string[] = [],
Expand Down Expand Up @@ -125,6 +135,7 @@ export async function init(acceptedLocales: Locale[], fileNameTemplate?: string)
const localeChangeCBs: ((locale: Locale) => void)[] = []
export function onLocaleChange(cb: (locale: Locale) => void) {
localeChangeCBs.push(cb)
if (<any>globalThis.T) cb(locale)
return () => localeChangeCBs.splice(localeChangeCBs.indexOf(cb), 1)
}

Expand Down
Loading

0 comments on commit 3d0fe81

Please sign in to comment.