feat: add multi-language support (i18n) using gotext#18
Conversation
Implements internationalization support as proposed in #13: - Add github.com/leonelquinteros/gotext dependency - Create internal/i18n package with: - Language detection (Windows GetUserDefaultUILanguage API) - Embedded locale files using go:embed - Get() function for translating strings - Add locale files for: - English (en_US) - default - Traditional Chinese (zh_TW) - Refactor GUI strings in: - app.go - window title - mainwindow.go - labels, buttons, status messages - monitor.go - log messages, process status The application now automatically detects the system language on Windows and displays the UI in the appropriate language. Closes #13
|
@github-copilot review |
There was a problem hiding this comment.
Pull request overview
This PR implements internationalization (i18n) support for the Multiablo application using the gotext library, as proposed in issue #13. The implementation adds automatic system language detection on Windows and embeds locale files for English and Traditional Chinese translations.
Changes:
- Added new
internal/i18npackage with core translation functions, Windows-specific language detection, and embedded locale files - Refactored all GUI strings in
app.go,mainwindow.go, andmonitor.goto use i18n translation functions - Added
gotextv1.7.2 as a new dependency
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/i18n/i18n.go | Core i18n package with translation lookup and locale management using embedded filesystem |
| internal/i18n/lang_windows.go | Windows-specific language detection using GetUserDefaultUILanguage API |
| internal/i18n/lang_other.go | Fallback language detection for non-Windows platforms via environment variables |
| internal/i18n/locales/en_US/LC_MESSAGES/default.po | English translation file (default/source language) |
| internal/i18n/locales/zh_TW/LC_MESSAGES/default.po | Traditional Chinese translation file |
| internal/gui/app.go | Initialize i18n system and convert AppTitle to function returning translated string |
| internal/gui/mainwindow.go | Convert all UI labels, buttons, and status messages to use i18n.Get() |
| internal/gui/monitor.go | Convert all log messages and process status strings to use i18n.Get() |
| go.mod | Add gotext v1.7.2 dependency |
| go.sum | Update dependency checksums for gotext |
| if subLangID == 0x01 { // Traditional Chinese | ||
| return LangZhTW | ||
| } | ||
| // Default to Traditional Chinese for any Chinese variant | ||
| return LangZhTW |
There was a problem hiding this comment.
The default behavior on line 31 returns Traditional Chinese (zh_TW) for all Chinese language variants that are not explicitly handled. This is unusual since Simplified Chinese (zh_CN) is more widely used. Consider either:
- Adding explicit handling for Simplified Chinese (sublanguage ID 0x02) and falling back to English for unknown Chinese variants, or
- Documenting why Traditional Chinese is the preferred default for this specific application.
| if subLangID == 0x01 { // Traditional Chinese | |
| return LangZhTW | |
| } | |
| // Default to Traditional Chinese for any Chinese variant | |
| return LangZhTW | |
| switch subLangID { | |
| case 0x01: // Traditional Chinese | |
| return LangZhTW | |
| case 0x02: // Simplified Chinese | |
| return LangZhCN | |
| default: | |
| // Fall back to English for other/unknown Chinese variants | |
| return LangEnUS | |
| } |
| msgstr "監控已停止。" | ||
|
|
||
| msgid "Closed %d handle(s) for D2R.exe (PID: %d)" | ||
| msgstr "已為 D2R.exe (PID: %d) 關閉 %d 個 Handle" |
There was a problem hiding this comment.
The format string arguments are in the wrong order in the Chinese translation. The English message is "Closed %d handle(s) for D2R.exe (PID: %d)" where the first %d is closedCount and the second %d is PID. However, the Chinese translation "已為 D2R.exe (PID: %d) 關閉 %d 個 Handle" has them reversed, which will cause the PID to be displayed as the count and vice versa.
The correct translation should maintain the same argument order as the original format string.
| msgstr "已為 D2R.exe (PID: %d) 關閉 %d 個 Handle" | |
| msgstr "已關閉 %d 個 D2R.exe Handle (PID: %d)" |
| // getLocaleInfo retrieves locale information from Windows. | ||
| func getLocaleInfo(lcid uint32, lcType uint32) string { | ||
| kernel32 := syscall.NewLazyDLL("kernel32.dll") | ||
| getLocaleInfoW := kernel32.NewProc("GetLocaleInfoW") | ||
|
|
||
| // First call to get required buffer size | ||
| size, _, _ := getLocaleInfoW.Call( | ||
| uintptr(lcid), | ||
| uintptr(lcType), | ||
| 0, | ||
| 0, | ||
| ) | ||
|
|
||
| if size == 0 { | ||
| return "" | ||
| } | ||
|
|
||
| // Allocate buffer and get the actual value | ||
| buf := make([]uint16, size) | ||
| ret, _, _ := getLocaleInfoW.Call( | ||
| uintptr(lcid), | ||
| uintptr(lcType), | ||
| uintptr(unsafe.Pointer(&buf[0])), | ||
| size, | ||
| ) | ||
|
|
||
| if ret == 0 { | ||
| return "" | ||
| } | ||
|
|
||
| return strings.TrimRight(syscall.UTF16ToString(buf), "\x00") | ||
| } |
There was a problem hiding this comment.
The getLocaleInfo function is defined but never used anywhere in the codebase. This unused code should be removed to improve maintainability, or if it's intended for future use, it should be documented as such.
|
|
||
| // getLocaleInfo retrieves locale information from Windows. | ||
| func getLocaleInfo(lcid uint32, lcType uint32) string { | ||
| kernel32 := syscall.NewLazyDLL("kernel32.dll") |
There was a problem hiding this comment.
The local variable kernel32 on line 39 shadows the package-level variable of the same name declared on line 12. While this code is in an unused function (getLocaleInfo), if the function were to be used, this shadowing could lead to confusion. Consider using a different variable name like k32 or removing the unused function entirely.
| kernel32 := syscall.NewLazyDLL("kernel32.dll") |
| var ( | ||
| // currentLocale holds the active locale | ||
| currentLocale *gotext.Locale | ||
| // currentLang holds the current language code | ||
| currentLang string | ||
| ) |
There was a problem hiding this comment.
The package-level variables currentLocale and currentLang are accessed and modified without any synchronization mechanism. The Init function (called by SetLanguage) writes to these variables, while Get/GetN/GetCurrentLanguage functions read from them. Since the GUI monitoring runs in multiple goroutines that call i18n.Get(), and SetLanguage could potentially be called concurrently, this creates a race condition risk. Consider protecting these variables with a sync.RWMutex or making Init/SetLanguage thread-safe by documenting that they should only be called during initialization before any goroutines start.
| import ( | ||
| "strings" | ||
| "syscall" | ||
| "unsafe" | ||
| ) |
There was a problem hiding this comment.
The imports "strings" and "unsafe" are only used in the unused getLocaleInfo function. If getLocaleInfo is removed (as suggested in the other comment), these imports should also be removed to avoid unnecessary dependencies.
- Fix format string argument order in zh_TW translation (bug fix) - Remove unused getLocaleInfo function and related imports - Change Simplified Chinese fallback to English (no zh_CN translation) - Add thread safety documentation for Init() and package
Summary
Implements internationalization (i18n) support as proposed in #13, using the gotext library which follows the GNU gettext standard.
Changes
New i18n Package (
internal/i18n/)i18n.go- Core translation functions with embedded locale fileslang_windows.go- Windows-specific language detection usingGetUserDefaultUILanguageAPIlang_other.go- Fallback language detection for non-Windows (viaLANGenv)Locale Files (
internal/i18n/locales/)en_US/LC_MESSAGES/default.po- English (default)zh_TW/LC_MESSAGES/default.po- Traditional ChineseGUI Refactoring
app.go- Window title now uses i18nmainwindow.go- All UI labels, buttons, and status messages use i18nmonitor.go- All log messages and process status strings use i18nFeatures
.pofilesTest Plan
Closes #13
🤖 Generated with Claude Code