@@ -2,10 +2,21 @@ import { get } from 'lodash-es';
22
33import 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 */
920export 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