Skip to content

Commit 4f0c969

Browse files
committed
feat(i18n): implement core infrastructure and CLI entrypoint
1 parent 51d8afa commit 4f0c969

7 files changed

Lines changed: 306 additions & 105 deletions

File tree

package-lock.json

Lines changed: 80 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/src/gemini.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
// Initialize i18n before any React components
8+
import './i18n/index.js';
9+
710
import React from 'react';
811
import { render } from 'ink';
912
import { AppContainer } from './ui/AppContainer.js';

packages/cli/src/i18n/index.ts

Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import i18n, { t } from 'i18next';
88
import { initReactI18next } from 'react-i18next';
99
import fs from 'node:fs/promises';
10+
import fsSync from 'node:fs';
1011
import path from 'node:path';
1112
import { 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'];
3733
const 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
60156
async 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: {

packages/cli/src/i18n/useTranslation.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)