diff --git a/README.md b/README.md index 1ba369e..63d32a5 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,7 @@ Generic i18n library managing the fullstack interaction in a CI/CD pace. The dictionaries are stored in a DB edited by the translators through a(/the same) web application - managing translation errors, missing keys, ... -- 1.0.x - ~~alpha~~ - 1.1.x - [beta](https://www.youtube.com/watch?v=1gSZfX91zYk) -- 1.1.5 - The library finally has well-set entry points and export bundles -- 1.1.8 - UMD-able [![view on npm](https://badgen.net/npm/v/omni18n)](https://www.npmjs.org/package/omni18n) [![npm module downloads](https://badgen.net/npm/dt/omni18n)](https://www.npmjs.org/package/omni18n) diff --git a/docs/umd.md b/docs/umd.md index 5a846cd..e685297 100644 --- a/docs/umd.md +++ b/docs/umd.md @@ -59,11 +59,7 @@ A script-less way to use the library is by providing the arguments (`locales`, ` We speak about the "blink" when the page is just loaded and still displayed in its native language for half a second before being translated in the target language. -[_For now_](#todo), the solution needs to specify manually all the locales who shouldn't blink. - -```html - -``` +[_For now_](#todo), the best solution is to let the texts empty when an `i18n` attribute is specified Also, as many mobile webapp tend to let the resource loading at the end of the page, hurrying the translation by inserting a `translatePage` between the page content and the late loads (audio/scripts/...) can show useful. diff --git a/src/client/helpers.ts b/src/client/helpers.ts index be288f5..2d77693 100644 --- a/src/client/helpers.ts +++ b/src/client/helpers.ts @@ -72,13 +72,17 @@ export function translate(context: TContext, args: any[]): string { export function translator(context: TContext): Translator { Object.freeze(context) + function escapeArgs(args: any[]) { + // TODO? replace more that \ and : + return args.map((a) => (typeof a === 'string' ? a.replace(/([\\:])/g, '\\$1') : a)) + } const translation = context.key ? function (...args: any[]): string { - return translate(context, args) + return translate(context, escapeArgs(args)) } : function (key?: TextKey, ...args: any[]): string { if (!key) throw new TranslationError('Root translator called without key') - if (typeof key === 'string') return translate({ ...context, key }, args) + if (typeof key === 'string') return translate({ ...context, key }, escapeArgs(args)) return translate({ ...context, key }, args) } const primitive = new Proxy(translation, { @@ -232,3 +236,9 @@ export function bulkDictionary( } return dictionaryToTranslation(current, key) } + +export function split2(s: string, sep: string) { + const ndx = s.indexOf(sep) + if (ndx === -1) return [s] + return [s.slice(0, ndx), s.slice(ndx + sep.length)] +} diff --git a/src/client/interpolation.ts b/src/client/interpolation.ts index 8cb14c4..d52cbcf 100644 --- a/src/client/interpolation.ts +++ b/src/client/interpolation.ts @@ -1,4 +1,4 @@ -import { reportMissing, reportError, translate } from './helpers' +import { reportMissing, reportError, translate, split2 } from './helpers' import { TContext, TranslationError } from './types' export const formats: Record<'date' | 'number' | 'relative', Record> = { @@ -165,9 +165,9 @@ function objectArgument( if (typeof arg === 'object') return arg // Here we throw as it means the code gave a wrong argument if (typeof arg !== 'string') throw new TranslationError(`Invalid argument type: ${typeof arg}`) - if (!/:/.test(arg)) return arg + if (!/:[^\/\\]/.test(arg)) return arg return Object.fromEntries( - arg.split(',').map((part) => part.split(':', 2).map((part) => unescape(part.trim()))) + arg.split(',').map((part) => split2(part, ':').map((part) => unescape(part.trim()))) ) } @@ -189,13 +189,17 @@ export function interpolate(context: TContext, text: string, args: any[]): strin const escapements: Record = { '/': '/' }, unescapements: Record = {} let escapementCounter = 0 - placeholder = placeholder.replace(/\\(.)/g, (_, c) => { - if (!escapements[c]) { - unescapements[escapementCounter] = c - escapements[c] = '\u0004' + escapementCounter++ + '\u0005' - } - return escapements[c] - }) + function escaped(s: string) { + return s.replace(/\\(.)/g, (_, c) => { + if (!escapements[c]) { + unescapements[escapementCounter] = c + escapements[c] = '\u0004' + escapementCounter++ + '\u0005' + } + return escapements[c] + }) + } + placeholder = escaped(placeholder) + args = args.map((arg) => (typeof arg === 'string' ? escaped(arg) : arg)) function unescape(s: string) { return s .replace(/\u0003/g, '\n') diff --git a/src/umd/client.ts b/src/umd/client.ts index faa721e..5b13d69 100644 --- a/src/umd/client.ts +++ b/src/umd/client.ts @@ -1,3 +1,4 @@ +import { split2 } from 'src/client/helpers' import { I18nClient, CondensedDictionary, @@ -47,7 +48,7 @@ export function translatePage() { 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()) + const [attr, key] = split2(part, ':').map((k) => k.trim()) if (attr === 'html') element.innerHTML = T[key]() if (key) element.setAttribute(attr, T[key]()) else element.textContent = T[attr]() @@ -59,7 +60,7 @@ export function translatePage() { 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()) + const [attr, key] = split2(part, ':').map((k) => k.trim()) if (attr === 'html' || !key) element.textContent = '' } } @@ -80,7 +81,7 @@ export function translatePage() { localeName = new Intl.DisplayNames(locale, { type: 'language' }).of(locale), selected = usedLocale === locale ? 'selected' : '' selectionList.push(` - @@ -102,7 +103,7 @@ export function translatePage() { `, localeName = new Intl.DisplayNames(locale, { type: 'language' }).of(locale) currentLocaleElm.innerHTML = ` -