77import i18n , { t } from 'i18next' ;
88import { initReactI18next } from 'react-i18next' ;
99import fs from 'node:fs/promises' ;
10+ import fsSync from 'node:fs' ;
1011import path from 'node:path' ;
1112import { fileURLToPath } from 'node:url' ;
1213
@@ -23,21 +24,116 @@ async function loadTranslationFile(
2324 const filePath = path . join ( __dirname , 'locales' , lang , `${ namespace } .json` ) ;
2425 const content = await fs . readFile ( filePath , 'utf8' ) ;
2526 return JSON . parse ( content ) as Record < string , unknown > ;
26- } catch ( error ) {
27- console . warn (
28- `Failed to load translation file for ${ lang } /${ namespace } :` ,
29- error ,
30- ) ;
27+ } catch ( _error ) {
28+ // Silently fail if file doesn't exist - i18next handles missing keys
3129 return { } ;
3230 }
3331}
3432
35- // Phase 1: Only English locale, core namespaces
36- const languages = [ 'en' ] ;
3733const namespaces = [ 'common' , 'help' , 'dialogs' ] ;
34+ const localesDir = path . join ( __dirname , 'locales' ) ;
3835
39- // Async resource loading
40- async function loadResources ( ) {
36+ // Language display names for settings UI
37+ const LANGUAGE_LABELS : Record < string , string > = {
38+ en : 'English' ,
39+ ja : '日本語' ,
40+ // Add more language labels here as locales are added
41+ } ;
42+
43+ /**
44+ * Synchronously scan the locales directory to find available language packs.
45+ * This runs at module load time so the schema can access the options.
46+ */
47+ function detectAvailableLanguagesSync ( ) : string [ ] {
48+ try {
49+ const entries = fsSync . readdirSync ( localesDir , { withFileTypes : true } ) ;
50+ const langs = entries
51+ . filter ( ( entry ) => entry . isDirectory ( ) )
52+ . map ( ( entry ) => entry . name )
53+ . filter ( ( name ) => ! name . startsWith ( '.' ) ) ; // Exclude hidden directories
54+
55+ // Ensure 'en' is always first (default/fallback language)
56+ if ( langs . includes ( 'en' ) ) {
57+ return [ 'en' , ...langs . filter ( ( l ) => l !== 'en' ) ] ;
58+ }
59+ return langs . length > 0 ? langs : [ 'en' ] ;
60+ } catch {
61+ // If we can't read the directory, fall back to English only
62+ return [ 'en' ] ;
63+ }
64+ }
65+
66+ // Detect available languages synchronously at module load
67+ const availableLanguages : string [ ] = detectAvailableLanguagesSync ( ) ;
68+
69+ /**
70+ * Get the list of available languages (detected from locale folders).
71+ */
72+ export function getAvailableLanguages ( ) : string [ ] {
73+ return [ ...availableLanguages ] ;
74+ }
75+
76+ /**
77+ * Check if a language code is available (has a locale pack).
78+ */
79+ export function isLanguageAvailable ( lang : string ) : boolean {
80+ return availableLanguages . includes ( lang ) ;
81+ }
82+
83+ /**
84+ * Get language options formatted for the settings schema.
85+ * Returns an array with 'auto' option followed by available languages.
86+ */
87+ export function getLanguageOptions ( ) : Array < { value : string ; label : string } > {
88+ const options : Array < { value : string ; label : string } > = [
89+ { value : 'auto' , label : 'Auto' } ,
90+ ] ;
91+
92+ for ( const lang of availableLanguages ) {
93+ options . push ( {
94+ value : lang ,
95+ label : LANGUAGE_LABELS [ lang ] ?? lang . toUpperCase ( ) ,
96+ } ) ;
97+ }
98+
99+ return options ;
100+ }
101+
102+ /**
103+ * Detect the system language from environment variables or Intl API.
104+ * Priority: GEMINI_LANG > LANG > Intl > 'en' (fallback)
105+ * Only returns languages that have available locale packs.
106+ */
107+ function getSystemLanguage ( ) : string {
108+ const checkLang = ( locale : string | undefined ) : string | null => {
109+ if ( ! locale ) return null ;
110+ const lang = locale . split ( / [ - _ ] / ) [ 0 ] ?. toLowerCase ( ) ;
111+ return lang && availableLanguages . includes ( lang ) ? lang : null ;
112+ } ;
113+
114+ // 1. Check GEMINI_LANG environment variable (explicit override)
115+ const fromGeminiLang = checkLang ( process . env [ 'GEMINI_LANG' ] ) ;
116+ if ( fromGeminiLang ) return fromGeminiLang ;
117+
118+ // 2. Check LANG environment variable (Unix-style locale)
119+ const fromLangEnv = checkLang ( process . env [ 'LANG' ] ) ;
120+ if ( fromLangEnv ) return fromLangEnv ;
121+
122+ // 3. Check Intl API (browser/Node.js locale detection)
123+ try {
124+ const intlLocale = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . locale ;
125+ const fromIntl = checkLang ( intlLocale ) ;
126+ if ( fromIntl ) return fromIntl ;
127+ } catch {
128+ // Intl may not be available in some environments
129+ }
130+
131+ // 4. Fallback to English
132+ return 'en' ;
133+ }
134+
135+ // Async resource loading - only loads for available languages
136+ async function loadResources ( languages : string [ ] ) {
41137 const resources : Record < string , Record < string , Record < string , unknown > > > = { } ;
42138
43139 for ( const lang of languages ) {
@@ -58,12 +154,16 @@ async function loadResources() {
58154
59155// Initialize i18next with async resource loading
60156async function initializeI18n ( ) {
61- const resources = await loadResources ( ) ;
157+ // Load resources only for available languages (detected at module load)
158+ const resources = await loadResources ( availableLanguages ) ;
159+
160+ // Determine the language to use (auto-detect or fallback)
161+ const detectedLanguage = getSystemLanguage ( ) ;
62162
63163 // eslint-disable-next-line import/no-named-as-default-member
64164 await i18n . use ( initReactI18next ) . init ( {
65165 resources,
66- lng : 'en' ,
166+ lng : detectedLanguage ,
67167 fallbackLng : 'en' ,
68168
69169 interpolation : {
0 commit comments