diff --git a/package.json b/package.json index 7d3b3c32b..85e4d03c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "opendatacapture", "type": "module", - "version": "1.8.8", + "version": "1.8.9", "private": true, "packageManager": "pnpm@9.15.4", "license": "Apache-2.0", diff --git a/packages/runtime-core/src/i18n.ts b/packages/runtime-core/src/i18n.ts index 71f258d7e..f213cf7f0 100644 --- a/packages/runtime-core/src/i18n.ts +++ b/packages/runtime-core/src/i18n.ts @@ -2,7 +2,7 @@ import { get } from 'lodash-es'; import type { Language } from './types/core.js'; -function InitializedOnly( +function InitializedOnly( target: (this: T, ...args: TArgs) => TReturn, context: ClassGetterDecoratorContext | ClassMethodDecoratorContext | ClassSetterDecoratorContext ) { @@ -29,45 +29,65 @@ export type TranslationKey export type LanguageChangeHandler = (this: void, language: Language) => void; /** @public */ -export class Translator { - isInitialized: boolean; - #fallbackLanguage: Language; - #handleLanguageChange: LanguageChangeHandler | null; - #resolvedLanguage: Language; - #translations: T; - - constructor(options: { fallbackLanguage?: Language; translations: T }) { - this.isInitialized = false; - this.#fallbackLanguage = options.fallbackLanguage ?? 'en'; - this.#handleLanguageChange = null; - this.#resolvedLanguage = this.#fallbackLanguage; - this.#translations = options.translations; +export type TranslatorOptions = { + fallbackLanguage?: Language; + translations: T; +}; + +/** @public */ +export type TranslatorInitOptions = { + onLanguageChange?: LanguageChangeHandler | null; +}; + +/** @public */ +export abstract class BaseTranslator { + protected currentDocumentLanguage: Language | null; + protected fallbackLanguage: Language; + protected handleLanguageChange: LanguageChangeHandler | null; + protected translations: T; + #isInitialized: boolean; + + constructor({ fallbackLanguage, translations }: TranslatorOptions) { + this.currentDocumentLanguage = null; + this.fallbackLanguage = fallbackLanguage ?? 'en'; + this.handleLanguageChange = null; + this.#isInitialized = false; + this.translations = translations; + } + + get isInitialized() { + return this.#isInitialized; + } + + protected set isInitialized(value: boolean) { + this.#isInitialized = value; } @InitializedOnly set onLanguageChange(handler: LanguageChangeHandler) { - this.#handleLanguageChange = handler; + this.handleLanguageChange = handler; } @InitializedOnly get resolvedLanguage() { - return this.#resolvedLanguage; + return this.currentDocumentLanguage ?? this.fallbackLanguage; } - @InitializedOnly - changeLanguage(language: Language) { - window.top!.document.dispatchEvent(new CustomEvent('changeLanguage', { detail: language })); - } + abstract changeLanguage(language: Language): void; - init(options?: { onLanguageChange?: LanguageChangeHandler | null }) { - if (typeof window === 'undefined') { - throw new Error('Cannot initialize Translator outside of browser'); - } else if (!window.frameElement) { - throw new Error('Cannot initialize Translator in context where window.frameElement is null'); + @InitializedOnly + protected extractLanguageProperty(element: Element) { + const lang = element.getAttribute('lang'); + if (lang === 'en' || lang === 'fr') { + return lang; } + console.error(`Unexpected value for 'lang' attribute: '${lang}'`); + return null; + } + init(options: TranslatorInitOptions, targetElement: Element) { this.isInitialized = true; - this.#resolvedLanguage = this.extractLanguageProperty(window.frameElement); + this.currentDocumentLanguage = this.extractLanguageProperty(targetElement); if (options?.onLanguageChange) { this.onLanguageChange = options.onLanguageChange; @@ -76,31 +96,69 @@ export class Translator { mutations.forEach((mutation) => { if (mutation.attributeName === 'lang') { - this.#resolvedLanguage = this.extractLanguageProperty(mutation.target as Element); - this.#handleLanguageChange?.(this.#resolvedLanguage); + this.currentDocumentLanguage = this.extractLanguageProperty(mutation.target as Element); + this.handleLanguageChange?.(this.resolvedLanguage); } }); }); - languageAttributeObserver.observe(window.frameElement, { attributes: true }); + languageAttributeObserver.observe(targetElement, { attributes: true }); } @InitializedOnly t(key: TranslationKey) { - const value = get(this.#translations, key) as { [key: string]: string } | string | undefined; + const value = get(this.translations, key) as { [key: string]: string } | string | undefined; if (typeof value === 'string') { return value; } - return value?.[this.resolvedLanguage] ?? value?.[this.#fallbackLanguage] ?? key; + return value?.[this.resolvedLanguage] ?? value?.[this.fallbackLanguage] ?? key; + } +} + +/** @public */ +export class SynchronizedTranslator extends BaseTranslator { + constructor(options: TranslatorOptions) { + super(options); + } + + @InitializedOnly + changeLanguage(language: Language) { + window.top!.document.dispatchEvent(new CustomEvent('changeLanguage', { detail: language })); + } + + override init(options: TranslatorInitOptions = {}) { + if (typeof window === 'undefined') { + throw new Error('Cannot initialize SynchronizedTranslator outside of browser'); + } else if (!window.frameElement) { + throw new Error('Cannot initialize SynchronizedTranslator in context where window.frameElement is null'); + } else if (window.frameElement.getAttribute('name') !== 'interactive-instrument') { + throw new Error('SynchronizedTranslator must be initialized in InstrumentRenderer'); + } + return super.init(options, window.frameElement); } +} +/** @public */ +export class StandaloneTranslator extends BaseTranslator { @InitializedOnly - private extractLanguageProperty(element: Element) { - const lang = element.getAttribute('lang'); - if (lang === 'en' || lang === 'fr') { - return lang; + changeLanguage(language: Language) { + document.documentElement.setAttribute('lang', language); + } + + override init(options: TranslatorInitOptions = {}) { + if (typeof window === 'undefined') { + throw new Error('Cannot initialize StandaloneTranslator outside of browser'); } - console.error(`Unexpected value for 'lang' attribute: '${lang}'`); - return this.#fallbackLanguage; + return super.init(options, document.documentElement); } } + +/** @public */ +let Translator: typeof BaseTranslator; +if (typeof window === 'undefined' || window.self !== window.top) { + Translator = SynchronizedTranslator; +} else { + Translator = StandaloneTranslator; +} + +export { Translator }; diff --git a/runtime/v1/package.json b/runtime/v1/package.json index 0c60412f3..cb3e93099 100644 --- a/runtime/v1/package.json +++ b/runtime/v1/package.json @@ -1,7 +1,7 @@ { "name": "@opendatacapture/runtime-v1", "type": "module", - "version": "1.6.3", + "version": "1.8.0", "author": { "name": "Douglas Neuroinformatics", "email": "support@douglasneuroinformatics.ca"