@@ -2,10 +2,21 @@ import { get } from 'lodash-es';
2
2
3
3
import type { Language } from './types/core.js' ;
4
4
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
+ }
7
18
8
- /** @alpha */
19
+ /** @public */
9
20
export type TranslationKey < T extends { [ key : string ] : unknown } , Key = keyof T > = Key extends string
10
21
? T [ Key ] extends { [ key : string ] : unknown }
11
22
? T [ Key ] extends { [ K in Language ] : string }
@@ -14,67 +25,82 @@ export type TranslationKey<T extends { [key: string]: unknown }, Key = keyof T>
14
25
: `${Key } `
15
26
: never ;
16
27
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 } ) ) ;
38
60
}
39
61
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 ;
44
74
}
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
+ } ) ;
56
83
} ) ;
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 ;
78
102
}
79
- } ;
103
+ console . error ( `Unexpected value for 'lang' attribute: '${ lang } '` ) ;
104
+ return this . #fallbackLanguage;
105
+ }
80
106
}
0 commit comments