mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
Ignoring reserved characters in a resource key in a scoped translation helper
This commit is contained in:
parent
8c610dcb33
commit
349c8acfdc
@ -88,15 +88,20 @@ type InferResult<T> = T extends Record<string, string | number | boolean>|undefi
|
|||||||
/**
|
/**
|
||||||
* Resolves the translation of the given key and substitutes. Supports dom elements interpolation.
|
* Resolves the translation of the given key and substitutes. Supports dom elements interpolation.
|
||||||
*/
|
*/
|
||||||
export function t<T extends Record<string, any>>(key: string, args?: T|null, instance = i18next): InferResult<T> {
|
export function t<T extends Record<string, any>>(key: string, args?: T|null, instance = i18next): InferResult<T> {
|
||||||
if (!instance.exists(key, args || undefined)) {
|
if (!instance.exists(key, args || undefined)) {
|
||||||
const error = new Error(`Missing translation for key: ${key} and language: ${i18next.language}`);
|
const error = new Error(`Missing translation for key: ${key} and language: ${i18next.language}`);
|
||||||
reportError(error);
|
reportError(error);
|
||||||
}
|
}
|
||||||
|
// Don't need to bind `t` function.
|
||||||
|
return domT(key, args, instance.t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function domT(key: string, args: any, tImpl: typeof i18next.t) {
|
||||||
// If there are any DomElements in args, handle it with missingInterpolationHandler.
|
// If there are any DomElements in args, handle it with missingInterpolationHandler.
|
||||||
const domElements = !args ? [] : Object.entries(args).filter(([_, value]) => isLikeDomContents(value));
|
const domElements = !args ? [] : Object.entries(args).filter(([_, value]) => isLikeDomContents(value));
|
||||||
if (!args || !domElements.length) {
|
if (!args || !domElements.length) {
|
||||||
return instance.t(key, args || undefined) as any;
|
return tImpl(key, args || undefined) as any;
|
||||||
} else {
|
} else {
|
||||||
// Make a copy of the arguments, and remove any dom elements from it. It will instruct
|
// Make a copy of the arguments, and remove any dom elements from it. It will instruct
|
||||||
// i18next library to use `missingInterpolationHandler` handler.
|
// i18next library to use `missingInterpolationHandler` handler.
|
||||||
@ -105,7 +110,7 @@ type InferResult<T> = T extends Record<string, string | number | boolean>|undefi
|
|||||||
|
|
||||||
// Passing `missingInterpolationHandler` will allow as to resolve all missing keys
|
// Passing `missingInterpolationHandler` will allow as to resolve all missing keys
|
||||||
// and replace them with a marker.
|
// and replace them with a marker.
|
||||||
const result: string = instance.t(key, {...copy, missingInterpolationHandler});
|
const result: string = tImpl(key, {...copy, missingInterpolationHandler});
|
||||||
|
|
||||||
// Now replace all markers with dom elements passed as arguments.
|
// Now replace all markers with dom elements passed as arguments.
|
||||||
const parts = result.split(/(\[\[\[[^\]]+?\]\]\])/);
|
const parts = result.split(/(\[\[\[[^\]]+?\]\]\])/);
|
||||||
@ -141,10 +146,30 @@ function isLikeDomContents(value: any): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to create scoped t function.
|
* Helper function to create a scoped t function. Scoped t function is bounded to a specific
|
||||||
|
* namespace and a key prefix (a scope).
|
||||||
*/
|
*/
|
||||||
export function makeT(scope: string) {
|
export function makeT(scope: string, instance?: typeof i18next) {
|
||||||
return function<T extends Record<string, any>>(key: string, args?: T|null, instance = i18next) {
|
// Can create the scopedInstance yet as it might not be initialized.
|
||||||
return t(`${scope}.${key}`, args, instance);
|
let scopedInstance: null|typeof i18next = null;
|
||||||
|
let scopedResolver: null|typeof i18next.t = null;
|
||||||
|
return function<T extends Record<string, any>>(key: string, args?: T|null) {
|
||||||
|
// Create a scoped instance with disabled namespace and nested features.
|
||||||
|
// This enables keys like `key1.key2:key3` to be resolved properly.
|
||||||
|
if (!scopedInstance) {
|
||||||
|
scopedInstance = (instance ?? i18next).cloneInstance({
|
||||||
|
keySeparator: false,
|
||||||
|
nsSeparator: false,
|
||||||
|
});
|
||||||
|
// Create a version of `t` function that will use the provided prefix as default.
|
||||||
|
scopedResolver = scopedInstance.getFixedT(null, null, scope);
|
||||||
|
}
|
||||||
|
// If the key has interpolation or we did pass some arguments, make sure that
|
||||||
|
// the key exists.
|
||||||
|
if ((args || key.includes("{{")) && !scopedInstance.exists(`${scope}.${key}`, args || undefined)) {
|
||||||
|
const error = new Error(`Missing translation for key: ${key} and language: ${i18next.language}`);
|
||||||
|
reportError(error);
|
||||||
|
}
|
||||||
|
return domT(key, args, scopedResolver!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ describe('localization', function() {
|
|||||||
'Argument_variant': 'Variant {{arg1}} {{arg2}}{{end}}',
|
'Argument_variant': 'Variant {{arg1}} {{arg2}}{{end}}',
|
||||||
'Parent': {
|
'Parent': {
|
||||||
'Child': 'Translated child {{arg}}',
|
'Child': 'Translated child {{arg}}',
|
||||||
|
'Not.Valid:Characters': 'Works',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,8 +94,8 @@ describe('localization', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('supports scoping through makeT', function() {
|
it('supports scoping through makeT', function() {
|
||||||
const scoped = makeT('Parent');
|
const scoped = makeT('Parent', instance);
|
||||||
assert.equal(scoped('Child', { arg : 'Arg'}, instance), 'Translated child Arg');
|
assert.equal(scoped('Child', { arg : 'Arg'}), 'Translated child Arg');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('infers result from parameters', function() {
|
it('infers result from parameters', function() {
|
||||||
@ -108,17 +109,22 @@ describe('localization', function() {
|
|||||||
typeString = t('Argument', {arg1: 'argument 1', arg2: 'argument 2'}, instance);
|
typeString = t('Argument', {arg1: 'argument 1', arg2: 'argument 2'}, instance);
|
||||||
typeString = t('Argument', {arg1: 1, arg2: true}, instance);
|
typeString = t('Argument', {arg1: 1, arg2: true}, instance);
|
||||||
typeString = t('Argument', undefined, instance);
|
typeString = t('Argument', undefined, instance);
|
||||||
const scoped = makeT('Parent');
|
const scoped = makeT('Parent', instance);
|
||||||
typeString = scoped('Child', {arg: 'argument 1'}, instance);
|
typeString = scoped('Child', {arg: 'argument 1'});
|
||||||
typeString = scoped('Child', {arg: 1}, instance);
|
typeString = scoped('Child', {arg: 1});
|
||||||
typeString = scoped('Child', undefined, instance);
|
typeString = scoped('Child', undefined);
|
||||||
|
|
||||||
let domContent: DomContents = null; void domContent;
|
let domContent: DomContents = null; void domContent;
|
||||||
|
|
||||||
domContent = t('Argument', {arg1: 'argument 1', arg2: dom('span')}, instance);
|
domContent = t('Argument', {arg1: 'argument 1', arg2: dom('span')}, instance);
|
||||||
domContent = t('Argument', {arg1: 1, arg2: dom.domComputed(observable('test'))}, instance);
|
domContent = t('Argument', {arg1: 1, arg2: dom.domComputed(observable('test'))}, instance);
|
||||||
domContent = t('Argument', undefined, instance);
|
domContent = t('Argument', undefined, instance);
|
||||||
domContent = scoped('Child', {arg: dom.create(Component)}, instance);
|
domContent = scoped('Child', {arg: dom.create(Component)});
|
||||||
domContent = scoped('Child', {arg: dom.maybe(observable(true), () => dom('span'))}, instance);
|
domContent = scoped('Child', {arg: dom.maybe(observable(true), () => dom('span'))});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports : and . characters in scoped function', function() {
|
||||||
|
const scoped = makeT('Parent', instance);
|
||||||
|
assert.equal(scoped('Not.Valid:Characters'), 'Works');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user