Skip to content

Commit 09a34f5

Browse files
authored
Merge pull request #1094 from joshunrau/dev
add polymorphic translator
2 parents daff7d4 + abc06ff commit 09a34f5

File tree

3 files changed

+97
-39
lines changed

3 files changed

+97
-39
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "opendatacapture",
33
"type": "module",
4-
"version": "1.8.8",
4+
"version": "1.8.9",
55
"private": true,
66
"packageManager": "[email protected]",
77
"license": "Apache-2.0",

packages/runtime-core/src/i18n.ts

Lines changed: 95 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { get } from 'lodash-es';
22

33
import type { Language } from './types/core.js';
44

5-
function InitializedOnly<T extends Translator, TArgs extends any[], TReturn>(
5+
function InitializedOnly<T extends BaseTranslator, TArgs extends any[], TReturn>(
66
target: (this: T, ...args: TArgs) => TReturn,
77
context: ClassGetterDecoratorContext<T> | ClassMethodDecoratorContext<T> | ClassSetterDecoratorContext<T>
88
) {
@@ -29,45 +29,65 @@ export type TranslationKey<T extends { [key: string]: unknown }, Key = keyof T>
2929
export type LanguageChangeHandler = (this: void, language: Language) => void;
3030

3131
/** @public */
32-
export class Translator<T extends { [key: string]: unknown } = { [key: string]: unknown }> {
33-
isInitialized: boolean;
34-
#fallbackLanguage: Language;
35-
#handleLanguageChange: LanguageChangeHandler | null;
36-
#resolvedLanguage: Language;
37-
#translations: T;
38-
39-
constructor(options: { fallbackLanguage?: Language; translations: T }) {
40-
this.isInitialized = false;
41-
this.#fallbackLanguage = options.fallbackLanguage ?? 'en';
42-
this.#handleLanguageChange = null;
43-
this.#resolvedLanguage = this.#fallbackLanguage;
44-
this.#translations = options.translations;
32+
export type TranslatorOptions<T extends { [key: string]: unknown }> = {
33+
fallbackLanguage?: Language;
34+
translations: T;
35+
};
36+
37+
/** @public */
38+
export type TranslatorInitOptions = {
39+
onLanguageChange?: LanguageChangeHandler | null;
40+
};
41+
42+
/** @public */
43+
export abstract class BaseTranslator<T extends { [key: string]: unknown } = { [key: string]: unknown }> {
44+
protected currentDocumentLanguage: Language | null;
45+
protected fallbackLanguage: Language;
46+
protected handleLanguageChange: LanguageChangeHandler | null;
47+
protected translations: T;
48+
#isInitialized: boolean;
49+
50+
constructor({ fallbackLanguage, translations }: TranslatorOptions<T>) {
51+
this.currentDocumentLanguage = null;
52+
this.fallbackLanguage = fallbackLanguage ?? 'en';
53+
this.handleLanguageChange = null;
54+
this.#isInitialized = false;
55+
this.translations = translations;
56+
}
57+
58+
get isInitialized() {
59+
return this.#isInitialized;
60+
}
61+
62+
protected set isInitialized(value: boolean) {
63+
this.#isInitialized = value;
4564
}
4665

4766
@InitializedOnly
4867
set onLanguageChange(handler: LanguageChangeHandler) {
49-
this.#handleLanguageChange = handler;
68+
this.handleLanguageChange = handler;
5069
}
5170

5271
@InitializedOnly
5372
get resolvedLanguage() {
54-
return this.#resolvedLanguage;
73+
return this.currentDocumentLanguage ?? this.fallbackLanguage;
5574
}
5675

57-
@InitializedOnly
58-
changeLanguage(language: Language) {
59-
window.top!.document.dispatchEvent(new CustomEvent('changeLanguage', { detail: language }));
60-
}
76+
abstract changeLanguage(language: Language): void;
6177

62-
init(options?: { onLanguageChange?: LanguageChangeHandler | null }) {
63-
if (typeof window === 'undefined') {
64-
throw new Error('Cannot initialize Translator outside of browser');
65-
} else if (!window.frameElement) {
66-
throw new Error('Cannot initialize Translator in context where window.frameElement is null');
78+
@InitializedOnly
79+
protected extractLanguageProperty(element: Element) {
80+
const lang = element.getAttribute('lang');
81+
if (lang === 'en' || lang === 'fr') {
82+
return lang;
6783
}
84+
console.error(`Unexpected value for 'lang' attribute: '${lang}'`);
85+
return null;
86+
}
6887

88+
init(options: TranslatorInitOptions, targetElement: Element) {
6989
this.isInitialized = true;
70-
this.#resolvedLanguage = this.extractLanguageProperty(window.frameElement);
90+
this.currentDocumentLanguage = this.extractLanguageProperty(targetElement);
7191

7292
if (options?.onLanguageChange) {
7393
this.onLanguageChange = options.onLanguageChange;
@@ -76,31 +96,69 @@ export class Translator<T extends { [key: string]: unknown } = { [key: string]:
7696
const languageAttributeObserver = new MutationObserver((mutations) => {
7797
mutations.forEach((mutation) => {
7898
if (mutation.attributeName === 'lang') {
79-
this.#resolvedLanguage = this.extractLanguageProperty(mutation.target as Element);
80-
this.#handleLanguageChange?.(this.#resolvedLanguage);
99+
this.currentDocumentLanguage = this.extractLanguageProperty(mutation.target as Element);
100+
this.handleLanguageChange?.(this.resolvedLanguage);
81101
}
82102
});
83103
});
84104

85-
languageAttributeObserver.observe(window.frameElement, { attributes: true });
105+
languageAttributeObserver.observe(targetElement, { attributes: true });
86106
}
87107

88108
@InitializedOnly
89109
t(key: TranslationKey<T>) {
90-
const value = get(this.#translations, key) as { [key: string]: string } | string | undefined;
110+
const value = get(this.translations, key) as { [key: string]: string } | string | undefined;
91111
if (typeof value === 'string') {
92112
return value;
93113
}
94-
return value?.[this.resolvedLanguage] ?? value?.[this.#fallbackLanguage] ?? key;
114+
return value?.[this.resolvedLanguage] ?? value?.[this.fallbackLanguage] ?? key;
115+
}
116+
}
117+
118+
/** @public */
119+
export class SynchronizedTranslator<T extends { [key: string]: unknown }> extends BaseTranslator<T> {
120+
constructor(options: TranslatorOptions<T>) {
121+
super(options);
122+
}
123+
124+
@InitializedOnly
125+
changeLanguage(language: Language) {
126+
window.top!.document.dispatchEvent(new CustomEvent('changeLanguage', { detail: language }));
127+
}
128+
129+
override init(options: TranslatorInitOptions = {}) {
130+
if (typeof window === 'undefined') {
131+
throw new Error('Cannot initialize SynchronizedTranslator outside of browser');
132+
} else if (!window.frameElement) {
133+
throw new Error('Cannot initialize SynchronizedTranslator in context where window.frameElement is null');
134+
} else if (window.frameElement.getAttribute('name') !== 'interactive-instrument') {
135+
throw new Error('SynchronizedTranslator must be initialized in InstrumentRenderer');
136+
}
137+
return super.init(options, window.frameElement);
95138
}
139+
}
96140

141+
/** @public */
142+
export class StandaloneTranslator<T extends { [key: string]: unknown }> extends BaseTranslator<T> {
97143
@InitializedOnly
98-
private extractLanguageProperty(element: Element) {
99-
const lang = element.getAttribute('lang');
100-
if (lang === 'en' || lang === 'fr') {
101-
return lang;
144+
changeLanguage(language: Language) {
145+
document.documentElement.setAttribute('lang', language);
146+
}
147+
148+
override init(options: TranslatorInitOptions = {}) {
149+
if (typeof window === 'undefined') {
150+
throw new Error('Cannot initialize StandaloneTranslator outside of browser');
102151
}
103-
console.error(`Unexpected value for 'lang' attribute: '${lang}'`);
104-
return this.#fallbackLanguage;
152+
return super.init(options, document.documentElement);
105153
}
106154
}
155+
156+
/** @public */
157+
let Translator: typeof BaseTranslator;
158+
if (typeof window === 'undefined' || window.self !== window.top) {
159+
Translator = SynchronizedTranslator;
160+
} else {
161+
Translator = StandaloneTranslator;
162+
}
163+
164+
export { Translator };

runtime/v1/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@opendatacapture/runtime-v1",
33
"type": "module",
4-
"version": "1.6.3",
4+
"version": "1.8.0",
55
"author": {
66
"name": "Douglas Neuroinformatics",
77
"email": "[email protected]"

0 commit comments

Comments
 (0)