Skip to content

Commit b8e681e

Browse files
committed
feat: new implementation for i18n
1 parent 019790c commit b8e681e

File tree

2 files changed

+89
-62
lines changed

2 files changed

+89
-62
lines changed

packages/instrument-renderer/src/components/InteractiveContent/InteractiveContent.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const InteractiveContent = React.memo<InteractiveContentProps>(function I
1818
bundle,
1919
onSubmit
2020
}) {
21-
const { changeLanguage } = useTranslation();
21+
const { changeLanguage, resolvedLanguage } = useTranslation();
2222
const [_, updateTheme] = useTheme();
2323
const [scale, setScale] = useState(100);
2424
const iFrameRef = useRef<HTMLIFrameElement>(null);
@@ -109,6 +109,7 @@ export const InteractiveContent = React.memo<InteractiveContentProps>(function I
109109
allow="fullscreen"
110110
className="origin-top-left"
111111
data-bundle={bundle}
112+
lang={resolvedLanguage}
112113
name="interactive-instrument"
113114
ref={iFrameRef}
114115
srcDoc={`<script type="module">${bootstrapScript}</script>`}

packages/runtime-core/src/i18n.ts

Lines changed: 87 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,21 @@ import { get } from 'lodash-es';
22

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

5-
/** @alpha */
6-
export type LanguageChangeHandler = (this: void, language: Language) => void;
5+
function InitializedOnly<T extends Translator, TArgs extends any[], TReturn>(
6+
target: (this: T, ...args: TArgs) => TReturn,
7+
context: ClassGetterDecoratorContext<T> | ClassMethodDecoratorContext<T> | ClassSetterDecoratorContext<T>
8+
) {
9+
const name = context.name.toString();
10+
function replacementMethod(this: T, ...args: TArgs): TReturn {
11+
if (!this.isInitialized) {
12+
throw new Error(`Cannot access ${context.kind} '${name}' of Translator before initialization in browser`);
13+
}
14+
return target.call(this, ...args);
15+
}
16+
return replacementMethod;
17+
}
718

8-
/** @alpha */
19+
/** @public */
920
export type TranslationKey<T extends { [key: string]: unknown }, Key = keyof T> = Key extends string
1021
? T[Key] extends { [key: string]: unknown }
1122
? T[Key] extends { [K in Language]: string }
@@ -14,67 +25,82 @@ export type TranslationKey<T extends { [key: string]: unknown }, Key = keyof T>
1425
: `${Key}`
1526
: never;
1627

17-
/** @alpha */
18-
export type I18N<T extends { [key: string]: unknown }> = {
19-
changeLanguage: (language: Language) => void;
20-
readonly resolvedLanguage: Language;
21-
set onLanguageChange(value: LanguageChangeHandler);
22-
readonly t: (key: TranslationKey<T>) => string;
23-
};
24-
25-
/** @alpha */
26-
export function createI18Next<const T extends { [key: string]: unknown }>({
27-
fallbackLanguage = 'en',
28-
translations
29-
}: {
30-
fallbackLanguage?: Language;
31-
translations?: T;
32-
} = {}): I18N<T> {
33-
let resolvedLanguage: Language;
34-
let handleLanguageChange: LanguageChangeHandler | null = null;
35-
36-
if (!window) {
37-
throw new Error('Window is not defined');
28+
/** @public */
29+
export type LanguageChangeHandler = (this: void, language: Language) => void;
30+
31+
/** @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;
45+
}
46+
47+
@InitializedOnly
48+
set onLanguageChange(handler: LanguageChangeHandler) {
49+
this.#handleLanguageChange = handler;
50+
}
51+
52+
@InitializedOnly
53+
get resolvedLanguage() {
54+
return this.#resolvedLanguage;
55+
}
56+
57+
@InitializedOnly
58+
changeLanguage(language: Language) {
59+
window.top!.document.dispatchEvent(new CustomEvent('changeLanguage', { detail: language }));
3860
}
3961

40-
const documentElement = window.top!.document.documentElement;
41-
const extractLanguageProperty = (element: HTMLElement) => {
42-
if (element.lang === 'en' || element.lang === 'fr') {
43-
return element.lang;
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');
67+
}
68+
69+
this.isInitialized = true;
70+
this.#resolvedLanguage = this.extractLanguageProperty(window.frameElement);
71+
72+
if (options?.onLanguageChange) {
73+
this.onLanguageChange = options.onLanguageChange;
4474
}
45-
console.error(`Unexpected value for HTMLElement 'lang' attribute: '${element.lang}'`);
46-
return fallbackLanguage;
47-
};
48-
49-
const languageAttributeObserver = new MutationObserver((mutations) => {
50-
mutations.forEach((mutation) => {
51-
if (mutation.attributeName === 'lang') {
52-
resolvedLanguage = extractLanguageProperty(mutation.target as HTMLElement);
53-
handleLanguageChange?.(resolvedLanguage);
54-
handleLanguageChange?.(resolvedLanguage);
55-
}
75+
76+
const languageAttributeObserver = new MutationObserver((mutations) => {
77+
mutations.forEach((mutation) => {
78+
if (mutation.attributeName === 'lang') {
79+
this.#resolvedLanguage = this.extractLanguageProperty(mutation.target as Element);
80+
this.#handleLanguageChange?.(this.#resolvedLanguage);
81+
}
82+
});
5683
});
57-
});
58-
59-
resolvedLanguage = extractLanguageProperty(documentElement);
60-
languageAttributeObserver.observe(documentElement, { attributes: true });
61-
62-
return {
63-
changeLanguage: (language) => {
64-
window.top!.document.dispatchEvent(new CustomEvent('changeLanguage', { detail: language }));
65-
},
66-
set onLanguageChange(handler: LanguageChangeHandler) {
67-
handleLanguageChange = handler;
68-
},
69-
get resolvedLanguage() {
70-
return resolvedLanguage;
71-
},
72-
t: (key) => {
73-
const value = get(translations, key) as { [key: string]: string } | string | undefined;
74-
if (typeof value === 'string') {
75-
return value;
76-
}
77-
return value?.[resolvedLanguage] ?? value?.[fallbackLanguage] ?? key;
84+
85+
languageAttributeObserver.observe(window.frameElement, { attributes: true });
86+
}
87+
88+
@InitializedOnly
89+
t(key: TranslationKey<T>) {
90+
const value = get(this.#translations, key) as { [key: string]: string } | string | undefined;
91+
if (typeof value === 'string') {
92+
return value;
93+
}
94+
return value?.[this.resolvedLanguage] ?? value?.[this.#fallbackLanguage] ?? key;
95+
}
96+
97+
@InitializedOnly
98+
private extractLanguageProperty(element: Element) {
99+
const lang = element.getAttribute('lang');
100+
if (lang === 'en' || lang === 'fr') {
101+
return lang;
78102
}
79-
};
103+
console.error(`Unexpected value for 'lang' attribute: '${lang}'`);
104+
return this.#fallbackLanguage;
105+
}
80106
}

0 commit comments

Comments
 (0)