From 54cbe186f9b2cca67bfd676850420dfa249306cb Mon Sep 17 00:00:00 2001 From: k1enn Date: Tue, 9 Jun 2026 11:13:08 +0000 Subject: [PATCH 1/3] feat(frontend): add vietnamese language and switcher - Add vi.json locale (full translation, 72 keys) - Register vi locale with no_prefix strategy + browser detect cookie - Add LanguageSetting segmented switcher in /settings Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/frontend/nuxt.config.ts | 14 +- .../components/setting/LanguageSetting.vue | 48 ++++++ apps/frontend/src/app/pages/settings.vue | 1 + apps/frontend/src/i18n/locales/vi.json | 141 ++++++++++++++++++ 4 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/src/app/components/setting/LanguageSetting.vue create mode 100644 apps/frontend/src/i18n/locales/vi.json diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts index f8e329c..9e7304b 100644 --- a/apps/frontend/nuxt.config.ts +++ b/apps/frontend/nuxt.config.ts @@ -47,7 +47,19 @@ export default defineNuxtConfig({ i18n: { restructureDir: "./src/i18n", defaultLocale: "en", - locales: [{ code: "en", name: "English", file: "en.json" }], + // no_prefix: locale lives in a cookie, no /vi/ path — keeps the + // root /[shortCode] redirect route untouched. + strategy: "no_prefix", + locales: [ + { code: "en", name: "English", file: "en.json" }, + { code: "vi", name: "Tiếng Việt", file: "vi.json" }, + ], + // Detect browser language on first visit, then persist the choice. + detectBrowserLanguage: { + useCookie: true, + cookieKey: "i18n_redirected", + redirectOn: "root", + }, }, // Runtime runtimeConfig: { diff --git a/apps/frontend/src/app/components/setting/LanguageSetting.vue b/apps/frontend/src/app/components/setting/LanguageSetting.vue new file mode 100644 index 0000000..dd41f3a --- /dev/null +++ b/apps/frontend/src/app/components/setting/LanguageSetting.vue @@ -0,0 +1,48 @@ + + + diff --git a/apps/frontend/src/app/pages/settings.vue b/apps/frontend/src/app/pages/settings.vue index 5414827..cc198c5 100644 --- a/apps/frontend/src/app/pages/settings.vue +++ b/apps/frontend/src/app/pages/settings.vue @@ -21,6 +21,7 @@ useSeoMeta({
+
diff --git a/apps/frontend/src/i18n/locales/vi.json b/apps/frontend/src/i18n/locales/vi.json new file mode 100644 index 0000000..e1f1f68 --- /dev/null +++ b/apps/frontend/src/i18n/locales/vi.json @@ -0,0 +1,141 @@ +{ + "site": { + "url_shortener": { + "title": "rút gọn url - solitar", + "og_title": "Rút Gọn URL - Solitar", + "description": "Tạo url ngắn cho website và trang mạng xã hội của bạn trong vài giây" + }, + "qr_generator": { + "title": "tạo mã qr - solitar", + "og_title": "Trình Tạo Mã QR - Solitar", + "description": "Tạo mã qr cho url của bạn trong vài giây, lưu và chia sẻ ngay lập tức" + }, + "settings": { + "title": "cài đặt - solitar", + "og_title": "Cài Đặt - Solitar", + "description": "Tùy chỉnh trải nghiệm solitar của bạn" + } + }, + + "nav": { + "url_shortener": "rút gọn url", + "qr_generator": "tạo mã qr", + "settings": "cài đặt" + }, + + "footer": { + "column": { + "about": "giới thiệu", + "resource": "tài nguyên", + "legal": "pháp lý" + }, + "item": { + "changelog": "nhật ký thay đổi", + "status": "trạng thái", + "translate": "dịch thuật", + "api": "api", + "terms_of_use": "điều khoản sử dụng", + "privacy_policy": "chính sách bảo mật", + "report": "báo cáo" + } + }, + + "feature": { + "title": "Solitar có những gì?", + "subtitle": "Mọi thứ bạn cần khi làm việc với url", + "item": { + "speed": { + "title": "Nhanh Chóng Mặt", + "description": "Chuyển hướng tức thì, không độ trễ. Không trang trung gian hay quảng cáo chèn ngang." + }, + "analytics": { + "title": "Phân Tích Tôn Trọng Quyền Riêng Tư", + "description": "Theo dõi lượt nhấp và nguồn truy cập mà không thu thập dữ liệu cá nhân người dùng." + }, + "api": { + "title": "Ưu Tiên Lập Trình Viên", + "description": "REST API mạnh mẽ để tự động tạo liên kết ngay trong ứng dụng của bạn." + }, + "expiration": { + "title": "Hết Hạn Linh Hoạt", + "description": "Đặt liên kết tồn tại vĩnh viễn hoặc tự hủy sau một khoảng thời gian nhất định." + }, + "security": { + "title": "Xác Minh Bảo Mật", + "description": "Cảnh báo người dùng khi truy cập các trang web không tuân thủ tiêu chuẩn bảo mật." + }, + "open_source": { + "title": "Mã Nguồn Mở", + "description": "Logic chuyển hướng hoàn toàn minh bạch. Kiểm tra mã nguồn, tự lưu trữ." + } + } + }, + + "form": { + "password": { + "empty": "Mật khẩu không được để trống", + "invalid": "Mật khẩu phải là chuỗi ký tự", + "min": "Mật khẩu phải có ít nhất 3 ký tự", + "max": "Mật khẩu không được vượt quá 255 ký tự" + }, + "url": { + "required": "URL là bắt buộc", + "invalid": "URL không hợp lệ" + }, + "alias": { + "invalid": "Bí danh phải là chuỗi ký tự", + "min": "Bí danh phải có ít nhất 7 ký tự", + "max": "Bí danh không được vượt quá 255 ký tự" + }, + "expire_time": { + "invalid": "Thời gian hết hạn phải là số", + "min": "Thời gian hết hạn phải lớn hơn 0" + } + }, + + "settings": { + "theme": { + "auto": "Tự động", + "light": "Sáng", + "dark": "Tối" + } + }, + + "counter": "{count} liên kết đã được tạo và truy cập {clicks} lần", + + "hero": { + "title": "Công cụ tất-cả-trong-một mà bạn cần.", + "subtitle_1": "Solitar là nền tảng mã nguồn mở, mạnh mẽ, dùng hằng ngày, giúp bạn rút gọn liên kết dài và tạo mã qr tức thì.", + "subtitle_2": "Solitar mang đến chuyển hướng tức thì không quảng cáo chèn ngang, tự động hóa quy trình với REST API và đặt thời hạn linh hoạt cho liên kết của bạn." + }, + + "not_secure_warning": { + "title": "Kết nối của bạn không an toàn", + "description": "Liên kết bạn sắp truy cập không dùng HTTPS, nghĩa là kết nối của bạn không được mã hóa. Dữ liệu của bạn có thể bị bên thứ ba chặn lấy.", + "enforce_https": "Bắt buộc HTTPS", + "accept_risk": "Chấp nhận rủi ro & tiếp tục" + }, + + "unlock": { + "title": "URL đã bị khóa", + "description": "Liên kết bạn sắp truy cập đã bị người tạo khóa. Vui lòng nhập mật khẩu để mở khóa" + }, + + "button": { + "download": "tải xuống", + "copy_url": "sao chép url", + "close": "đóng", + "unlock": "mở khóa", + "return": "quay lại" + }, + + "error": { + "url_not_found": "Không tìm thấy URL", + "url_expired": "URL này không còn khả dụng", + "url_disabled": "URL này đã bị vô hiệu hóa do vi phạm điều khoản", + "short_code_conflict": "Bí danh này đã tồn tại.", + "password_protected": "Vui lòng nhập mật khẩu hợp lệ để mở khóa URL này", + "incorrect_password": "Mật khẩu được cung cấp không chính xác.", + "default": "Đã xảy ra lỗi" + } +} From e86ffa727284a5b1ce9710fe05465fc657959a53 Mon Sep 17 00:00:00 2001 From: k1enn Date: Tue, 9 Jun 2026 11:22:09 +0000 Subject: [PATCH 2/3] fix(frontend): make i18n labels reactive to locale switch $t() called once in setup froze translated strings until reload. Recompute on locale change instead: - Wrap nav, footer, theme label arrays in computed - Pass regle validation messages as getter functions Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/frontend/src/app/components/Footer.vue | 5 ++-- apps/frontend/src/app/components/Header.vue | 27 +++++++------------ .../src/app/components/form/ShortenForm.vue | 20 +++++++------- .../src/app/components/form/UnlockForm.vue | 2 +- .../app/components/setting/ThemeSetting.vue | 5 ++-- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/apps/frontend/src/app/components/Footer.vue b/apps/frontend/src/app/components/Footer.vue index b72673b..7a0505a 100644 --- a/apps/frontend/src/app/components/Footer.vue +++ b/apps/frontend/src/app/components/Footer.vue @@ -19,7 +19,8 @@ const socialLinks: SocialLink[] = [ }, ]; -const footerItems: FooterColumn[] = [ +// Recompute labels on locale change — setLocale() does not re-run setup. +const footerItems = computed(() => [ { title: $t("footer.column.about"), items: [ @@ -63,7 +64,7 @@ const footerItems: FooterColumn[] = [ }, ], }, -]; +]);