Skip to content

Commit

Permalink
doc-ing
Browse files Browse the repository at this point in the history
  • Loading branch information
eddow committed Apr 30, 2024
1 parent fc7fd19 commit ced6183
Show file tree
Hide file tree
Showing 14 changed files with 259 additions and 66 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ name: Node.js CI

on:
push:
branches: [docs]
branches: [main]
pull_request:
branches: [docs]
branches: [main]

jobs:
build:
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ console.log(T('msg.hello'))

The full-stack case will insert the http protocol between `client` and `server`. The `condense` function takes few arguments and return a (promise of) json-able object so can go through an http request.

The "Omni" part is that it can be integrated for various asynchronous scenarios and in many frameworks.

### Interactive mode

In interactive mode (using `InteractiveServer`), the DB interface contains modification functions and the server exposes modification function, that will modify the DB but also raise events. In this case, an `InteractiveServer` instance has to be created for every client, with an interface toward the DB and a callback for event raising.
Expand Down Expand Up @@ -257,13 +259,17 @@ import { reports, type TContext } from "omni18n";
client: I18nClient
}*/

reports.missing = ({key, client}: TContext) {
if (client.loading) return `...` // `onModification` callback has been provided
return `[${key}]`
reports.loading = ({ key, client }: TContext): string {
// report if not expected
return '...'
}
reports.missing = ({ key, client }: TContext, fallback?: string): string {
// report
return fallback ?? `[${key}]`
}
reports.error = (context: TContext, error: string, spec: object) {
if (client.loading) return `...` // `onModification` callback has been provided
return `[!${error}]`
// report
return `[!${error}]`
}
```

Expand Down
11 changes: 9 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# OmnI18n

Test GH-pages
[Overview](../README.md)

> :warning: **Work in progress!**
Projects using OmnI18n use it in 4 layers
1. [The `client`](./client.md): The client manages the cache and download along with text retrieval and interpolation
2. (optional) The HTTP or any other layer. This part is implemented by the user
3. The `server`: The server exposes functions to interact with the languages
4. The `database`: A class implementing some interface that interacts directly with a database

[Part 2](./part2)
77 changes: 77 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Client part

The client part `I18nClient` is usually instantiated once per client.

For instance, in the browser for an SPA is is instantiated once for the whole web-application lifetime, while on the server-side for SSR, it might be instantiated once per request or per session.

## Interactions and configurations

### With the server

```ts
I18nClient(locales: OmnI18n.Locale[], condense: OmnI18n.Condense, onModification?: OmnI18n.OnModification)
```

- `locales`: A list of locales: from preferred to fallback
- `condense`: A function that will query the server for the condensed dictionary
- `onModification`: A function that will be called when the dictionary is modified

```ts
const client = new I18nClient(['fr', 'en'], server.condense, frontend.refreshTexts)
```

### Global settings

These are variables you can import and modify:

```ts
import { reports, formats, processors } from 'omni18n'
```

#### `reports`

Reporting mechanism in case of problem. They both take an argument of type `TContext` describing mainly the client and the key where the problem occurred
```ts
export interface TContext {
key: string
zones: string[]
client: I18nClient
}
```

> If texts might be displayed before loading is complete, make sure `onModification` has been specified as it will be called when the translations will be provided
These reports will:
- have any side effect, like logging or making a request that will log
- return a string that will be used instead of the expected translation

`reports` contain:

- A missing key report
```ts
reports.missing = ({ key, client }: TContext, fallback?: string)=> {
// report
return fallback ?? `[${key}]`
}
```

- 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.

> The specification is json-able *except* in the case of `error: "Error in processor"`, in which case `spec.error` is whatever had been thrown and might be an `Error` or `Exception`
```ts
reports.error = ({ key, client }: TContext, error: string, spec: object)=> {
// report
return '[!error!]'
}
```

#### `formats`

#### `processors`
1 change: 0 additions & 1 deletion jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
},
"testEnvironment": "node",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
"collectCoverage": true,
"coverageReporters": ["lcov"]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"type": "module",
"scripts": {
"test": "jest",
"test:cover": "jest --coverage",
"prettier": "prettier --write .",
"build": "rollup -c",
"jsdoc": "jsdoc",
Expand Down
42 changes: 40 additions & 2 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@
*/
import '../polyfill'
import Defer from '../defer'
import { ClientDictionary, OmnI18nClient, Internals, TContext, text, zone } from './types'
import {
ClientDictionary,
OmnI18nClient,
Internals,
TContext as RootContext,
text,
zone,
fallback
} from './types'
import { interpolate } from './interpolation'
import { longKeyList, parseInternals, recurExtend, translator } from './helpers'
import { longKeyList, parseInternals, recurExtend, reports, translator } from './helpers'

export type TContext = RootContext<I18nClient>

export default class I18nClient implements OmnI18nClient {
readonly ordinalRules: Intl.PluralRules
Expand All @@ -18,9 +28,18 @@ export default class I18nClient implements OmnI18nClient {
private loadDefer = new Defer()

public loaded: Promise<void> = Promise.resolve()
public checkOnLoad = new Set<string>()

public timeZone?: string
public currency?: string

/**
*
* @param locales A list of locales: from preferred to fallback
* @param condense A function that will query the server for the condensed dictionary
* @param onModification A function that will be called when the dictionary is modified
* @example new I18nClient(['fr', 'en'], server.condense, frontend.refreshTexts)
*/
constructor(
public locales: OmnI18n.Locale[],
// On the server side, this is `server.condensed`. From the client-side this is an http request of some sort
Expand Down Expand Up @@ -66,6 +85,25 @@ 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
93 changes: 64 additions & 29 deletions src/client/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
import { parse } from 'hjson'
import { ClientDictionary, TContext, TranslationError, Translator, text, zone } from './types'
import {
ClientDictionary,
TContext,
TranslationError,
Translator,
text,
zone,
fallback
} from './types'

function entry(t: string, z: string): ClientDictionary {
return { [text]: t, [zone]: z }
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 = {
missing({ key, client }: TContext): string {
if (client.loading) return `...` // `onModification` callback has been provided
return `[${key}]`
loading({ client }: TContext): string {
return '...' // `onModification` callback has been provided
},
error({ client }: TContext, error: string, spec: object): string {
if (client.loading) return `...` // `onModification` callback has been provided
/**
* Report a missing translation
* @param key The key that is missing
* @param client The client that is missing the translation. The expected locale is in `client.locales[0]`
* @param fallback A fallback from another language if any
* @returns The string to display instead of the expected translation
*/
missing({ key, client }: TContext, fallback?: string): string {
return fallback ?? `[${key}]`
},
/**
* Report a missing translation
* @param key The key that is missing
* @param client The client that is missing the translation. The expected locale is in `client.locales[0]`
* @param fallback A fallback from another language if any
* @returns The string to display instead of the expected translation
*/
error({ key, client }: TContext, error: string, spec: object): string {
return `[!${error}]`
}
}
Expand All @@ -20,19 +49,22 @@ export function translate(context: TContext, args: any[]): string {
const { client, key } = context,
keys = key.split('.')
let current = client.dictionary,
value: [string, string] | undefined
value: [string, string, true | undefined] | undefined

for (const k of keys) {
if (!current[k]) break
else {
const next = current[k] as ClientDictionary
if (text in next) value = [next[text]!, next[zone]!]
if (text in next) value = [next[text]!, next[zone]!, next[fallback]]
current = next
}
}
// This case can happen for example in role-zoning, when roles are entered separately
//if (value && !context.zones.includes(value[1])) reports.missing(context, value[1])
return value ? client.interpolate(context, value[0], args) : reports.missing(context)

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

export function translator(context: TContext): Translator {
Expand Down Expand Up @@ -74,9 +106,10 @@ function condensed2dictionary(
condensed: OmnI18n.CondensedDictionary,
zone: OmnI18n.Zone
): ClientDictionary {
const dictionary: ClientDictionary = '' in condensed ? entry(condensed['']!, zone) : {}
const dictionary: ClientDictionary =
'' in condensed ? entry(condensed['']!, zone, !!condensed['.']) : {}
for (const key in condensed)
if (key) {
if (!['', '.'].includes(key)) {
const value = condensed[key]
if (typeof value === 'string') dictionary[key] = entry(value, zone)
else dictionary[key] = condensed2dictionary(value, zone)
Expand All @@ -89,21 +122,23 @@ export function recurExtend(
src: OmnI18n.CondensedDictionary,
zone: OmnI18n.Zone
) {
for (const key in src) {
if (!dst[key])
dst[key] =
typeof src[key] === 'string'
? entry(<string>src[key], zone)
: condensed2dictionary(<OmnI18n.CondensedDictionary>src[key], zone)
else {
if (typeof src[key] === 'string')
dst[key] = {
...dst[key],
...entry(<string>src[key], zone)
}
else recurExtend(dst[key], <OmnI18n.CondensedDictionary>src[key], zone)
for (const key in src)
if (key === '') Object.assign(dst, entry(src[key]!, zone, !!src['.']))
else if (key !== '.') {
if (!dst[key])
dst[key] =
typeof src[key] === 'string'
? entry(<string>src[key], zone)
: condensed2dictionary(<OmnI18n.CondensedDictionary>src[key], zone)
else {
if (typeof src[key] === 'string')
dst[key] = {
...dst[key],
...entry(<string>src[key], zone)
}
else recurExtend(dst[key], <OmnI18n.CondensedDictionary>src[key], zone)
}
}
}
}

export function longKeyList(condensed: OmnI18n.CondensedDictionary) {
Expand Down
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 } from './client'
export { type ClientDictionary, type TContext, TranslationError, type Translator } from './types'
export { default as I18nClient, type TContext } from './client'
export { type ClientDictionary, TranslationError, type Translator } from './types'
export { translator, reports } from './helpers'
export { formats, globals, processors } from './interpolation'
export { formats, processors } from './interpolation'
Loading

0 comments on commit ced6183

Please sign in to comment.