Skip to content

Commit

Permalink
bulk system
Browse files Browse the repository at this point in the history
  • Loading branch information
eddow committed Apr 30, 2024
1 parent a95a07c commit 4bd8548
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 125 deletions.
7 changes: 0 additions & 7 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,6 @@ reports.missing = ({ key, client }: TContext, fallback?: string) => {
}
```

- A "missing key while loading" report
This one is called only when the client is in a loading state. If `onModification` was specified, it will be called once loaded. If not, the client will automatically check all the keys that went through this error to check them again.

```ts
reports.loading = ({ client }: TContext) => '...'
```

- An interpolation error
When interpolating, an error calls this report with a textual description and some specifications depending on the error.

Expand Down
41 changes: 11 additions & 30 deletions src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
/// <reference path="../types.d.ts" />

/**
* i18n consumption/usage, both client and server side.
*/
import '../polyfill'
import Defer from '../defer'
import {
ClientDictionary,
contextKey,
OmnI18nClient,
Internals,
TContext as RootContext,
text,
zone,
fallback
fallback,
Translatable,
Translator
} from './types'
import { interpolate } from './interpolation'
import { longKeyList, parseInternals, recurExtend, reports, translator } from './helpers'
import { longKeyList, parseInternals, recurExtend, reports, translate, translator } from './helpers'

export type TContext = RootContext<I18nClient>

Expand All @@ -27,8 +31,6 @@ export default class I18nClient implements OmnI18nClient {
private toLoadZones = new Set<OmnI18n.Zone>()
private loadDefer = new Defer()

public checkOnLoad = new Set<string>()

public timeZone?: string
public currency?: string

Expand All @@ -49,10 +51,6 @@ export default class I18nClient implements OmnI18nClient {
this.cardinalRules = new Intl.PluralRules(locales[0], { type: 'cardinal' })
}

get loading() {
return this.loadDefer.deferring
}

/**
* This should be called for each user-control, page, ...
* If zoning per user role, the call can specify no zone and the zone can be specified on main page-load or user change
Expand Down Expand Up @@ -84,25 +82,6 @@ export default class I18nClient implements OmnI18nClient {
this.internals = parseInternals(this.dictionary.internals)

this.onModification?.(condensed.map(longKeyList).flat())
for (const key of this.checkOnLoad) {
const keys = key.split('.')
let current = this.dictionary
let value = false,
fallenBack: string | undefined
for (const key of keys) {
if (!current[key]) break
if (current[key][text]) {
if (current[key][fallback]) fallenBack = current[key][text]
else {
value = true
break
}
}
current = current[key]
}
if (!value) reports.missing({ key, client: this, zones }, fallenBack)
}
this.checkOnLoad = new Set()
}

private async download(zones: string[]) {
Expand Down Expand Up @@ -144,7 +123,9 @@ export default class I18nClient implements OmnI18nClient {
this.onModification?.(Object.keys(entries))
}

interpolate(context: TContext, text: string, args: any[]): string {
return interpolate(context, text, args)
}
interpolate: (context: TContext, text: string, args: any[]) => string = interpolate
}

export function getContext(translator: Translator): TContext {
return translator[contextKey] as TContext
}
104 changes: 49 additions & 55 deletions src/client/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,16 @@ import {
text,
zone,
fallback,
Translatable,
bulk,
BulkTranslator
contextKey,
Translatable
} from './types'
import { interpolate } from './interpolation'

function entry(t: string, z: string, isFallback?: boolean): ClientDictionary {
return { [text]: t, [zone]: z, ...(isFallback ? { [fallback]: true } : {}) }
}

export function reportMissing(context: TContext, fallback?: string): string {
if (!context.client.loading) return reports.missing(context, fallback)
if (!context.client.onModification) context.client.checkOnLoad.add(context.key)
return reports.loading(context)
}

export const reports = {
loading({ client }: TContext): string {
return '...' // `onModification` callback has been provided
},
/**
* Report a missing translation
* @param key The key that is missing
Expand Down Expand Up @@ -64,49 +54,10 @@ export function translate(context: TContext, args: any[]): string {
}

return value?.[2]
? client.interpolate(context, reportMissing(context, value[0]), args)
? client.interpolate(context, reports.missing(context, value[0]), args)
: value
? client.interpolate(context, value[0], args)
: reportMissing(context)
}

function translateBulk<T extends Translatable = Translatable>(
context: TContext
): BulkTranslator<T> {
return function (source: T | string = '', ...args: any[]): T | string {
const { key, client } = context,
keyPfx = key ? key + '.' : ''
function recursivelyTranslate<T extends Translatable>(obj: T): T {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [
k,
typeof v === 'string'
? translate({ ...context, key: keyPfx + v }, args)
: recursivelyTranslate(v)
])
) as T
}
if (typeof source === 'object') return recursivelyTranslate(source)
let current = client.dictionary
const finalKey = !key ? source : !source ? key : `${key}.${source}`
for (const k of finalKey.split('.')) {
current = current[k] as ClientDictionary
if (!current) break
}
if (!current) return reportMissing(context)
function dictionaryToTranslation(obj: ClientDictionary, key: string): T | string {
const rv: any = {}
const subCtx = { ...context, key }
const value = () =>
interpolate(subCtx, obj[fallback] ? reportMissing(subCtx, obj[text]) : obj[text]!, args)
if (Object.keys(obj).every((k) => typeof k === 'symbol')) return value()
for (const [k, v] of Object.entries(obj))
rv[k] = dictionaryToTranslation(v, key ? `${key}.${k}` : k)
if (obj[text]) Object.defineProperty(rv, 'toString', { value })
return <T>rv
}
return dictionaryToTranslation(current, finalKey)
}
: reports.missing(context)
}

export function translator(context: TContext): Translator {
Expand All @@ -132,8 +83,8 @@ export function translator(context: TContext): Translator {
return String
case 'then': // Must be unthenable in order to be awaited
return 'Translators must be unthenable. `then` cannot be used as a text key.'
case bulk:
return translateBulk(context)
case contextKey:
return Object.freeze(context)
}
if (typeof key !== 'string') throw new TranslationError(`Invalid key type: ${typeof key}`)
return translator({ ...context, key: context.key ? `${context.key}.${key}` : key })
Expand Down Expand Up @@ -204,3 +155,46 @@ export function longKeyList(condensed: OmnI18n.CondensedDictionary) {
recur(condensed, '')
return keys
}

export function bulkObject<T extends Translatable = Translatable>(
t: Translator,
source: T,
...args: any[]
): T {
const context = t[contextKey]
function recursivelyTranslate<T extends Translatable>(obj: T): T {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [
k,
typeof v === 'string' ? translate({ ...context, key: v }, args) : recursivelyTranslate(v)
])
) as T
}
return recursivelyTranslate(source)
}
export function objectFromDictionary<T extends Translatable = Translatable>(
t: Translator,
...args: any[]
): T | string {
const context = t[contextKey],
{ client, key } = context

let current = client.dictionary
for (const k of key.split('.')) {
current = current[k] as ClientDictionary
if (!current) break
}
if (!current) return reports.missing({ ...context, key })
function dictionaryToTranslation(obj: ClientDictionary, key: string): T | string {
const rv: any = {}
const subCtx = { ...context, key }
const value = () =>
interpolate(subCtx, obj[fallback] ? reports.missing(subCtx, obj[text]) : obj[text]!, args)
if (Object.keys(obj).every((k) => typeof k === 'symbol')) return value()
for (const [k, v] of Object.entries(obj))
rv[k] = dictionaryToTranslation(v, key ? `${key}.${k}` : k)
if (obj[text]) Object.defineProperty(rv, 'toString', { value })
return <T>rv
}
return dictionaryToTranslation(current, key)
}
6 changes: 3 additions & 3 deletions src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { default as I18nClient, type TContext } from './client'
export { bulk, type ClientDictionary, TranslationError, type Translator } from './types'
export { reports } from './helpers'
export { default as I18nClient, type TContext, getContext } from './client'
export { type ClientDictionary, TranslationError, type Translator } from './types'
export { reports, bulkObject, objectFromDictionary } from './helpers'
export { formats, processors } from './interpolation'
6 changes: 3 additions & 3 deletions src/client/interpolation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { reportMissing, reports, translate } from './helpers'
import { reports, translate } from './helpers'
import { TContext, TranslationError } from './types'

export const formats: Record<'date' | 'number' | 'relative', Record<string, object>> = {
Expand Down Expand Up @@ -41,7 +41,7 @@ export const processors: Record<string, (...args: any[]) => string> = {
},
ordinal(this: TContext, str: string) {
const { client } = this
if (!client.internals.ordinals) return reportMissing({ ...this, key: 'internals.ordinals' })
if (!client.internals.ordinals) return reports.missing({ ...this, key: 'internals.ordinals' })
const num = parseInt(str)
if (isNaN(num)) return reports.error(this, 'NaN', { str })
return client.internals.ordinals[client.ordinalRules.select(num)].replace('$', str)
Expand All @@ -56,7 +56,7 @@ export const processors: Record<string, (...args: any[]) => string> = {
: designation

if (typeof rules === 'string') {
if (!client.internals.plurals) return reportMissing({ ...this, key: 'internals.plurals' })
if (!client.internals.plurals) return reports.missing({ ...this, key: 'internals.plurals' })
if (!client.internals.plurals[rule])
return reports.error(this, 'Missing rule in plurals', { rule })
return client.internals.plurals[rule].replace('$', designation)
Expand Down
10 changes: 2 additions & 8 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface Internals {
export const zone = Symbol('Zone'),
text = Symbol('Text'),
fallback = Symbol('Fallback'),
bulk = Symbol('Bulk')
contextKey = Symbol('context')

export type ClientDictionary = {
[key: string]: ClientDictionary
Expand All @@ -24,8 +24,6 @@ export interface OmnI18nClient {
timeZone?: string
currency?: string
interpolate(context: TContext, text: string, args: any[]): string
readonly loading: boolean
readonly checkOnLoad: Set<string>
onModification?: OmnI18n.OnModification
}

Expand All @@ -39,13 +37,9 @@ export class TranslationError extends Error {
name = 'TranslationError'
}

export type BulkTranslator<T extends Translatable = Translatable> =
| ((source?: string, ...args: any[]) => T | string)
| ((source: T, ...args: any[]) => T)

export type Translator = ((...args: any[]) => string) & {
[k: string]: Translator
[bulk]<T extends Translatable = Translatable>(source: T | string, ...args: any[]): T | string
[contextKey]: TContext
}

export type Translatable = { [key: string]: Translatable | string }
45 changes: 26 additions & 19 deletions test/specifics.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
FileDB,
I18nClient,
InteractiveServer,
MemDictionary,
TContext,
Translator,
bulk,
bulkObject,
objectFromDictionary,
reports
} from '../src/index'
import { readFile, writeFile, unlink } from 'node:fs/promises'
Expand All @@ -17,43 +19,48 @@ reports.missing = ({ key }: TContext, fallback?: string) => {
}

describe('bulk', () => {
let T: Translator
let T: Translator, client: I18nClient
const expected = {
ok: 'fr-v1',
ok: 'fr-v1.42',
missing: 'en-v2',
sub: { v3: 'fr-v3' }
}

beforeAll(async () => {
const { Tp } = localStack({
'sub.obj.v1': { fr: 'fr-v1' },
'sub.obj.v2': { en: 'en-v2' },
'sub.obj.v3': { fr: 'fr-v3' },
'struct.obj.ok': { fr: 'fr-v1' },
'struct.obj.missing': { en: 'en-v2' },
'struct.obj.sub.v3': { fr: 'fr-v3' },
'struct.obj.sub': { fr: 'toString' }
const { Tp, client: lclClient } = localStack({
'obj.v1': { fr: 'fr-v1.{=parm}' },
'obj.v2': { en: 'en-v2' },
'obj.v3': { fr: 'fr-v3' },
'struct.ok': { fr: 'fr-v1.{=parm}' },
'struct.missing': { en: 'en-v2' },
'struct.sub.v3': { fr: 'fr-v3' },
'struct.sub': { fr: 'toString' }
})
T = await Tp
client = lclClient
})

test('from object', async () => {
misses.mockClear()
expect(
T.sub[bulk]({
ok: 'obj.v1',
missing: 'obj.v2',
sub: { v3: 'obj.v3' }
})
bulkObject(
T,
{
ok: 'obj.v1',
missing: 'obj.v2',
sub: { v3: 'obj.v3' }
},
{ parm: 42 }
)
).toEqual(expected)
expect(misses).toHaveBeenCalledWith('sub.obj.v2')
expect(misses).toHaveBeenCalledWith('obj.v2')
})

test('from dictionary', async () => {
misses.mockClear()
const built = T.struct[bulk]('obj')
const built = objectFromDictionary(T.struct, { parm: 42 })
expect(built).toEqual(expected)
expect(misses).toHaveBeenCalledWith('struct.obj.missing')
expect(misses).toHaveBeenCalledWith('struct.missing')
expect('' + built.sub).toBe('toString')
})
})
Expand Down

0 comments on commit 4bd8548

Please sign in to comment.