Skip to content

Commit d6ea143

Browse files
committed
v1.1.0
- Added Windows-aware UI localisation with language override
1 parent ec4177f commit d6ea143

File tree

9 files changed

+646
-132
lines changed

9 files changed

+646
-132
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "claude-code-usage-monitor"
3-
version = "1.0.15"
3+
version = "1.1.0"
44
edition = "2021"
55
license = "MIT"
66
description = "Windows taskbar widget for monitoring Claude Code usage and rate limits"
@@ -27,6 +27,7 @@ dirs = "6"
2727
version = "0.58"
2828
features = [
2929
"Win32_Foundation",
30+
"Win32_Globalization",
3031
"Win32_Graphics_Gdi",
3132
"Win32_System_LibraryLoader",
3233
"Win32_UI_Shell",

build.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ fn main() {
1818

1919
fn pack_version(version: &str) -> u64 {
2020
let core = version.split('-').next().unwrap_or(version);
21-
let mut parts = core
22-
.split('.')
23-
.map(|part| part.parse::<u64>().unwrap_or(0));
21+
let mut parts = core.split('.').map(|part| part.parse::<u64>().unwrap_or(0));
2422

2523
let major = parts.next().unwrap_or(0).min(u16::MAX as u64);
2624
let minor = parts.next().unwrap_or(0).min(u16::MAX as u64);

src/localization.rs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
use windows::core::PWSTR;
2+
use windows::Win32::Globalization::{
3+
GetUserDefaultLocaleName, GetUserDefaultUILanguage, GetUserPreferredUILanguages,
4+
LCIDToLocaleName, LOCALE_ALLOW_NEUTRAL_NAMES, MAX_LOCALE_NAME, MUI_LANGUAGE_NAME,
5+
};
6+
7+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8+
pub enum LanguageId {
9+
English,
10+
Spanish,
11+
French,
12+
German,
13+
Japanese,
14+
}
15+
16+
impl LanguageId {
17+
pub const ALL: [LanguageId; 5] = [
18+
LanguageId::English,
19+
LanguageId::Spanish,
20+
LanguageId::French,
21+
LanguageId::German,
22+
LanguageId::Japanese,
23+
];
24+
25+
pub fn code(self) -> &'static str {
26+
match self {
27+
Self::English => "en",
28+
Self::Spanish => "es",
29+
Self::French => "fr",
30+
Self::German => "de",
31+
Self::Japanese => "ja",
32+
}
33+
}
34+
35+
pub fn native_name(self) -> &'static str {
36+
match self {
37+
Self::English => "English",
38+
Self::Spanish => "Español",
39+
Self::French => "Français",
40+
Self::German => "Deutsch",
41+
Self::Japanese => "日本語",
42+
}
43+
}
44+
45+
pub fn strings(self) -> Strings {
46+
match self {
47+
Self::English => ENGLISH,
48+
Self::Spanish => SPANISH,
49+
Self::French => FRENCH,
50+
Self::German => GERMAN,
51+
Self::Japanese => JAPANESE,
52+
}
53+
}
54+
55+
pub fn from_code(code: &str) -> Option<Self> {
56+
let normalized = code.trim().replace('_', "-").to_ascii_lowercase();
57+
if normalized.is_empty() || normalized == "system" {
58+
return None;
59+
}
60+
61+
match normalized.split('-').next().unwrap_or_default() {
62+
"en" => Some(Self::English),
63+
"es" => Some(Self::Spanish),
64+
"fr" => Some(Self::French),
65+
"de" => Some(Self::German),
66+
"ja" => Some(Self::Japanese),
67+
_ => None,
68+
}
69+
}
70+
}
71+
72+
#[derive(Clone, Copy, Debug)]
73+
pub struct Strings {
74+
pub window_title: &'static str,
75+
pub refresh: &'static str,
76+
pub update_frequency: &'static str,
77+
pub one_minute: &'static str,
78+
pub five_minutes: &'static str,
79+
pub fifteen_minutes: &'static str,
80+
pub one_hour: &'static str,
81+
pub settings: &'static str,
82+
pub start_with_windows: &'static str,
83+
pub reset_position: &'static str,
84+
pub language: &'static str,
85+
pub system_default: &'static str,
86+
pub exit: &'static str,
87+
pub session_window: &'static str,
88+
pub weekly_window: &'static str,
89+
pub now: &'static str,
90+
pub day_suffix: &'static str,
91+
pub hour_suffix: &'static str,
92+
pub minute_suffix: &'static str,
93+
pub second_suffix: &'static str,
94+
}
95+
96+
const ENGLISH: Strings = Strings {
97+
window_title: "Claude Code Usage Monitor",
98+
refresh: "Refresh",
99+
update_frequency: "Update Frequency",
100+
one_minute: "1 Minute",
101+
five_minutes: "5 Minutes",
102+
fifteen_minutes: "15 Minutes",
103+
one_hour: "1 Hour",
104+
settings: "Settings",
105+
start_with_windows: "Start with Windows",
106+
reset_position: "Reset Position",
107+
language: "Language",
108+
system_default: "System Default",
109+
exit: "Exit",
110+
session_window: "5h",
111+
weekly_window: "7d",
112+
now: "now",
113+
day_suffix: "d",
114+
hour_suffix: "h",
115+
minute_suffix: "m",
116+
second_suffix: "s",
117+
};
118+
119+
const SPANISH: Strings = Strings {
120+
window_title: "Monitor de uso de Claude Code",
121+
refresh: "Actualizar",
122+
update_frequency: "Frecuencia de actualizacion",
123+
one_minute: "1 minuto",
124+
five_minutes: "5 minutos",
125+
fifteen_minutes: "15 minutos",
126+
one_hour: "1 hora",
127+
settings: "Configuracion",
128+
start_with_windows: "Iniciar con Windows",
129+
reset_position: "Restablecer posicion",
130+
language: "Idioma",
131+
system_default: "Predeterminado del sistema",
132+
exit: "Salir",
133+
session_window: "5h",
134+
weekly_window: "7d",
135+
now: "ahora",
136+
day_suffix: "d",
137+
hour_suffix: "h",
138+
minute_suffix: "m",
139+
second_suffix: "s",
140+
};
141+
142+
const FRENCH: Strings = Strings {
143+
window_title: "Moniteur d'utilisation Claude Code",
144+
refresh: "Actualiser",
145+
update_frequency: "Frequence de mise a jour",
146+
one_minute: "1 minute",
147+
five_minutes: "5 minutes",
148+
fifteen_minutes: "15 minutes",
149+
one_hour: "1 heure",
150+
settings: "Parametres",
151+
start_with_windows: "Demarrer avec Windows",
152+
reset_position: "Reinitialiser la position",
153+
language: "Langue",
154+
system_default: "Par defaut du systeme",
155+
exit: "Quitter",
156+
session_window: "5h",
157+
weekly_window: "7d",
158+
now: "maintenant",
159+
day_suffix: "j",
160+
hour_suffix: "h",
161+
minute_suffix: "m",
162+
second_suffix: "s",
163+
};
164+
165+
const GERMAN: Strings = Strings {
166+
window_title: "Claude Code Nutzungsmonitor",
167+
refresh: "Aktualisieren",
168+
update_frequency: "Aktualisierungsintervall",
169+
one_minute: "1 Minute",
170+
five_minutes: "5 Minuten",
171+
fifteen_minutes: "15 Minuten",
172+
one_hour: "1 Stunde",
173+
settings: "Einstellungen",
174+
start_with_windows: "Mit Windows starten",
175+
reset_position: "Position zurucksetzen",
176+
language: "Sprache",
177+
system_default: "Systemstandard",
178+
exit: "Beenden",
179+
session_window: "5h",
180+
weekly_window: "7d",
181+
now: "jetzt",
182+
day_suffix: "T",
183+
hour_suffix: "h",
184+
minute_suffix: "m",
185+
second_suffix: "s",
186+
};
187+
188+
const JAPANESE: Strings = Strings {
189+
window_title: "Claude Code 使用量モニター",
190+
refresh: "更新",
191+
update_frequency: "更新間隔",
192+
one_minute: "1分",
193+
five_minutes: "5分",
194+
fifteen_minutes: "15分",
195+
one_hour: "1時間",
196+
settings: "設定",
197+
start_with_windows: "Windows と同時に開始",
198+
reset_position: "位置をリセット",
199+
language: "言語",
200+
system_default: "システム既定",
201+
exit: "終了",
202+
session_window: "5h",
203+
weekly_window: "7d",
204+
now: "今",
205+
day_suffix: "日",
206+
hour_suffix: "時間",
207+
minute_suffix: "分",
208+
second_suffix: "秒",
209+
};
210+
211+
pub fn resolve_language(language_override: Option<LanguageId>) -> LanguageId {
212+
language_override.unwrap_or_else(detect_system_language)
213+
}
214+
215+
pub fn detect_system_language() -> LanguageId {
216+
preferred_ui_languages()
217+
.into_iter()
218+
.find_map(|locale| LanguageId::from_code(&locale))
219+
.or_else(default_ui_locale)
220+
.or_else(default_locale_name)
221+
.unwrap_or(LanguageId::English)
222+
}
223+
224+
fn preferred_ui_languages() -> Vec<String> {
225+
unsafe {
226+
let mut num_languages = 0u32;
227+
let mut buffer_len = 0u32;
228+
if GetUserPreferredUILanguages(
229+
MUI_LANGUAGE_NAME,
230+
&mut num_languages,
231+
PWSTR::null(),
232+
&mut buffer_len,
233+
)
234+
.is_err()
235+
|| buffer_len == 0
236+
{
237+
return Vec::new();
238+
}
239+
240+
let mut buffer = vec![0u16; buffer_len as usize];
241+
if GetUserPreferredUILanguages(
242+
MUI_LANGUAGE_NAME,
243+
&mut num_languages,
244+
PWSTR(buffer.as_mut_ptr()),
245+
&mut buffer_len,
246+
)
247+
.is_err()
248+
{
249+
return Vec::new();
250+
}
251+
252+
buffer
253+
.split(|unit| *unit == 0)
254+
.filter(|part| !part.is_empty())
255+
.map(String::from_utf16_lossy)
256+
.collect()
257+
}
258+
}
259+
260+
fn default_ui_locale() -> Option<LanguageId> {
261+
unsafe {
262+
let lang_id = GetUserDefaultUILanguage();
263+
let mut buffer = [0u16; MAX_LOCALE_NAME as usize];
264+
let len = LCIDToLocaleName(
265+
lang_id as u32,
266+
Some(&mut buffer),
267+
LOCALE_ALLOW_NEUTRAL_NAMES,
268+
);
269+
if len <= 1 {
270+
return None;
271+
}
272+
let locale = String::from_utf16_lossy(&buffer[..(len as usize - 1)]);
273+
LanguageId::from_code(&locale)
274+
}
275+
}
276+
277+
fn default_locale_name() -> Option<LanguageId> {
278+
unsafe {
279+
let mut buffer = [0u16; MAX_LOCALE_NAME as usize];
280+
let len = GetUserDefaultLocaleName(&mut buffer);
281+
if len <= 1 {
282+
return None;
283+
}
284+
let locale = String::from_utf16_lossy(&buffer[..(len as usize - 1)]);
285+
LanguageId::from_code(&locale)
286+
}
287+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![windows_subsystem = "windows"]
22

3+
mod localization;
34
mod models;
45
mod native_interop;
56
mod poller;

src/native_interop.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use windows::core::PCWSTR;
22
use windows::Win32::Foundation::{HWND, RECT};
33
use windows::Win32::UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK};
4-
use windows::Win32::UI::Shell::{ABM_GETTASKBARPOS, APPBARDATA, SHAppBarMessage};
4+
use windows::Win32::UI::Shell::{SHAppBarMessage, ABM_GETTASKBARPOS, APPBARDATA};
55
use windows::Win32::UI::WindowsAndMessaging::*;
66

77
// Window style constants
@@ -107,15 +107,7 @@ pub fn move_window(hwnd: HWND, x: i32, y: i32, w: i32, h: i32) {
107107
/// Set up a WinEvent hook for tray location changes
108108
pub fn set_tray_event_hook(
109109
thread_id: u32,
110-
callback: unsafe extern "system" fn(
111-
HWINEVENTHOOK,
112-
u32,
113-
HWND,
114-
i32,
115-
i32,
116-
u32,
117-
u32,
118-
),
110+
callback: unsafe extern "system" fn(HWINEVENTHOOK, u32, HWND, i32, i32, u32, u32),
119111
) -> Option<HWINEVENTHOOK> {
120112
unsafe {
121113
let hook = SetWinEventHook(

0 commit comments

Comments
 (0)