Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR adds internationalization (i18n) support to the openSUSE Kudos application using the gettext system with vue3-gettext. It also includes backend improvements for graceful shutdown handling.
Key changes:
- Integration of vue3-gettext for frontend translations with dynamic locale loading
- Scripts for extracting and compiling gettext translation files
- Addition of Czech language support including a playful "Brněnské Hantec" dialect
- Backend graceful shutdown implementation for proper cleanup on termination
Reviewed Changes
Copilot reviewed 12 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
frontend/src/main.js |
Adds gettext plugin initialization with dynamic locale discovery from JSON files |
frontend/src/components/LanguageToggle.vue |
New component for cycling through available languages |
scripts/extract-gettext.js |
Script to extract translatable strings from Vue/JS files into POT format |
scripts/compile-gettext.js |
Script to compile PO files into JSON format for runtime use |
package.json |
Adds i18n-related dependencies and npm scripts for translation workflows |
frontend/locale/*.json |
Language-specific translation files (Czech, English) |
frontend/locale/messages.pot |
Generated POT template file with all extractable strings |
frontend/locale/gettext.json |
Compiled translations in JSON format |
frontend/public/language.svg |
Language toggle icon asset |
backend/src/app.js |
Adds graceful shutdown handlers for SIGINT/SIGTERM signals |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (match) { | ||
| const code = match[1]; | ||
| translations[code] = content.default || content; | ||
| const langName = |
There was a problem hiding this comment.
The content.meta property access may fail if the JSON file structure is incorrect. The code assumes that locale JSON files have a meta property, but this could cause runtime errors if the property is missing or malformed.
Consider adding a safer fallback:
const langName =
content.default?.meta?.LanguageName ||
content.meta?.LanguageName ||
new Intl.DisplayNames([code], { type: "language" }).of(code) ||
code;| const langName = | |
| const langName = | |
| content.default?.meta?.LanguageName || |
| const match = path.match(/locale\/([a-zA-Z_-]+)\.json$/); | ||
| if (match) { | ||
| const code = match[1]; | ||
| translations[code] = content.default || content; |
There was a problem hiding this comment.
The translations object is built from JSON files but vue3-gettext expects a specific structure. The current implementation assigns the entire content to translations[code], but based on the JSON files shown (e.g., cs.json, en.json), the structure appears to be { meta: {...}, [code]: {...} }.
This mismatch could cause translations to fail. Consider:
translations[code] = content[code] || content.default?.[code] || {};| translations[code] = content.default || content; | |
| translations[code] = content[code] || content.default?.[code] || {}; |
| const translations = {}; | ||
|
|
||
| for (const [path, content] of Object.entries(localeModules)) { | ||
| const match = path.match(/locale\/([a-zA-Z_-]+)\.json$/); |
There was a problem hiding this comment.
The regex pattern /locale\/([a-zA-Z_-]+)\.json$/ allows locale codes with underscores and hyphens, but BCP 47 language tags use hyphens (e.g., en-US), not underscores. While cs_HANTEC uses an underscore, this inconsistency could cause confusion.
Consider standardizing on either hyphens or underscores, and document the expected format.
| const match = path.match(/locale\/([a-zA-Z_-]+)\.json$/); | |
| // Only allow BCP 47 language tags (e.g., en-US, zh-Hant). Locale filenames must use hyphens, not underscores. | |
| const match = path.match(/locale\/([a-zA-Z-]+)\.json$/); |
| const langName = | ||
| content.meta?.LanguageName || | ||
| new Intl.DisplayNames([code], { type: "language" }).of(code) || | ||
| code; |
There was a problem hiding this comment.
The Intl.DisplayNames API may not be supported in all browsers, and could throw an error if the locale code is invalid. Add error handling:
let langName = code;
try {
langName = content.meta?.LanguageName ||
new Intl.DisplayNames([code], { type: "language" }).of(code) ||
code;
} catch (e) {
console.warn(`Could not get display name for locale ${code}:`, e);
}
availableLanguages[code] = langName;| const langName = | |
| content.meta?.LanguageName || | |
| new Intl.DisplayNames([code], { type: "language" }).of(code) || | |
| code; | |
| let langName = code; | |
| try { | |
| langName = | |
| content.meta?.LanguageName || | |
| new Intl.DisplayNames([code], { type: "language" }).of(code) || | |
| code; | |
| } catch (e) { | |
| console.warn(`Could not get display name for locale ${code}:`, e); | |
| } |
| async function gracefulShutdown(signal) { | ||
| console.log(`\n🛑 Received ${signal}. Shutting down gracefully...`); | ||
|
|
||
| if (server) { | ||
| server.close(async (err) => { | ||
| if (err) { | ||
| console.error("❌ Error closing server:", err); | ||
| } | ||
| try { | ||
| console.log("💾 Closing Prisma connection..."); | ||
| await prisma.$disconnect(); | ||
| console.log("✅ Database disconnected cleanly."); | ||
| } catch (e) { | ||
| console.error("⚠️ Error disconnecting database:", e); | ||
| } | ||
|
|
||
| console.log("👋 Bye!"); | ||
| process.exit(0); | ||
| }); | ||
|
|
||
| // Optional safety timeout (10s) | ||
| setTimeout(() => { | ||
| console.warn("⏱ Forced exit after timeout."); | ||
| process.exit(1); | ||
| }, 10000).unref(); | ||
| } |
There was a problem hiding this comment.
The graceful shutdown implementation has a potential issue: if server.close() callback is never called (e.g., if there are hanging connections), the process won't exit until the 10-second timeout.
Consider tracking active connections and closing them explicitly, or reducing the timeout duration to 5 seconds for faster feedback during development.
Needed for prod, I don't want to kill db in middle of transaction :-)