Changing domT to a default translation function for browser

This commit is contained in:
Jarosław Sadziński 2022-10-19 20:44:56 +02:00
parent 2586b595a5
commit 2f29df1b17
6 changed files with 58 additions and 23 deletions

View File

@ -1,7 +1,7 @@
import {getGristConfig} from 'app/common/urlUtils'; import {getGristConfig} from 'app/common/urlUtils';
import {DomContents} from 'grainjs'; import {DomContents} from 'grainjs';
import {G} from 'grainjs/dist/cjs/lib/browserGlobals';
import i18next from 'i18next'; import i18next from 'i18next';
import {G} from 'grainjs/dist/cjs/lib/browserGlobals';
export async function setupLocale() { export async function setupLocale() {
const now = Date.now(); const now = Date.now();
@ -72,8 +72,8 @@ export async function setupLocale() {
/** /**
* Resolves the translation of the given key using the given options. * Resolves the translation of the given key using the given options.
*/ */
export function t(key: string, args?: any, instance = i18next): string { export function tString(key: string, args?: any, instance = i18next): string {
if (!instance.exists(key, args)) { 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);
} }
@ -84,15 +84,15 @@ export function t(key: string, args?: any, instance = i18next): string {
/** /**
* 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 domT(key: string, args?: any, instance = i18next): DomContents { export function t(key: string, args?: any, instance = i18next): DomContents {
if (!instance.exists(key)) { 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);
} }
// If there are any DomElements in args, handle it with missingInterpolationHandler. // If there are any DomElements in args, handle it with missingInterpolationHandler.
const domElements = 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 t(key, args); return instance.t(key, args || undefined) as string;
} 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.
@ -104,9 +104,9 @@ export function t(key: string, args?: any, instance = i18next): string {
const result: string = instance.t(key, {...copy, missingInterpolationHandler}); const result: string = instance.t(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(new RegExp(`(${DOM_MARKER}\\w+)`)); const parts = result.split(/(\[\[\[[^\]]+?\]\]\])/);
for (let i = 1; i < parts.length; i += 2) { // Every second element is our dom element. for (let i = 1; i < parts.length; i += 2) { // Every second element is our dom element.
const propName = parts[i].replace(DOM_MARKER, ""); const propName = parts[i].substring(3, parts[i].length - 3);
const domElement = args[propName] ?? `{{${propName}}}`; // If the prop is not there, simulate default behavior. const domElement = args[propName] ?? `{{${propName}}}`; // If the prop is not there, simulate default behavior.
parts[i] = domElement; parts[i] = domElement;
} }
@ -121,9 +121,8 @@ export function hasTranslation(key: string) {
return i18next.exists(key); return i18next.exists(key);
} }
const DOM_MARKER = '__domKey_';
function missingInterpolationHandler(key: string, value: any) { function missingInterpolationHandler(key: string, value: any) {
return `${DOM_MARKER}${value[1]}`; return `[[[${value[1]}]]]`;
} }
/** /**

View File

@ -3,7 +3,7 @@ import {icon} from 'app/client/ui2018/icons';
import {dom, DomElementArg, Observable, styled} from "grainjs"; import {dom, DomElementArg, Observable, styled} from "grainjs";
import {t} from 'app/client/lib/localization'; import {t} from 'app/client/lib/localization';
const translate = (x: string, args?: any): string => t(`AddNewButton.${x}`, args); const translate = (x: string, args?: any) => t(`AddNewButton.${x}`, args);
export function addNewButton(isOpen: Observable<boolean> | boolean = true, ...args: DomElementArg[]) { export function addNewButton(isOpen: Observable<boolean> | boolean = true, ...args: DomElementArg[]) {
return cssAddNewButton( return cssAddNewButton(

View File

@ -34,7 +34,7 @@ import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
import {bigBasicButton} from 'app/client/ui2018/buttons'; import {bigBasicButton} from 'app/client/ui2018/buttons';
import sortBy = require('lodash/sortBy'); import sortBy = require('lodash/sortBy');
const translate = (x: string, args?: any): string => t(`DocMenu.${x}`, args); const translate = (x: string, args?: any) => t(`DocMenu.${x}`, args);
const testId = makeTestId('test-dm-'); const testId = makeTestId('test-dm-');

View File

@ -1,4 +1,4 @@
import {domT, t} from 'app/client/lib/localization'; import {t} from 'app/client/lib/localization';
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState'; import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
import {HomeModel} from 'app/client/models/HomeModel'; import {HomeModel} from 'app/client/models/HomeModel';
import {productPill} from 'app/client/ui/AppHeader'; import {productPill} from 'app/client/ui/AppHeader';
@ -14,7 +14,7 @@ import {FullUser} from 'app/common/LoginSessionAPI';
import * as roles from 'app/common/roles'; import * as roles from 'app/common/roles';
import {Computed, dom, DomContents, styled} from 'grainjs'; import {Computed, dom, DomContents, styled} from 'grainjs';
const translate = (x: string, args?: any): string => t(`HomeIntro.${x}`, args); const translate = (x: string, args?: any) => t(`HomeIntro.${x}`, args);
export function buildHomeIntro(homeModel: HomeModel): DomContents { export function buildHomeIntro(homeModel: HomeModel): DomContents {
const isViewer = homeModel.app.currentOrg?.access === roles.VIEWER; const isViewer = homeModel.app.currentOrg?.access === roles.VIEWER;
@ -103,7 +103,7 @@ function makePersonalIntro(homeModel: HomeModel, user: FullUser) {
css.docListHeader(`Welcome to Grist, ${user.name}!`, testId('welcome-title')), css.docListHeader(`Welcome to Grist, ${user.name}!`, testId('welcome-title')),
cssIntroLine('Get started by creating your first Grist document.'), cssIntroLine('Get started by creating your first Grist document.'),
(shouldHideUiElement('helpCenter') ? null : (shouldHideUiElement('helpCenter') ? null :
cssIntroLine(domT('HomeIntro.VisitHelpCenter', { link: helpCenterLink() }), cssIntroLine(t('HomeIntro.VisitHelpCenter', { link: helpCenterLink() }),
testId('welcome-text')) testId('welcome-text'))
), ),
makeCreateButtons(homeModel), makeCreateButtons(homeModel),
@ -116,7 +116,7 @@ function makeAnonIntro(homeModel: HomeModel) {
css.docListHeader(translate('Welcome'), testId('welcome-title')), css.docListHeader(translate('Welcome'), testId('welcome-title')),
cssIntroLine('Get started by exploring templates, or creating your first Grist document.'), cssIntroLine('Get started by exploring templates, or creating your first Grist document.'),
cssIntroLine(signUp, ' to save your work. ', cssIntroLine(signUp, ' to save your work. ',
(shouldHideUiElement('helpCenter') ? null : domT('HomeIntro.VisitHelpCenter', { link: helpCenterLink() })), (shouldHideUiElement('helpCenter') ? null : t('HomeIntro.VisitHelpCenter', { link: helpCenterLink() })),
testId('welcome-text')), testId('welcome-text')),
makeCreateButtons(homeModel), makeCreateButtons(homeModel),
]; ];

View File

@ -101,6 +101,17 @@ function makeAnonIntro(homeModel: HomeModel) {
css.docListHeader(t('HomeIntro.Welcome'), testId('welcome-title')), css.docListHeader(t('HomeIntro.Welcome'), testId('welcome-title')),
``` ```
Function `t` on the client side is also able to use `DomContents` values (so material produced by
the GrainJS library) for interpolation. For example:
```ts
dom('span', t('Argument', {
arg1: dom('span', 'First'),
arg2: dom.domComputed(obs, (value) => dom('span', value))
}));
```
Some things are not supported at this moment and will need to be addressed in future development Some things are not supported at this moment and will need to be addressed in future development
tasks: tasks:

View File

@ -1,7 +1,7 @@
import {domT, t} from 'app/client/lib/localization'; import {t} from 'app/client/lib/localization';
import {assert} from 'chai'; import {assert} from 'chai';
import i18next, {i18n} from 'i18next'; import i18next, {i18n} from 'i18next';
import {dom} from "grainjs"; import {dom, observable} from "grainjs";
import {popGlobals, pushGlobals, G} from 'grainjs/dist/cjs/lib/browserGlobals'; import {popGlobals, pushGlobals, G} from 'grainjs/dist/cjs/lib/browserGlobals';
import {JSDOM} from 'jsdom'; import {JSDOM} from 'jsdom';
@ -26,31 +26,56 @@ describe('localization', function() {
translation: { translation: {
'Text': 'TranslatedText', 'Text': 'TranslatedText',
'Argument': 'Translated {{arg1}} {{arg2}}.', 'Argument': 'Translated {{arg1}} {{arg2}}.',
'Argument_variant': 'Variant {{arg1}} {{arg2}}.',
} }
} }
} }
}); });
}); });
it('supports basic operation', function() { it('supports basic operation for strings', function() {
assert.equal(t('Text', null, instance), 'TranslatedText');
assert.equal(t('Argument', {arg1: '1', arg2: '2'}, instance), 'Translated 1 2.'); assert.equal(t('Argument', {arg1: '1', arg2: '2'}, instance), 'Translated 1 2.');
assert.equal(t('Argument', {arg1: '1', arg2: '2', context: 'variant'}, instance), 'Variant 1 2.');
assert.equal(t('Text', null, instance), 'TranslatedText');
}); });
it('supports dom content interpolation', function() { it('supports dom content interpolation', function() {
const result = domT('Argument', { const obs = observable("Second");
const result = t('Argument', {
arg1: dom('span', 'First'), arg1: dom('span', 'First'),
arg2: dom.domComputed("test", (value) => dom('span', value)) arg2: dom.domComputed(obs, (value) => dom('span', value))
}, instance) as any; }, instance) as any;
assert.isTrue(Array.isArray(result)); assert.isTrue(Array.isArray(result));
assert.equal(result.length, 5); assert.equal(result.length, 5);
// First we have a plain string.
assert.equal(result[0], 'Translated '); assert.equal(result[0], 'Translated ');
// Next we have a span element.
assert.equal(result[1]?.tagName, 'SPAN'); assert.equal(result[1]?.tagName, 'SPAN');
assert.equal(result[1]?.textContent, 'First'); assert.equal(result[1]?.textContent, 'First');
// Empty space
assert.equal(result[2], ' '); assert.equal(result[2], ' ');
// Element 3 is the domComputed [Comment, Comment, function()] // Element 3 is the domComputed [Comment, Comment, function()]
assert.isTrue(Array.isArray(result[3])); assert.isTrue(Array.isArray(result[3]));
assert.isTrue(result[3][0] instanceof G.Node); assert.isTrue(result[3][0] instanceof G.Node);
assert.isTrue(result[3][1] instanceof G.Node);
assert.isTrue(typeof result[3][2] === 'function');
// As last we have "."
assert.equal(result[4], '.'); assert.equal(result[4], '.');
// Make sure that computed works.
const span = dom('span', result);
assert.equal(span.textContent, "Translated First Second.");
obs.set("Third");
assert.equal(span.textContent, "Translated First Third.");
// Test that context variable works.
const variantSpan = dom('span', t('Argument', {
arg1: dom('span', 'First'),
arg2: dom.domComputed(obs, (value) => dom('span', value)),
context: 'variant'
}, instance));
assert.equal(variantSpan.textContent, "Variant First Third.");
obs.set("Fourth");
assert.equal(variantSpan.textContent, "Variant First Fourth.");
}); });
}); });