claude-code-runner 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.zh-Hans.md +2 -2
- package/package.json +17 -17
- package/public/app.js +1134 -0
- package/public/i18n.js +247 -0
- package/public/index.html +660 -0
- package/public/locales/README.md +86 -0
- package/public/locales/en.json +60 -0
- package/public/locales/zh-CN.json +60 -0
package/public/i18n.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internationalization (i18n) module for Claude Code Runner
|
|
3
|
+
*
|
|
4
|
+
* To add a new language:
|
|
5
|
+
* 1. Create a new JSON file in /public/locales/ (e.g., "fr.json" for French)
|
|
6
|
+
* 2. Copy the structure from en.json and translate all values
|
|
7
|
+
* 3. The language will be automatically available in the language selector
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const I18n = (function () {
|
|
11
|
+
const STORAGE_KEY = 'claude-code-runner-lang';
|
|
12
|
+
const DEFAULT_LOCALE = 'en';
|
|
13
|
+
const SUPPORTED_LOCALES = ['en', 'zh-CN'];
|
|
14
|
+
|
|
15
|
+
let currentLocale = DEFAULT_LOCALE;
|
|
16
|
+
let translations = {};
|
|
17
|
+
const loadedLocales = {};
|
|
18
|
+
let isInitialized = false;
|
|
19
|
+
let initPromise = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get cookie value by name
|
|
23
|
+
*/
|
|
24
|
+
function getCookie(name) {
|
|
25
|
+
const value = `; ${document.cookie}`;
|
|
26
|
+
const parts = value.split(`; ${name}=`);
|
|
27
|
+
if (parts.length === 2) {
|
|
28
|
+
return parts.pop().split(';').shift();
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Set cookie with expiry
|
|
35
|
+
*/
|
|
36
|
+
function setCookie(name, value, days = 365) {
|
|
37
|
+
const date = new Date();
|
|
38
|
+
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
39
|
+
const expires = `expires=${date.toUTCString()}`;
|
|
40
|
+
document.cookie = `${name}=${value};${expires};path=/;SameSite=Lax`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Detect user's preferred language from browser settings
|
|
45
|
+
*/
|
|
46
|
+
function detectBrowserLanguage() {
|
|
47
|
+
const browserLangs = navigator.languages || [navigator.language || navigator.userLanguage];
|
|
48
|
+
|
|
49
|
+
for (const lang of browserLangs) {
|
|
50
|
+
// Check exact match first
|
|
51
|
+
if (SUPPORTED_LOCALES.includes(lang)) {
|
|
52
|
+
return lang;
|
|
53
|
+
}
|
|
54
|
+
// Check language code without region (e.g., 'zh' from 'zh-TW')
|
|
55
|
+
const baseLang = lang.split('-')[0];
|
|
56
|
+
// Map common variants
|
|
57
|
+
if (baseLang === 'zh') {
|
|
58
|
+
// For Chinese, default to Simplified Chinese
|
|
59
|
+
return 'zh-CN';
|
|
60
|
+
}
|
|
61
|
+
if (SUPPORTED_LOCALES.includes(baseLang)) {
|
|
62
|
+
return baseLang;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return DEFAULT_LOCALE;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load a locale's translations
|
|
71
|
+
*/
|
|
72
|
+
async function loadLocale(locale) {
|
|
73
|
+
if (loadedLocales[locale]) {
|
|
74
|
+
return loadedLocales[locale];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`/locales/${locale}.json`);
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Failed to load locale: ${locale}`);
|
|
81
|
+
}
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
loadedLocales[locale] = data;
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error(`Error loading locale ${locale}:`, error);
|
|
88
|
+
// Fallback to English if available
|
|
89
|
+
if (locale !== DEFAULT_LOCALE && loadedLocales[DEFAULT_LOCALE]) {
|
|
90
|
+
return loadedLocales[DEFAULT_LOCALE];
|
|
91
|
+
}
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get nested value from object by dot-notation path
|
|
98
|
+
*/
|
|
99
|
+
function getNestedValue(obj, path) {
|
|
100
|
+
return path.split('.').reduce((current, key) => {
|
|
101
|
+
return current && current[key] !== undefined ? current[key] : null;
|
|
102
|
+
}, obj);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Translate a key
|
|
107
|
+
* @param {string} key - Dot-notation key (e.g., 'header.connecting')
|
|
108
|
+
* @param {object} params - Optional parameters for interpolation
|
|
109
|
+
* @returns {string} Translated string or key if not found
|
|
110
|
+
*/
|
|
111
|
+
function t(key, params = {}) {
|
|
112
|
+
let value = getNestedValue(translations, key);
|
|
113
|
+
|
|
114
|
+
if (value === null) {
|
|
115
|
+
console.warn(`Translation missing for key: ${key}`);
|
|
116
|
+
return key;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Simple interpolation: replace {{param}} with params.param
|
|
120
|
+
if (typeof value === 'string' && Object.keys(params).length > 0) {
|
|
121
|
+
Object.keys(params).forEach((param) => {
|
|
122
|
+
value = value.replace(new RegExp(`{{${param}}}`, 'g'), params[param]);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Update all elements with data-i18n attribute
|
|
131
|
+
*/
|
|
132
|
+
function updateDOM() {
|
|
133
|
+
// Update elements with data-i18n attribute (text content)
|
|
134
|
+
document.querySelectorAll('[data-i18n]').forEach((element) => {
|
|
135
|
+
const key = element.getAttribute('data-i18n');
|
|
136
|
+
const translated = t(key);
|
|
137
|
+
if (translated !== key) {
|
|
138
|
+
element.textContent = translated;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Update elements with data-i18n-placeholder attribute
|
|
143
|
+
document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => {
|
|
144
|
+
const key = element.getAttribute('data-i18n-placeholder');
|
|
145
|
+
const translated = t(key);
|
|
146
|
+
if (translated !== key) {
|
|
147
|
+
element.placeholder = translated;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Update elements with data-i18n-title attribute
|
|
152
|
+
document.querySelectorAll('[data-i18n-title]').forEach((element) => {
|
|
153
|
+
const key = element.getAttribute('data-i18n-title');
|
|
154
|
+
const translated = t(key);
|
|
155
|
+
if (translated !== key) {
|
|
156
|
+
element.title = translated;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Update document title
|
|
161
|
+
document.title = t('app.title');
|
|
162
|
+
|
|
163
|
+
// Update language selector current value
|
|
164
|
+
const langSelector = document.getElementById('language-selector');
|
|
165
|
+
if (langSelector) {
|
|
166
|
+
langSelector.value = currentLocale;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set the current locale
|
|
172
|
+
*/
|
|
173
|
+
async function setLocale(locale) {
|
|
174
|
+
if (!SUPPORTED_LOCALES.includes(locale)) {
|
|
175
|
+
console.warn(`Locale ${locale} is not supported, falling back to ${DEFAULT_LOCALE}`);
|
|
176
|
+
locale = DEFAULT_LOCALE;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
translations = await loadLocale(locale);
|
|
180
|
+
currentLocale = locale;
|
|
181
|
+
setCookie(STORAGE_KEY, locale);
|
|
182
|
+
document.documentElement.lang = locale;
|
|
183
|
+
|
|
184
|
+
updateDOM();
|
|
185
|
+
|
|
186
|
+
// Dispatch event for components that need to react to language changes
|
|
187
|
+
window.dispatchEvent(new CustomEvent('languagechange', { detail: { locale } }));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Initialize i18n system
|
|
192
|
+
*/
|
|
193
|
+
async function init() {
|
|
194
|
+
if (initPromise) {
|
|
195
|
+
return initPromise;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
initPromise = (async () => {
|
|
199
|
+
// Determine initial locale
|
|
200
|
+
const savedLocale = getCookie(STORAGE_KEY);
|
|
201
|
+
const browserLocale = detectBrowserLanguage();
|
|
202
|
+
const initialLocale = savedLocale || browserLocale;
|
|
203
|
+
|
|
204
|
+
// Pre-load default locale as fallback
|
|
205
|
+
await loadLocale(DEFAULT_LOCALE);
|
|
206
|
+
|
|
207
|
+
// Load and set the initial locale
|
|
208
|
+
await setLocale(initialLocale);
|
|
209
|
+
|
|
210
|
+
isInitialized = true;
|
|
211
|
+
})();
|
|
212
|
+
|
|
213
|
+
return initPromise;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get current locale
|
|
218
|
+
*/
|
|
219
|
+
function getLocale() {
|
|
220
|
+
return currentLocale;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get list of supported locales
|
|
225
|
+
*/
|
|
226
|
+
function getSupportedLocales() {
|
|
227
|
+
return [...SUPPORTED_LOCALES];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check if i18n is initialized
|
|
232
|
+
*/
|
|
233
|
+
function ready() {
|
|
234
|
+
return isInitialized;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Public API
|
|
238
|
+
return {
|
|
239
|
+
init,
|
|
240
|
+
t,
|
|
241
|
+
setLocale,
|
|
242
|
+
getLocale,
|
|
243
|
+
getSupportedLocales,
|
|
244
|
+
updateDOM,
|
|
245
|
+
ready,
|
|
246
|
+
};
|
|
247
|
+
})();
|