@@ -2,7 +2,7 @@ import { get } from 'lodash-es';
22
33import 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>
2929export 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 } ;
0 commit comments