From a3feb725aa80f649dd9345e3c32fc4bf2b06c957 Mon Sep 17 00:00:00 2001 From: Nick Earnshaw Date: Sat, 28 Feb 2026 18:39:57 +0000 Subject: [PATCH 1/5] Fix internationalization for date formatting and dynamic content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace hardcoded Chinese date format (YYYY年MM月) with locale-aware formatting - Add getLocaleString() helper to map i18n language codes to proper locale strings - Pass translation strings to client-side JavaScript for dynamic content - Add new translation keys: time, loading, showHourlyDetails, showDailyDetails, clickForHourlyDetails, clickForDailyDetails - Replace all hardcoded Chinese strings in webview templates and JavaScript - Fix toLocaleDateString() calls to use current locale instead of system default Co-Authored-By: Claude Opus 4.5 --- src/i18n.ts | 42 +++++++++++++++++ src/webview.ts | 124 +++++++++++++++++++++++++++++++------------------ 2 files changed, 122 insertions(+), 44 deletions(-) diff --git a/src/i18n.ts b/src/i18n.ts index 7d5878a..4eb01cd 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -28,10 +28,16 @@ export interface Translations { monthlyBreakdown: string; hourlyBreakdown: string; date: string; + time: string; yesterday: string; dataDirectory: string; noDataMessage: string; errorMessage: string; + loading: string; + showHourlyDetails: string; + showDailyDetails: string; + clickForHourlyDetails: string; + clickForDailyDetails: string; }; settings: { title: string; @@ -71,10 +77,16 @@ const translations: Record = { monthlyBreakdown: 'Monthly Usage', hourlyBreakdown: 'Hourly Usage', date: 'Date', + time: 'Time', yesterday: 'Yesterday', dataDirectory: 'Data Directory', noDataMessage: 'No usage data found. Make sure Claude Code is running and configured correctly.', errorMessage: 'Error loading usage data. Please check your configuration.', + loading: 'Loading...', + showHourlyDetails: 'Show hourly details', + showDailyDetails: 'Show daily details', + clickForHourlyDetails: 'Click to view hourly details', + clickForDailyDetails: 'Click to view daily details', }, settings: { title: 'Claude Code Usage Settings', @@ -112,12 +124,18 @@ const translations: Record = { monthlyBreakdown: "Monats-Nutzungsübersicht", hourlyBreakdown: "Stunden-Nutzungsübersicht", date: "Datum", + time: "Zeit", yesterday: "Gestern", dataDirectory: "Daten Pfad", noDataMessage: "Keine Daten gefunden. Stell sicher, dass Claude Code läuft und entsprechend konfiguriert ist.", errorMessage: "Fehler beim laden der Nutzungsdaten. Bitte prüfe deine Konfiguration.", + loading: "Lädt...", + showHourlyDetails: "Stündliche Details anzeigen", + showDailyDetails: "Tägliche Details anzeigen", + clickForHourlyDetails: "Klicken für stündliche Details", + clickForDailyDetails: "Klicken für tägliche Details", }, settings: { title: "Claude Code Nutzungseinstellungen", @@ -155,10 +173,16 @@ const translations: Record = { monthlyBreakdown: '每月使用量', hourlyBreakdown: '每小時使用量', date: '日期', + time: '時間', yesterday: '昨日', dataDirectory: '資料目錄', noDataMessage: '找不到使用資料。請確認 Claude Code 正在執行且設定正確。', errorMessage: '載入使用資料時發生錯誤。請檢查您的設定。', + loading: '載入中...', + showHourlyDetails: '顯示每小時詳細資料', + showDailyDetails: '顯示每日詳細資料', + clickForHourlyDetails: '點擊查看每小時詳情', + clickForDailyDetails: '點擊查看每日詳情', }, settings: { title: 'Claude Code 使用量設定', @@ -196,10 +220,16 @@ const translations: Record = { monthlyBreakdown: '每月使用量', hourlyBreakdown: '每小时使用量', date: '日期', + time: '时间', yesterday: '昨日', dataDirectory: '数据目录', noDataMessage: '找不到使用数据。请确认 Claude Code 正在运行且配置正确。', errorMessage: '加载使用数据时发生错误。请检查您的配置。', + loading: '加载中...', + showHourlyDetails: '显示每小时详细数据', + showDailyDetails: '显示每日详细数据', + clickForHourlyDetails: '点击查看每小时详情', + clickForDailyDetails: '点击查看每日详情', }, settings: { title: 'Claude Code 使用量设置', @@ -237,10 +267,16 @@ const translations: Record = { monthlyBreakdown: '月別使用量', hourlyBreakdown: '時間別使用量', date: '日付', + time: '時刻', yesterday: '昨日', dataDirectory: 'データディレクトリ', noDataMessage: '使用データが見つかりません。Claude Code が実行され、正しく設定されていることを確認してください。', errorMessage: '使用データの読み込み中にエラーが発生しました。設定を確認してください。', + loading: '読み込み中...', + showHourlyDetails: '時間別詳細を表示', + showDailyDetails: '日別詳細を表示', + clickForHourlyDetails: 'クリックして時間別詳細を表示', + clickForDailyDetails: 'クリックして日別詳細を表示', }, settings: { title: 'Claude Code 使用量設定', @@ -278,10 +314,16 @@ const translations: Record = { monthlyBreakdown: '월별 사용량', hourlyBreakdown: '시간별 사용량', date: '날짜', + time: '시간', yesterday: '어제', dataDirectory: '데이터 디렉토리', noDataMessage: '사용 데이터를 찾을 수 없습니다. Claude Code가 실행 중이고 올바르게 구성되었는지 확인하세요.', errorMessage: '사용 데이터를 로드하는 중 오류가 발생했습니다. 구성을 확인하세요.', + loading: '로딩 중...', + showHourlyDetails: '시간별 상세 보기', + showDailyDetails: '일별 상세 보기', + clickForHourlyDetails: '클릭하여 시간별 상세 보기', + clickForDailyDetails: '클릭하여 일별 상세 보기', }, settings: { title: 'Claude Code 사용량 설정', diff --git a/src/webview.ts b/src/webview.ts index c6d9b3e..2f4800c 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -391,7 +391,7 @@ export class UsageWebviewProvider { '' + '' + '' + - '' + + '' + '' + @@ -597,7 +597,7 @@ export class UsageWebviewProvider { @@ -681,7 +681,7 @@ export class UsageWebviewProvider { @@ -735,7 +735,7 @@ export class UsageWebviewProvider { data-cache-creation="${data.totalCacheCreationTokens}" data-cache-read="${data.totalCacheReadTokens}" data-messages="${data.messageCount}" - title="${this.formatDate(date)}: ${I18n.formatCurrency(data.totalCost)} - 點擊查看每小時詳情"> + title="${this.formatDate(date)}: ${I18n.formatCurrency(data.totalCost)} - ${I18n.t.popup.clickForHourlyDetails}">
${this.getShortDate(date)}
@@ -773,7 +773,7 @@ export class UsageWebviewProvider { data-cache-creation="${data.totalCacheCreationTokens}" data-cache-read="${data.totalCacheReadTokens}" data-messages="${data.messageCount}" - title="${this.formatDate(date)}: ${I18n.formatCurrency(data.totalCost)} - 點擊查看每日詳情"> + title="${this.formatDate(date)}: ${I18n.formatCurrency(data.totalCost)} - ${I18n.t.popup.clickForDailyDetails}">
${this.getShortDate(date)}
@@ -835,15 +835,27 @@ export class UsageWebviewProvider { private formatDate(dateString: string): string { const date = new Date(dateString); + const locale = this.getLocaleString(); // Check if this is a month-only date (ends with -01) if (dateString.endsWith('-01')) { - // Format as YYYY年MM月 for monthly data - const year = date.getFullYear(); - const month = date.getMonth() + 1; - return `${year}年${month}月`; + // Format as year and month based on locale + return date.toLocaleDateString(locale, { year: 'numeric', month: 'long' }); } // Standard date formatting for daily data - return date.toLocaleDateString(); + return date.toLocaleDateString(locale); + } + + private getLocaleString(): string { + const lang = I18n.getCurrentLanguage(); + const localeMap: Record = { + 'en': 'en-US', + 'de-DE': 'de-DE', + 'zh-TW': 'zh-TW', + 'zh-CN': 'zh-CN', + 'ja': 'ja-JP', + 'ko': 'ko-KR', + }; + return localeMap[lang] || 'en-US'; } private getStyles(): string { @@ -1273,7 +1285,31 @@ export class UsageWebviewProvider { } private getScript(): string { + // Pass translations to JavaScript for dynamic content + const translations = JSON.stringify({ + hourlyBreakdown: I18n.t.popup.hourlyBreakdown, + dailyBreakdown: I18n.t.popup.dailyBreakdown, + monthlyBreakdown: I18n.t.popup.monthlyBreakdown, + cost: I18n.t.popup.cost, + inputTokens: I18n.t.popup.inputTokens, + outputTokens: I18n.t.popup.outputTokens, + cacheCreation: I18n.t.popup.cacheCreation, + cacheRead: I18n.t.popup.cacheRead, + messages: I18n.t.popup.messages, + date: I18n.t.popup.date, + time: I18n.t.popup.time, + noDataForDay: I18n.t.popup.noDataMessage, + noDataForMonth: I18n.t.popup.noDataMessage, + loading: I18n.t.popup.loading, + showHourlyDetails: I18n.t.popup.showHourlyDetails, + showDailyDetails: I18n.t.popup.showDailyDetails, + }); + const locale = this.getLocaleString(); + return ` +var I18nStrings = ${translations}; +var currentLocale = '${locale}'; + console.log("[DEBUG] === JAVASCRIPT INITIALIZATION START ==="); // Get VSCode API @@ -1783,20 +1819,20 @@ function renderHourlyData(hourlyData, date) { console.log("[DEBUG] renderHourlyData called with data:", hourlyData); if (!hourlyData || hourlyData.length === 0) { - return '
當日無使用資料
'; + return '
' + I18nStrings.noDataForDay + '
'; } let html = '
'; - html += '

' + new Date(date).toLocaleDateString() + ' ' + I18n.t.popup.hourlyBreakdown + '

'; + html += '

' + new Date(date).toLocaleDateString(currentLocale) + ' ' + I18nStrings.hourlyBreakdown + '

'; // Chart tabs html += '
'; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; html += '
'; // Chart container @@ -1811,13 +1847,13 @@ function renderHourlyData(hourlyData, date) { html += '
時間' + I18n.t.popup.time + '' + cost + '${I18n.formatNumber(data.totalCacheReadTokens)} ${I18n.formatNumber(data.messageCount)} -
${I18n.formatNumber(data.totalCacheReadTokens)} ${I18n.formatNumber(data.messageCount)} -
'; html += ''; html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; html += ''; html += ''; html += ''; @@ -1850,20 +1886,20 @@ function renderDailyData(dailyData, monthDate) { console.log("[DEBUG] renderDailyData called with data:", dailyData); if (!dailyData || dailyData.length === 0) { - return '
該月無使用資料
'; + return '
' + I18nStrings.noDataForMonth + '
'; } let html = '
'; - html += '

' + new Date(monthDate).toLocaleDateString('zh-TW', { year: 'numeric', month: 'long' }) + ' 每日使用量

'; + html += '

' + new Date(monthDate).toLocaleDateString(currentLocale, { year: 'numeric', month: 'long' }) + ' ' + I18nStrings.dailyBreakdown + '

'; // Chart tabs html += '
'; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; html += '
'; // Chart container @@ -1878,20 +1914,20 @@ function renderDailyData(dailyData, monthDate) { html += '
時間費用輸入 Token輸出 Token快取建立快取讀取訊息數' + I18nStrings.time + '' + I18nStrings.cost + '' + I18nStrings.inputTokens + '' + I18nStrings.outputTokens + '' + I18nStrings.cacheCreation + '' + I18nStrings.cacheRead + '' + I18nStrings.messages + '
'; html += ''; html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; html += ''; html += ''; html += ''; dailyData.forEach(function(item) { const dateObj = new Date(item.date); - const formattedDate = dateObj.toLocaleDateString('zh-TW', { month: 'numeric', day: 'numeric' }); + const formattedDate = dateObj.toLocaleDateString(currentLocale, { month: 'numeric', day: 'numeric' }); html += ''; html += ''; @@ -1977,7 +2013,7 @@ function renderDailyChart(dailyData, metric) { html += 'data-cache-creation="' + item.data.totalCacheCreationTokens + '" '; html += 'data-cache-read="' + item.data.totalCacheReadTokens + '" '; html += 'data-messages="' + item.data.messageCount + '" '; - html += 'title="' + dateObj.toLocaleDateString('zh-TW') + ': ' + formatValue(value, metric) + '">'; + html += 'title="' + dateObj.toLocaleDateString(currentLocale) + ': ' + formatValue(value, metric) + '">'; html += ''; html += '
' + shortDate + '
'; html += ''; From e07186f5087828c4914b1e54ca8a1ae138ed240c Mon Sep 17 00:00:00 2001 From: Nick Earnshaw Date: Sat, 28 Feb 2026 18:43:12 +0000 Subject: [PATCH 2/5] Fix remaining localization issues - Add missing translation keys: noChartData, noUsageRecords, unknownError - Fix incomplete German translations (error, currentSession) - Fix formatNumber() to respect user's language setting - Replace hardcoded English strings in extension.ts and webview.ts - Consolidate getLocaleString() into I18n class to avoid duplication Co-Authored-By: Claude Opus 4.5 --- src/extension.ts | 4 ++-- src/i18n.ts | 41 +++++++++++++++++++++++++++++++++++++---- src/webview.ts | 18 +++++------------- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index c11ff25..18e82d8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -132,7 +132,7 @@ export class ClaudeCodeUsageExtension { } if (records.length === 0) { - const error = 'No usage records found. Make sure Claude Code is running.'; + const error = I18n.t.popup.noUsageRecords; this.statusBar.updateUsageData(null, error); this.webviewProvider.updateData(null, null, null, null, [], [], [], error, dataDirectory); return; @@ -152,7 +152,7 @@ export class ClaudeCodeUsageExtension { this.webviewProvider.updateData(sessionData, todayData, monthData, allTimeData, dailyDataForMonth, dailyDataForAllTime, hourlyDataForToday, undefined, dataDirectory, records); } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + const errorMessage = error instanceof Error ? error.message : I18n.t.popup.unknownError; console.error('Error refreshing Claude Code usage data:', error); this.statusBar.updateUsageData(null, errorMessage); diff --git a/src/i18n.ts b/src/i18n.ts index 4eb01cd..2b8e2fb 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -38,6 +38,9 @@ export interface Translations { showDailyDetails: string; clickForHourlyDetails: string; clickForDailyDetails: string; + noChartData: string; + noUsageRecords: string; + unknownError: string; }; settings: { title: string; @@ -87,6 +90,9 @@ const translations: Record = { showDailyDetails: 'Show daily details', clickForHourlyDetails: 'Click to view hourly details', clickForDailyDetails: 'Click to view daily details', + noChartData: 'No data available', + noUsageRecords: 'No usage records found. Make sure Claude Code is running.', + unknownError: 'Unknown error occurred', }, settings: { title: 'Claude Code Usage Settings', @@ -101,12 +107,12 @@ const translations: Record = { loading: "Lädt...", noData: "Keine Claude Code Daten", notRunning: "Claude Code nicht erreichbar", - error: "Error", - currentSession: "Session", + error: "Fehler", + currentSession: "Aktuelle Sitzung", }, popup: { title: "Claude Code Nutzung", - currentSession: "Current Session", + currentSession: "Aktuelle Sitzung", today: "Heute", thisMonth: "Diesen Monat", allTime: "Seit Aufzeichnungsbeginn", @@ -136,6 +142,9 @@ const translations: Record = { showDailyDetails: "Tägliche Details anzeigen", clickForHourlyDetails: "Klicken für stündliche Details", clickForDailyDetails: "Klicken für tägliche Details", + noChartData: "Keine Daten verfügbar", + noUsageRecords: "Keine Nutzungsdaten gefunden. Stell sicher, dass Claude Code läuft.", + unknownError: "Unbekannter Fehler aufgetreten", }, settings: { title: "Claude Code Nutzungseinstellungen", @@ -183,6 +192,9 @@ const translations: Record = { showDailyDetails: '顯示每日詳細資料', clickForHourlyDetails: '點擊查看每小時詳情', clickForDailyDetails: '點擊查看每日詳情', + noChartData: '無可用資料', + noUsageRecords: '找不到使用紀錄。請確認 Claude Code 正在執行。', + unknownError: '發生未知錯誤', }, settings: { title: 'Claude Code 使用量設定', @@ -230,6 +242,9 @@ const translations: Record = { showDailyDetails: '显示每日详细数据', clickForHourlyDetails: '点击查看每小时详情', clickForDailyDetails: '点击查看每日详情', + noChartData: '无可用数据', + noUsageRecords: '找不到使用记录。请确认 Claude Code 正在运行。', + unknownError: '发生未知错误', }, settings: { title: 'Claude Code 使用量设置', @@ -277,6 +292,9 @@ const translations: Record = { showDailyDetails: '日別詳細を表示', clickForHourlyDetails: 'クリックして時間別詳細を表示', clickForDailyDetails: 'クリックして日別詳細を表示', + noChartData: 'データがありません', + noUsageRecords: '使用記録が見つかりません。Claude Code が実行されていることを確認してください。', + unknownError: '不明なエラーが発生しました', }, settings: { title: 'Claude Code 使用量設定', @@ -324,6 +342,9 @@ const translations: Record = { showDailyDetails: '일별 상세 보기', clickForHourlyDetails: '클릭하여 시간별 상세 보기', clickForDailyDetails: '클릭하여 일별 상세 보기', + noChartData: '데이터 없음', + noUsageRecords: '사용 기록을 찾을 수 없습니다. Claude Code가 실행 중인지 확인하세요.', + unknownError: '알 수 없는 오류가 발생했습니다', }, settings: { title: 'Claude Code 사용량 설정', @@ -375,6 +396,18 @@ export class I18n { } static formatNumber(num: number): string { - return num.toLocaleString(); + return num.toLocaleString(this.getLocaleString()); + } + + static getLocaleString(): string { + const localeMap: Record = { + 'en': 'en-US', + 'de-DE': 'de-DE', + 'zh-TW': 'zh-TW', + 'zh-CN': 'zh-CN', + 'ja': 'ja-JP', + 'ko': 'ko-KR', + }; + return localeMap[this.currentLanguage] || 'en-US'; } } diff --git a/src/webview.ts b/src/webview.ts index 2f4800c..066efef 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -710,7 +710,7 @@ export class UsageWebviewProvider { private renderDailyChart(): string { if (this.dailyDataForMonth.length === 0) { - return '
No data available
'; + return '
' + I18n.t.popup.noChartData + '
'; } // Sort data by date (oldest first for chart display) @@ -748,7 +748,7 @@ export class UsageWebviewProvider { private renderAllTimeChart(): string { if (this.dailyDataForAllTime.length === 0) { - return '
No data available
'; + return '
' + I18n.t.popup.noChartData + '
'; } // Sort data by date (oldest first for chart display) @@ -786,7 +786,7 @@ export class UsageWebviewProvider { private renderHourlyChart(): string { if (this.hourlyDataForToday.length === 0) { - return '
No data available
'; + return '
' + I18n.t.popup.noChartData + '
'; } // Sort data by hour (chronological order) @@ -846,16 +846,7 @@ export class UsageWebviewProvider { } private getLocaleString(): string { - const lang = I18n.getCurrentLanguage(); - const localeMap: Record = { - 'en': 'en-US', - 'de-DE': 'de-DE', - 'zh-TW': 'zh-TW', - 'zh-CN': 'zh-CN', - 'ja': 'ja-JP', - 'ko': 'ko-KR', - }; - return localeMap[lang] || 'en-US'; + return I18n.getLocaleString(); } private getStyles(): string { @@ -1303,6 +1294,7 @@ export class UsageWebviewProvider { loading: I18n.t.popup.loading, showHourlyDetails: I18n.t.popup.showHourlyDetails, showDailyDetails: I18n.t.popup.showDailyDetails, + noChartData: I18n.t.popup.noChartData, }); const locale = this.getLocaleString(); From c7238d829b909675617131c1310b077bacfae0db Mon Sep 17 00:00:00 2001 From: Nick Earnshaw Date: Sat, 28 Feb 2026 18:45:43 +0000 Subject: [PATCH 3/5] Fix all remaining locale-aware formatting issues - Add clickForDetails translation key for status bar tooltip - Fix all JavaScript toLocaleString() calls to use currentLocale - Fix toLocaleDateString() in chart tooltip to use currentLocale - Ensure number formatting in dynamic tables respects user's language Co-Authored-By: Claude Opus 4.5 --- src/i18n.ts | 7 +++++++ src/statusBar.ts | 2 +- src/webview.ts | 24 ++++++++++++------------ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/i18n.ts b/src/i18n.ts index 2b8e2fb..df1f2c7 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -41,6 +41,7 @@ export interface Translations { noChartData: string; noUsageRecords: string; unknownError: string; + clickForDetails: string; }; settings: { title: string; @@ -93,6 +94,7 @@ const translations: Record = { noChartData: 'No data available', noUsageRecords: 'No usage records found. Make sure Claude Code is running.', unknownError: 'Unknown error occurred', + clickForDetails: 'Click for detailed breakdown', }, settings: { title: 'Claude Code Usage Settings', @@ -145,6 +147,7 @@ const translations: Record = { noChartData: "Keine Daten verfügbar", noUsageRecords: "Keine Nutzungsdaten gefunden. Stell sicher, dass Claude Code läuft.", unknownError: "Unbekannter Fehler aufgetreten", + clickForDetails: "Klicken für detaillierte Aufschlüsselung", }, settings: { title: "Claude Code Nutzungseinstellungen", @@ -195,6 +198,7 @@ const translations: Record = { noChartData: '無可用資料', noUsageRecords: '找不到使用紀錄。請確認 Claude Code 正在執行。', unknownError: '發生未知錯誤', + clickForDetails: '點擊查看詳細資訊', }, settings: { title: 'Claude Code 使用量設定', @@ -245,6 +249,7 @@ const translations: Record = { noChartData: '无可用数据', noUsageRecords: '找不到使用记录。请确认 Claude Code 正在运行。', unknownError: '发生未知错误', + clickForDetails: '点击查看详细信息', }, settings: { title: 'Claude Code 使用量设置', @@ -295,6 +300,7 @@ const translations: Record = { noChartData: 'データがありません', noUsageRecords: '使用記録が見つかりません。Claude Code が実行されていることを確認してください。', unknownError: '不明なエラーが発生しました', + clickForDetails: 'クリックして詳細を表示', }, settings: { title: 'Claude Code 使用量設定', @@ -345,6 +351,7 @@ const translations: Record = { noChartData: '데이터 없음', noUsageRecords: '사용 기록을 찾을 수 없습니다. Claude Code가 실행 중인지 확인하세요.', unknownError: '알 수 없는 오류가 발생했습니다', + clickForDetails: '클릭하여 상세 정보 보기', }, settings: { title: 'Claude Code 사용량 설정', diff --git a/src/statusBar.ts b/src/statusBar.ts index 608d476..162fdec 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -74,7 +74,7 @@ export class StatusBarManager { `${I18n.t.popup.outputTokens}: ${I18n.formatNumber(todayData.totalOutputTokens)}`, `${I18n.t.popup.messages}: ${I18n.formatNumber(todayData.messageCount)}`, '', - 'Click for detailed breakdown' + I18n.t.popup.clickForDetails ]; return lines.join('\n'); diff --git a/src/webview.ts b/src/webview.ts index 066efef..1f6a3e9 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -1770,7 +1770,7 @@ function updateMainChart(metric, container) { bar.title = hour + ': ' + formattedValue; } else if (date) { const dateObj = new Date(date); - bar.title = dateObj.toLocaleDateString() + ': ' + formattedValue; + bar.title = dateObj.toLocaleDateString(currentLocale) + ': ' + formattedValue; } }); } @@ -1803,7 +1803,7 @@ function formatValue(value, metric) { if (metric === 'cost') { return '$' + value.toFixed(2); } else { - return value.toLocaleString(); + return value.toLocaleString(currentLocale); } } @@ -1854,11 +1854,11 @@ function renderHourlyData(hourlyData, date) { html += '
'; html += ''; html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; html += ''; }); @@ -1924,11 +1924,11 @@ function renderDailyData(dailyData, monthDate) { html += ''; html += ''; html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; html += ''; }); From 3e5a5b40755e3c93815f73fd28d52dfc448c3b4b Mon Sep 17 00:00:00 2001 From: Nick Earnshaw Date: Sat, 28 Feb 2026 18:53:03 +0000 Subject: [PATCH 4/5] Add comprehensive i18n test suite with GitHub Actions CI - Add mocha, ts-node, and @types/mocha for testing - Create i18n.test.ts with 34 tests covering: - Translation completeness across all languages - Translation quality (no empty/null values) - Locale mapping validation - formatNumber and formatCurrency functions - Language detection from environment - Source code validation for locale-aware formatting - Add getSupportedLanguages() to I18n class - Add GitHub Actions workflow to run tests on push/PR Co-Authored-By: Claude Opus 4.5 --- .github/workflows/test.yml | 34 ++ package-lock.json | 1161 +++++++++++++++++++++++++++++++++++- package.json | 6 +- src/i18n.test.ts | 507 ++++++++++++++++ src/i18n.ts | 4 + 5 files changed, 1701 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 src/i18n.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2fc18e3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Compile TypeScript + run: npm run compile diff --git a/package-lock.json b/package-lock.json index 4001b6d..994aa4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,39 @@ { "name": "claude-code-usage", - "version": "1.0.0", + "version": "1.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-usage", - "version": "1.0.0", + "version": "1.0.8", "license": "MIT", "devDependencies": { "@types/glob": "^9.0.0", + "@types/mocha": "^10.0.1", "@types/node": "24.0.15", "@types/vscode": "^1.74.0", + "mocha": "^10.2.0", + "ts-node": "^10.9.1", "typescript": "^5.8.3" }, "engines": { "vscode": "^1.74.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -59,6 +75,62 @@ "node": ">=12" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/glob": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-9.0.0.tgz", @@ -70,6 +142,13 @@ "glob": "*" } }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.0.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz", @@ -87,6 +166,42 @@ "dev": true, "license": "MIT" }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -113,6 +228,259 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -133,6 +501,13 @@ "dev": true, "license": "MIT" }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -148,6 +523,47 @@ "node": ">= 8" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -162,6 +578,69 @@ "dev": true, "license": "MIT" }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -179,6 +658,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", @@ -203,6 +714,81 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -213,6 +799,52 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -220,20 +852,66 @@ "dev": true, "license": "ISC" }, - "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": "20 || >=22" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lru-cache": { @@ -246,6 +924,13 @@ "node": "20 || >=22" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/minimatch": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", @@ -272,6 +957,135 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -279,6 +1093,16 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -306,6 +1130,83 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -446,6 +1347,102 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -467,6 +1464,13 @@ "dev": true, "license": "MIT" }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -483,6 +1487,13 @@ "node": ">= 8" } }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -580,6 +1591,136 @@ "engines": { "node": ">=8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index d11d246..83c583a 100644 --- a/package.json +++ b/package.json @@ -95,12 +95,16 @@ "scripts": { "vscode:prepublish": "npm run compile", "compile": "tsc -p ./", - "watch": "tsc -watch -p ./" + "watch": "tsc -watch -p ./", + "test": "mocha --require ts-node/register 'src/**/*.test.ts'" }, "devDependencies": { "@types/glob": "^9.0.0", + "@types/mocha": "^10.0.1", "@types/node": "24.0.15", "@types/vscode": "^1.74.0", + "mocha": "^10.2.0", + "ts-node": "^10.9.1", "typescript": "^5.8.3" } } \ No newline at end of file diff --git a/src/i18n.test.ts b/src/i18n.test.ts new file mode 100644 index 0000000..d32eb02 --- /dev/null +++ b/src/i18n.test.ts @@ -0,0 +1,507 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import { I18n } from './i18n'; +import { SupportedLanguage } from './types'; + +// Get all supported languages from the production code +const SUPPORTED_LANGUAGES = I18n.getSupportedLanguages(); + +// Helper to get all keys from an object recursively +function getAllKeys(obj: object, prefix: string = ''): string[] { + const keys: string[] = []; + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + keys.push(...getAllKeys(value, fullKey)); + } else { + keys.push(fullKey); + } + } + return keys; +} + +// Helper to get value by dot-notation path +function getValueByPath(obj: object, path: string): unknown { + return path.split('.').reduce((current: unknown, key: string) => { + if (current && typeof current === 'object') { + return (current as Record)[key]; + } + return undefined; + }, obj); +} + +describe('I18n', () => { + describe('Translation Completeness', () => { + // Get English keys as the reference + let englishKeys: string[]; + + before(() => { + I18n.setLanguage('en'); + englishKeys = getAllKeys(I18n.t); + }); + + it('should have all keys from English in every language', () => { + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const langKeys = getAllKeys(I18n.t); + + for (const key of englishKeys) { + assert.ok( + langKeys.includes(key), + `Missing key "${key}" in language "${lang}"` + ); + } + } + }); + + it('should not have extra keys in any language beyond English', () => { + for (const lang of SUPPORTED_LANGUAGES) { + if (lang === 'en') continue; + + I18n.setLanguage(lang); + const langKeys = getAllKeys(I18n.t); + + for (const key of langKeys) { + assert.ok( + englishKeys.includes(key), + `Extra key "${key}" in language "${lang}" not found in English` + ); + } + } + }); + + it('should have identical structure across all languages', () => { + I18n.setLanguage('en'); + const referenceKeys = getAllKeys(I18n.t).sort(); + + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const langKeys = getAllKeys(I18n.t).sort(); + + assert.deepStrictEqual( + langKeys, + referenceKeys, + `Language "${lang}" has different structure than English` + ); + } + }); + }); + + describe('Translation Quality', () => { + it('should have no empty string translations', () => { + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const keys = getAllKeys(I18n.t); + + for (const key of keys) { + const value = getValueByPath(I18n.t, key); + assert.ok( + typeof value === 'string' && value.trim() !== '', + `Empty translation for key "${key}" in language "${lang}"` + ); + } + } + }); + + it('should have no null or undefined translations', () => { + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const keys = getAllKeys(I18n.t); + + for (const key of keys) { + const value = getValueByPath(I18n.t, key); + assert.ok( + value !== null && value !== undefined, + `Null/undefined translation for key "${key}" in language "${lang}"` + ); + } + } + }); + + it('should not have obvious English text in non-English translations', () => { + // Common English patterns that should be translated + const englishPatterns = [ + /^Loading\.\.\.$/i, + /^Error$/i, + /^Settings$/i, + /^Refresh$/i, + /^Today$/i, + /^Yesterday$/i, + /^This Month$/i, + /^All Time$/i, + ]; + + // Keys that are allowed to contain English (like "Claude Code", "Token") + const allowedEnglishKeys = [ + 'statusBar.noData', + 'statusBar.notRunning', + 'popup.title', + 'popup.totalTokens', + 'popup.inputTokens', + 'popup.outputTokens', + 'settings.title', + ]; + + for (const lang of SUPPORTED_LANGUAGES) { + if (lang === 'en') continue; + + I18n.setLanguage(lang); + const keys = getAllKeys(I18n.t); + + for (const key of keys) { + if (allowedEnglishKeys.some((allowed) => key.includes(allowed))) { + continue; + } + + const value = getValueByPath(I18n.t, key); + if (typeof value === 'string') { + for (const pattern of englishPatterns) { + assert.ok( + !pattern.test(value), + `Untranslated English "${value}" for key "${key}" in language "${lang}"` + ); + } + } + } + } + }); + }); + + describe('Locale Mapping', () => { + it('should return valid locale string for each language', () => { + const expectedLocales: Record = { + en: 'en-US', + 'de-DE': 'de-DE', + 'zh-TW': 'zh-TW', + 'zh-CN': 'zh-CN', + ja: 'ja-JP', + ko: 'ko-KR', + }; + + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const locale = I18n.getLocaleString(); + assert.strictEqual( + locale, + expectedLocales[lang], + `Incorrect locale for language "${lang}": expected "${expectedLocales[lang]}", got "${locale}"` + ); + } + }); + + it('should return a valid BCP 47 locale format', () => { + // BCP 47 format: language-REGION or language + const bcp47Pattern = /^[a-z]{2}(-[A-Z]{2})?$/; + + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const locale = I18n.getLocaleString(); + assert.ok( + bcp47Pattern.test(locale), + `Locale "${locale}" for language "${lang}" is not valid BCP 47 format` + ); + } + }); + }); + + describe('Formatting Functions', () => { + describe('formatNumber', () => { + it('should format numbers correctly for each locale', () => { + const testNumber = 1234567.89; + + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const formatted = I18n.formatNumber(testNumber); + + // Should not throw an error + assert.ok(typeof formatted === 'string', `formatNumber returned non-string for ${lang}`); + + // Should contain digits + assert.ok(/\d/.test(formatted), `formatNumber result "${formatted}" contains no digits for ${lang}`); + } + }); + + it('should handle zero correctly', () => { + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const formatted = I18n.formatNumber(0); + assert.ok(formatted.includes('0'), `formatNumber(0) should contain "0" for ${lang}`); + } + }); + + it('should handle negative numbers', () => { + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const formatted = I18n.formatNumber(-1234); + assert.ok(/[-\u2212]/.test(formatted), `formatNumber(-1234) should contain minus sign for ${lang}`); + } + }); + + it('should use locale-specific separators', () => { + // German uses period as thousands separator, comma as decimal + I18n.setLanguage('de-DE'); + const germanFormatted = I18n.formatNumber(1234567); + // Should contain a period as thousands separator (1.234.567) + assert.ok(germanFormatted.includes('.'), `German should use period as thousands separator: ${germanFormatted}`); + + // English uses comma as thousands separator + I18n.setLanguage('en'); + const englishFormatted = I18n.formatNumber(1234567); + assert.ok(englishFormatted.includes(','), `English should use comma as thousands separator: ${englishFormatted}`); + }); + }); + + describe('formatCurrency', () => { + it('should format currency with dollar sign', () => { + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const formatted = I18n.formatCurrency(123.45); + assert.ok(formatted.includes('$'), `formatCurrency should include $ for ${lang}`); + } + }); + + it('should respect decimal places parameter', () => { + I18n.setLanguage('en'); + + assert.strictEqual(I18n.formatCurrency(123.456, 0), '$123'); + assert.strictEqual(I18n.formatCurrency(123.456, 1), '$123.5'); + assert.strictEqual(I18n.formatCurrency(123.456, 2), '$123.46'); + assert.strictEqual(I18n.formatCurrency(123.456, 3), '$123.456'); + assert.strictEqual(I18n.formatCurrency(123.456, 4), '$123.4560'); + }); + + it('should default to 2 decimal places', () => { + I18n.setLanguage('en'); + const formatted = I18n.formatCurrency(123.456); + assert.strictEqual(formatted, '$123.46'); + }); + + it('should handle zero cost', () => { + I18n.setLanguage('en'); + const formatted = I18n.formatCurrency(0); + assert.strictEqual(formatted, '$0.00'); + }); + + it('should handle very small amounts', () => { + I18n.setLanguage('en'); + const formatted = I18n.formatCurrency(0.001, 4); + assert.strictEqual(formatted, '$0.0010'); + }); + }); + }); + + describe('Language Detection', () => { + const originalEnv = { ...process.env }; + + afterEach(() => { + // Restore original environment + process.env = { ...originalEnv }; + }); + + it('should detect English by default', () => { + delete process.env.LANG; + delete process.env.LANGUAGE; + I18n.setLanguage('auto'); + assert.strictEqual(I18n.getCurrentLanguage(), 'en'); + }); + + it('should detect Traditional Chinese for zh_TW locale', () => { + process.env.LANG = 'zh_TW.UTF-8'; + I18n.setLanguage('auto'); + assert.strictEqual(I18n.getCurrentLanguage(), 'zh-TW'); + }); + + it('should detect Traditional Chinese for zh_HK locale', () => { + process.env.LANG = 'zh_HK.UTF-8'; + I18n.setLanguage('auto'); + assert.strictEqual(I18n.getCurrentLanguage(), 'zh-TW'); + }); + + it('should detect Traditional Chinese for zh_MO locale', () => { + process.env.LANG = 'zh_MO.UTF-8'; + I18n.setLanguage('auto'); + assert.strictEqual(I18n.getCurrentLanguage(), 'zh-TW'); + }); + + it('should detect Simplified Chinese for zh_CN locale', () => { + process.env.LANG = 'zh_CN.UTF-8'; + I18n.setLanguage('auto'); + assert.strictEqual(I18n.getCurrentLanguage(), 'zh-CN'); + }); + + it('should detect Simplified Chinese for generic zh locale', () => { + process.env.LANG = 'zh.UTF-8'; + I18n.setLanguage('auto'); + assert.strictEqual(I18n.getCurrentLanguage(), 'zh-CN'); + }); + + it('should detect Japanese for ja locale', () => { + process.env.LANG = 'ja_JP.UTF-8'; + I18n.setLanguage('auto'); + assert.strictEqual(I18n.getCurrentLanguage(), 'ja'); + }); + + it('should detect Korean for ko locale', () => { + process.env.LANG = 'ko_KR.UTF-8'; + I18n.setLanguage('auto'); + assert.strictEqual(I18n.getCurrentLanguage(), 'ko'); + }); + + it('should fall back to English for unknown locales', () => { + process.env.LANG = 'fr_FR.UTF-8'; + I18n.setLanguage('auto'); + assert.strictEqual(I18n.getCurrentLanguage(), 'en'); + }); + + it('should allow manual language setting', () => { + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + assert.strictEqual(I18n.getCurrentLanguage(), lang); + } + }); + }); + + describe('Source Code Validation', () => { + const srcDir = path.join(__dirname); + const sourceFiles = ['webview.ts', 'extension.ts', 'dataLoader.ts', 'statusBar.ts']; + + it('should not have toLocaleString() without locale parameter in source files', () => { + // Pattern for toLocaleString() without parameters + const unsafePattern = /\.toLocaleString\(\s*\)/g; + + for (const file of sourceFiles) { + const filePath = path.join(srcDir, file); + if (!fs.existsSync(filePath)) continue; + + const content = fs.readFileSync(filePath, 'utf8'); + const matches = content.match(unsafePattern); + + assert.ok( + !matches, + `Found toLocaleString() without locale in ${file}: ${matches?.join(', ')}` + ); + } + }); + + it('should not have toLocaleDateString() without locale parameter in source files', () => { + // Pattern for toLocaleDateString() without parameters + const unsafePattern = /\.toLocaleDateString\(\s*\)/g; + + for (const file of sourceFiles) { + const filePath = path.join(srcDir, file); + if (!fs.existsSync(filePath)) continue; + + const content = fs.readFileSync(filePath, 'utf8'); + const matches = content.match(unsafePattern); + + assert.ok( + !matches, + `Found toLocaleDateString() without locale in ${file}: ${matches?.join(', ')}` + ); + } + }); + + it('should not have toLocaleTimeString() without locale parameter in source files', () => { + // Pattern for toLocaleTimeString() without parameters + const unsafePattern = /\.toLocaleTimeString\(\s*\)/g; + + for (const file of sourceFiles) { + const filePath = path.join(srcDir, file); + if (!fs.existsSync(filePath)) continue; + + const content = fs.readFileSync(filePath, 'utf8'); + const matches = content.match(unsafePattern); + + assert.ok( + !matches, + `Found toLocaleTimeString() without locale in ${file}: ${matches?.join(', ')}` + ); + } + }); + + it('should use I18n.formatNumber or locale parameter for number formatting', () => { + // This test checks that any toLocaleString call on numbers either: + // 1. Uses I18n.getLocaleString() or a locale parameter + // 2. Or uses I18n.formatNumber() + + // We look for patterns that suggest locale-unaware formatting + const suspiciousPatterns = [ + /\d+\.toLocaleString\(\s*\)/g, // Direct number.toLocaleString() + /\)\s*\.toLocaleString\(\s*\)/g, // result.toLocaleString() + ]; + + for (const file of sourceFiles) { + const filePath = path.join(srcDir, file); + if (!fs.existsSync(filePath)) continue; + + const content = fs.readFileSync(filePath, 'utf8'); + + for (const pattern of suspiciousPatterns) { + const matches = content.match(pattern); + assert.ok( + !matches, + `Suspicious locale-unaware number formatting in ${file}: ${matches?.join(', ')}` + ); + } + } + }); + }); + + describe('Integration Tests', () => { + it('should maintain consistent behavior when switching languages', () => { + // Test that we can switch languages and get correct translations for "today" + const expectedValues: Record = { + en: 'Today', + 'de-DE': 'Heute', + 'zh-TW': '今日', + 'zh-CN': '今日', + ja: '今日', + ko: '오늘', + }; + + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + assert.strictEqual( + I18n.t.popup.today, + expectedValues[lang], + `Incorrect translation for "today" in ${lang}` + ); + } + }); + + it('should return translations object with correct type', () => { + for (const lang of SUPPORTED_LANGUAGES) { + I18n.setLanguage(lang); + const translations = I18n.t; + + // Check structure + assert.ok(typeof translations === 'object'); + assert.ok('statusBar' in translations); + assert.ok('popup' in translations); + assert.ok('settings' in translations); + + // Check nested structure + assert.ok(typeof translations.statusBar === 'object'); + assert.ok(typeof translations.popup === 'object'); + assert.ok(typeof translations.settings === 'object'); + } + }); + + it('should format numbers and dates consistently with current locale', () => { + // Ensure formatNumber uses the correct locale after language change + I18n.setLanguage('de-DE'); + const germanNumber = I18n.formatNumber(1234.56); + + I18n.setLanguage('en'); + const englishNumber = I18n.formatNumber(1234.56); + + // German and English should format differently + assert.notStrictEqual( + germanNumber, + englishNumber, + 'German and English number formatting should differ' + ); + }); + }); +}); diff --git a/src/i18n.ts b/src/i18n.ts index df1f2c7..e6099fa 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -417,4 +417,8 @@ export class I18n { }; return localeMap[this.currentLanguage] || 'en-US'; } + + static getSupportedLanguages(): SupportedLanguage[] { + return Object.keys(translations) as SupportedLanguage[]; + } } From 9473d42dc96acaba8d8ac51c9883a892435535a3 Mon Sep 17 00:00:00 2001 From: Nick Earnshaw Date: Sat, 28 Feb 2026 18:54:57 +0000 Subject: [PATCH 5/5] Remove GitHub Actions workflow Co-Authored-By: Claude Opus 4.5 --- .github/workflows/test.yml | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 2fc18e3..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Tests - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [18.x, 20.x, 22.x] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm test - - - name: Compile TypeScript - run: npm run compile
日期費用輸入 Token輸出 Token快取建立快取讀取訊息數' + I18nStrings.date + '' + I18nStrings.cost + '' + I18nStrings.inputTokens + '' + I18nStrings.outputTokens + '' + I18nStrings.cacheCreation + '' + I18nStrings.cacheRead + '' + I18nStrings.messages + '
' + formattedDate + '
' + item.hour + '$' + item.data.totalCost.toFixed(2) + '' + item.data.totalInputTokens.toLocaleString() + '' + item.data.totalOutputTokens.toLocaleString() + '' + item.data.totalCacheCreationTokens.toLocaleString() + '' + item.data.totalCacheReadTokens.toLocaleString() + '' + item.data.messageCount.toLocaleString() + '' + item.data.totalInputTokens.toLocaleString(currentLocale) + '' + item.data.totalOutputTokens.toLocaleString(currentLocale) + '' + item.data.totalCacheCreationTokens.toLocaleString(currentLocale) + '' + item.data.totalCacheReadTokens.toLocaleString(currentLocale) + '' + item.data.messageCount.toLocaleString(currentLocale) + '
' + formattedDate + '$' + item.data.totalCost.toFixed(2) + '' + item.data.totalInputTokens.toLocaleString() + '' + item.data.totalOutputTokens.toLocaleString() + '' + item.data.totalCacheCreationTokens.toLocaleString() + '' + item.data.totalCacheReadTokens.toLocaleString() + '' + item.data.messageCount.toLocaleString() + '' + item.data.totalInputTokens.toLocaleString(currentLocale) + '' + item.data.totalOutputTokens.toLocaleString(currentLocale) + '' + item.data.totalCacheCreationTokens.toLocaleString(currentLocale) + '' + item.data.totalCacheReadTokens.toLocaleString(currentLocale) + '' + item.data.messageCount.toLocaleString(currentLocale) + '