mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	Adding domT method for component interpolation
This commit is contained in:
		
							parent
							
								
									4ebffff06d
								
							
						
					
					
						commit
						2586b595a5
					
				| @ -1,4 +1,6 @@ | ||||
| import {getGristConfig} from 'app/common/urlUtils'; | ||||
| import {DomContents} from 'grainjs'; | ||||
| import {G} from 'grainjs/dist/cjs/lib/browserGlobals'; | ||||
| import i18next from 'i18next'; | ||||
| 
 | ||||
| export async function setupLocale() { | ||||
| @ -68,14 +70,48 @@ 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): string { | ||||
|   if (!i18next.exists(key, args)) { | ||||
| export function t(key: string, args?: any, instance = i18next): string { | ||||
|   if (!instance.exists(key, args)) { | ||||
|     const error = new Error(`Missing translation for key: ${key} and language: ${i18next.language}`); | ||||
|     reportError(error); | ||||
|   } | ||||
|   return i18next.t(key, args); | ||||
|   return instance.t(key, args); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Resolves the translation of the given key and substitutes. Supports dom elements interpolation. | ||||
|  */ | ||||
|  export function domT(key: string, args?: any, instance = i18next): DomContents { | ||||
|   if (!instance.exists(key)) { | ||||
|     const error = new Error(`Missing translation for key: ${key} and language: ${i18next.language}`); | ||||
|     reportError(error); | ||||
|   } | ||||
|   // If there are any DomElements in args, handle it with missingInterpolationHandler.
 | ||||
|   const domElements = Object.entries(args).filter(([_, value]) => isLikeDomContents(value)); | ||||
|   if (!args || !domElements.length) { | ||||
|     return t(key, args); | ||||
|   } else { | ||||
|     // Make a copy of the arguments, and remove any dom elements from it. It will instruct
 | ||||
|     // i18next library to use `missingInterpolationHandler` handler.
 | ||||
|     const copy = {...args}; | ||||
|     domElements.forEach(([prop]) => delete copy[prop]); | ||||
| 
 | ||||
|     // Passing `missingInterpolationHandler` will allow as to resolve all missing keys
 | ||||
|     // and replace them with a marker.
 | ||||
|     const result: string = instance.t(key, {...copy, missingInterpolationHandler}); | ||||
| 
 | ||||
|     // Now replace all markers with dom elements passed as arguments.
 | ||||
|     const parts = result.split(new RegExp(`(${DOM_MARKER}\\w+)`)); | ||||
|     for (let i = 1; i < parts.length; i += 2) { // Every second element is our dom element.
 | ||||
|       const propName = parts[i].replace(DOM_MARKER, ""); | ||||
|       const domElement = args[propName] ?? `{{${propName}}}`; // If the prop is not there, simulate default behavior.
 | ||||
|       parts[i] = domElement; | ||||
|     } | ||||
|     return parts; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
| @ -84,3 +120,19 @@ export function t(key: string, args?: any): string { | ||||
| export function hasTranslation(key: string) { | ||||
|   return i18next.exists(key); | ||||
| } | ||||
| 
 | ||||
| const DOM_MARKER = '__domKey_'; | ||||
| function missingInterpolationHandler(key: string, value: any) { | ||||
|   return `${DOM_MARKER}${value[1]}`; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Very naive detection if an element has DomContents type. | ||||
|  */ | ||||
| function isLikeDomContents(value: any): boolean { | ||||
|   // As null and undefined are valid DomContents values, we don't treat them as such.
 | ||||
|   if (value === null || value === undefined) { return false; } | ||||
|   return value instanceof G.Node || // Node
 | ||||
|     (Array.isArray(value) && isLikeDomContents(value[0])) || // DomComputed
 | ||||
|     typeof value === 'function'; // DomMethod
 | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import {t} from 'app/client/lib/localization'; | ||||
| import {domT, t} from 'app/client/lib/localization'; | ||||
| import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState'; | ||||
| import {HomeModel} from 'app/client/models/HomeModel'; | ||||
| import {productPill} from 'app/client/ui/AppHeader'; | ||||
| @ -103,7 +103,7 @@ function makePersonalIntro(homeModel: HomeModel, user: FullUser) { | ||||
|     css.docListHeader(`Welcome to Grist, ${user.name}!`, testId('welcome-title')), | ||||
|     cssIntroLine('Get started by creating your first Grist document.'), | ||||
|     (shouldHideUiElement('helpCenter') ? null : | ||||
|       cssIntroLine('Visit our ', helpCenterLink(), ' to learn more.', | ||||
|       cssIntroLine(domT('HomeIntro.VisitHelpCenter', { link: helpCenterLink() }), | ||||
|         testId('welcome-text')) | ||||
|     ), | ||||
|     makeCreateButtons(homeModel), | ||||
| @ -115,8 +115,8 @@ function makeAnonIntro(homeModel: HomeModel) { | ||||
|   return [ | ||||
|     css.docListHeader(translate('Welcome'), testId('welcome-title')), | ||||
|     cssIntroLine('Get started by exploring templates, or creating your first Grist document.'), | ||||
|     cssIntroLine(signUp, ' to save your work.', | ||||
|       (shouldHideUiElement('helpCenter') ? null : [' Visit our ', helpCenterLink(), ' to learn more.']), | ||||
|     cssIntroLine(signUp, ' to save your work. ', | ||||
|       (shouldHideUiElement('helpCenter') ? null : domT('HomeIntro.VisitHelpCenter', { link: helpCenterLink() })), | ||||
|       testId('welcome-text')), | ||||
|     makeCreateButtons(homeModel), | ||||
|   ]; | ||||
|  | ||||
| @ -12,6 +12,7 @@ | ||||
|   }, | ||||
|   "HomeIntro": { | ||||
|     "Welcome": "Welcome to Grist!", | ||||
|     "SignUp": "Sign up" | ||||
|     "SignUp": "Sign up", | ||||
|     "VisitHelpCenter": "Visit our {{link}} to learn more." | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										56
									
								
								test/client/lib/localization.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								test/client/lib/localization.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| import {domT, t} from 'app/client/lib/localization'; | ||||
| import {assert} from 'chai'; | ||||
| import i18next, {i18n} from 'i18next'; | ||||
| import {dom} from "grainjs"; | ||||
| import {popGlobals, pushGlobals, G} from 'grainjs/dist/cjs/lib/browserGlobals'; | ||||
| import {JSDOM} from 'jsdom'; | ||||
| 
 | ||||
| describe('localization', function() { | ||||
|   beforeEach(function() { | ||||
|     // These grainjs browserGlobals are needed for using dom() in tests.
 | ||||
|     const jsdomDoc = new JSDOM("<!doctype html><html><body></body></html>"); | ||||
|     pushGlobals(jsdomDoc.window); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(function() { | ||||
|     popGlobals(); | ||||
|   }); | ||||
| 
 | ||||
|   let instance: i18n; | ||||
|   before(() => { | ||||
|     instance = i18next.createInstance(); | ||||
|     instance.init({ | ||||
|       lng: 'en', | ||||
|       resources: { | ||||
|         en: { | ||||
|           translation: { | ||||
|             'Text': 'TranslatedText', | ||||
|             'Argument': 'Translated {{arg1}} {{arg2}}.', | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   it('supports basic operation', function() { | ||||
|     assert.equal(t('Text', null, instance), 'TranslatedText'); | ||||
|     assert.equal(t('Argument', {arg1: '1', arg2: '2'}, instance), 'Translated 1 2.'); | ||||
|   }); | ||||
| 
 | ||||
|   it('supports dom content interpolation', function() { | ||||
|     const result = domT('Argument', { | ||||
|       arg1: dom('span', 'First'), | ||||
|       arg2: dom.domComputed("test", (value) => dom('span', value)) | ||||
|     }, instance) as any; | ||||
|     assert.isTrue(Array.isArray(result)); | ||||
|     assert.equal(result.length, 5); | ||||
|     assert.equal(result[0], 'Translated '); | ||||
|     assert.equal(result[1]?.tagName, 'SPAN'); | ||||
|     assert.equal(result[1]?.textContent, 'First'); | ||||
|     assert.equal(result[2], ' '); | ||||
|     // Element 3 is the domComputed [Comment, Comment, function()]
 | ||||
|     assert.isTrue(Array.isArray(result[3])); | ||||
|     assert.isTrue(result[3][0] instanceof G.Node); | ||||
|     assert.equal(result[4], '.'); | ||||
|   }); | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user