1212// See the License for the specific language governing permissions and
1313// limitations under the License.
1414
15- import { default as i18n , InitOptions } from "i18next" ;
16- import LanguageDetector , {
17- DetectorOptions ,
18- } from "i18next-browser-languagedetector" ;
19- import I18NextHttpBackend , { HttpBackendOptions } from "i18next-http-backend" ;
15+ import {
16+ default as i18n ,
17+ InitOptions ,
18+ LanguageDetectorModule ,
19+ BackendModule ,
20+ ReadCallback ,
21+ ResourceKey ,
22+ } from "i18next" ;
2023import { initReactI18next } from "react-i18next" ;
2124
2225// This generates a map of locale names to their URL (based on import.meta.url), which looks like this:
@@ -30,39 +33,71 @@ const locales = import.meta.glob<string>("../locales/*.json", {
3033 eager : true ,
3134} ) ;
3235
33- const getLocaleUrl = ( name : string ) : string =>
36+ const getLocaleUrl = ( name : string ) : string | undefined =>
3437 locales [ `../locales/${ name } .json` ] ;
3538
3639const supportedLngs = Object . keys ( locales ) . map (
3740 ( url ) => url . match ( / \/ ( [ ^ / ] + ) \. j s o n $ / ) ! [ 1 ] ,
3841) ;
3942
43+ // A simple language detector that reads the `lang` attribute from the HTML tag
44+ const LanguageDetector = {
45+ type : "languageDetector" ,
46+
47+ detect ( ) : string | undefined {
48+ const htmlTag =
49+ typeof document !== "undefined" ? document . documentElement : null ;
50+
51+ if ( htmlTag && typeof htmlTag . getAttribute === "function" ) {
52+ return htmlTag . getAttribute ( "lang" ) || undefined ;
53+ }
54+ } ,
55+ } satisfies LanguageDetectorModule ;
56+
57+ // A backend that fetches the locale files from the URLs generated by the glob above
58+ const Backend = {
59+ type : "backend" ,
60+ init ( ) : void { } ,
61+ read ( language : string , _namespace : string , callback : ReadCallback ) : void {
62+ ( async function ( ) : Promise < ResourceKey > {
63+ const url = getLocaleUrl ( language ) ;
64+ if ( ! url ) {
65+ throw new Error ( `Locale ${ language } not found` ) ;
66+ }
67+
68+ const response = await fetch ( url , {
69+ credentials : "omit" ,
70+ headers : {
71+ Accept : "application/json" ,
72+ } ,
73+ } ) ;
74+
75+ if ( ! response . ok ) {
76+ throw Error ( `Failed to fetch ${ url } ` ) ;
77+ }
78+
79+ // XXX: we don't check the JSON shape here, which should be fine
80+ return await response . json ( ) ;
81+ } ) ( ) . then (
82+ ( data ) => callback ( null , data ) ,
83+ ( error ) => callback ( error , null ) ,
84+ ) ;
85+ } ,
86+ } satisfies BackendModule ;
87+
4088i18n
41- . use ( I18NextHttpBackend )
89+ . use ( Backend )
4290 . use ( LanguageDetector )
4391 . use ( initReactI18next )
4492 . init ( {
4593 fallbackLng : "en" ,
4694 keySeparator : "." ,
4795 pluralSeparator : ":" ,
4896 supportedLngs,
49- detection : {
50- // This lets the backend fully decide the language to use
51- order : [ "htmlTag" ] ,
52- } satisfies DetectorOptions ,
5397 interpolation : {
5498 escapeValue : false , // React has built-in XSS protections
5599 } ,
56- backend : {
57- crossDomain : true ,
58- loadPath ( lngs : string [ ] , _ns : string [ ] ) : string {
59- return getLocaleUrl ( lngs [ 0 ] ) ;
60- } ,
61- requestOptions : {
62- credentials : "same-origin" ,
63- } ,
64- } ,
65- } satisfies InitOptions < HttpBackendOptions > ) ;
100+ } satisfies InitOptions ) ;
66101
67102import . meta. hot ?. on ( "locales-update" , ( ) => {
68103 i18n . reloadResources ( ) . then ( ( ) => {
0 commit comments