From 09c906501cdf27a371a7b2b41c7ac6e01c302dee Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Thu, 30 Jul 2026 14:07:34 +0700 Subject: [PATCH 1/4] feat: add Vietnamese language support and update language switcher --- .../web/src/components/LanguageSwitcher.tsx | 1 + packages/web/src/locales/de/common.json | 3 +- packages/web/src/locales/en/common.json | 3 +- packages/web/src/locales/es/common.json | 3 +- packages/web/src/locales/fr/common.json | 3 +- packages/web/src/locales/it/common.json | 3 +- packages/web/src/locales/ja/common.json | 3 +- packages/web/src/locales/vi/common.json | 22 +++++++++ packages/web/src/locales/vi/errors.json | 45 +++++++++++++++++ packages/web/src/locales/vi/game.json | 30 ++++++++++++ packages/web/src/locales/vi/manager.json | 45 +++++++++++++++++ packages/web/src/locales/vi/quizz.json | 48 +++++++++++++++++++ 12 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 packages/web/src/locales/vi/common.json create mode 100644 packages/web/src/locales/vi/errors.json create mode 100644 packages/web/src/locales/vi/game.json create mode 100644 packages/web/src/locales/vi/manager.json create mode 100644 packages/web/src/locales/vi/quizz.json diff --git a/packages/web/src/components/LanguageSwitcher.tsx b/packages/web/src/components/LanguageSwitcher.tsx index 3d598e876..1ed8bc225 100644 --- a/packages/web/src/components/LanguageSwitcher.tsx +++ b/packages/web/src/components/LanguageSwitcher.tsx @@ -15,6 +15,7 @@ const LANGUAGES = [ { code: "fr", label: "common:language.fr" }, { code: "it", label: "common:language.it" }, { code: "ja", label: "common:language.ja" }, + { code: "vi", label: "common:language.vi" }, ] const LanguageSwitcher = () => { diff --git a/packages/web/src/locales/de/common.json b/packages/web/src/locales/de/common.json index 9213c1747..9da938e1d 100644 --- a/packages/web/src/locales/de/common.json +++ b/packages/web/src/locales/de/common.json @@ -10,7 +10,8 @@ "es": "Español - Spanisch", "fr": "Français - Französisch", "it": "Italiano - Italienisch", - "ja": "日本語 - Japanisch" + "ja": "日本語 - Japanisch", + "vi": "Tiếng Việt - Vietnamesisch" }, "loading": "Laden...", "next": "Weiter", diff --git a/packages/web/src/locales/en/common.json b/packages/web/src/locales/en/common.json index 3c8ca967a..c4efa8e2f 100644 --- a/packages/web/src/locales/en/common.json +++ b/packages/web/src/locales/en/common.json @@ -10,7 +10,8 @@ "es": "Español - Spanish", "fr": "Français - French", "it": "Italiano - Italian", - "ja": "日本語 - Japanese" + "ja": "日本語 - Japanese", + "vi": "Tiếng Việt - Vietnamese" }, "loading": "Loading...", "next": "Next", diff --git a/packages/web/src/locales/es/common.json b/packages/web/src/locales/es/common.json index a5e0e1100..3ef693f1e 100644 --- a/packages/web/src/locales/es/common.json +++ b/packages/web/src/locales/es/common.json @@ -10,7 +10,8 @@ "es": "Español - Español", "fr": "Français - Francés", "it": "Italiano - Italiano", - "ja": "日本語 - Japonés" + "ja": "日本語 - Japonés", + "vi": "Tiếng Việt - Vietnamita" }, "loading": "Cargando...", "next": "Siguiente", diff --git a/packages/web/src/locales/fr/common.json b/packages/web/src/locales/fr/common.json index 2708b298b..d8fc14b18 100644 --- a/packages/web/src/locales/fr/common.json +++ b/packages/web/src/locales/fr/common.json @@ -10,7 +10,8 @@ "es": "Español - Espagnol", "fr": "Français - Français", "it": "Italiano - Italien", - "ja": "日本語 - Japonais" + "ja": "日本語 - Japonais", + "vi": "Tiếng Việt - Vietnamien" }, "loading": "Chargement...", "next": "Suivant", diff --git a/packages/web/src/locales/it/common.json b/packages/web/src/locales/it/common.json index aecb0fb41..b1e24d2ed 100644 --- a/packages/web/src/locales/it/common.json +++ b/packages/web/src/locales/it/common.json @@ -10,7 +10,8 @@ "es": "Español - Spagnolo", "fr": "Français - Francese", "it": "Italiano - Italiano", - "ja": "日本語 - Giapponese" + "ja": "日本語 - Giapponese", + "vi": "Tiếng Việt - Vietnamita" }, "loading": "Caricamento...", "next": "Avanti", diff --git a/packages/web/src/locales/ja/common.json b/packages/web/src/locales/ja/common.json index 4d60471e2..0bd931928 100644 --- a/packages/web/src/locales/ja/common.json +++ b/packages/web/src/locales/ja/common.json @@ -10,7 +10,8 @@ "es": "Español - スペイン語", "fr": "Français - フランス語", "it": "Italiano - イタリア語", - "ja": "日本語 - 日本語" + "ja": "日本語 - 日本語", + "vi": "Tiếng Việt - ベトナム語" }, "loading": "読み込み中...", "next": "次へ", diff --git a/packages/web/src/locales/vi/common.json b/packages/web/src/locales/vi/common.json new file mode 100644 index 000000000..ba6f3d5f5 --- /dev/null +++ b/packages/web/src/locales/vi/common.json @@ -0,0 +1,22 @@ +{ + "cancel": "Hủy", + "confirm": "Xác nhận", + "connecting": "Đang kết nối...", + "delete": "Xóa", + "exit": "Thoát", + "language": { + "de": "Deutsch - Tiếng Đức", + "en": "English - Tiếng Anh", + "es": "Español - Tiếng Tây Ban Nha", + "fr": "Français - Tiếng Pháp", + "it": "Italiano - Tiếng Ý", + "ja": "日本語 - Tiếng Nhật", + "vi": "Tiếng Việt - Vietnamese" + }, + "loading": "Đang tải...", + "next": "Tiếp", + "save": "Lưu", + "seconds": "giây", + "skip": "Bỏ qua", + "submit": "Gửi" +} \ No newline at end of file diff --git a/packages/web/src/locales/vi/errors.json b/packages/web/src/locales/vi/errors.json new file mode 100644 index 000000000..3ebaf3096 --- /dev/null +++ b/packages/web/src/locales/vi/errors.json @@ -0,0 +1,45 @@ +{ + "auth": { + "invalidInviteCode": "Mã tham dự không hợp lệ", + "usernameTooLong": "Tên người dùng không được vượt quá 20 ký tự", + "usernameTooShort": "Tên người dùng không được ngắn hơn 4 ký tự" + }, + "game": { + "expired": "Trò chơi đã hết hạn", + "kickedByManager": "Bạn đã bị quản trị viên mời ra khỏi phòng", + "managerAlreadyConnected": "Quản trị viên đã kết nối", + "managerDisconnected": "Quản trị viên đã ngắt kết nối", + "noPlayersConnected": "Không có người chơi nào kết nối", + "managerCannotJoin": "Bạn không thể tham gia trò chơi của chính mình", + "notFound": "Không tìm thấy trò chơi", + "playerAlreadyConnected": "Người chơi đã kết nối" + }, + "manager": { + "failedToReadConfig": "Không đọc được cấu hình trò chơi", + "invalidPassword": "Mật khẩu không hợp lệ", + "passwordNotConfigured": "Mật khẩu quản trị viên chưa được cấu hình" + }, + "notFound": { + "back": "Về trang chủ", + "description": "Trang bạn đang tìm không tồn tại.", + "title": "Không tìm thấy trang" + }, + "quizz": { + "answerEmpty": "Đáp án không được để trống", + "failedToDelete": "Xóa trò chơi thất bại", + "failedToSave": "Trò chơi chưa hoàn chỉnh, hãy đảm bảo mọi câu hỏi đều có tiêu đề và ít nhất một đáp án đúng", + "failedToUpdate": "Cập nhật trò chơi thất bại", + "invalidMediaUrl": "URL media phải là một URL hợp lệ", + "noQuestions": "Trò chơi phải có ít nhất một câu hỏi", + "notFound": "Không tìm thấy trò chơi", + "questionEmpty": "Câu hỏi không được để trống", + "subjectEmpty": "Chủ đề không được để trống", + "tooFewAnswers": "Một câu hỏi phải có ít nhất 2 câu trả lời", + "tooManyAnswers": "Một câu hỏi không được có quá 4 câu trả lời" + }, + "route": { + "back": "Về trang chủ", + "description": "Đã xảy ra lỗi không mong muốn. Vui lòng thử lại.", + "title": "Đã có lỗi xảy ra" + } +} \ No newline at end of file diff --git a/packages/web/src/locales/vi/game.json b/packages/web/src/locales/vi/game.json new file mode 100644 index 000000000..8cdb64ed1 --- /dev/null +++ b/packages/web/src/locales/vi/game.json @@ -0,0 +1,30 @@ +{ + "correct": "Chính xác!", + "gamePinLabel": "Mã PIN trò chơi:", + "hud": { + "answers": "Câu trả lời", + "time": "Thời gian" + }, + "joinInstruction": "Tham gia trò chơi tại", + "leaderboard": "Bảng xếp hạng", + "pinLabel": "Mã PIN", + "reconnect": "Kết nối lại", + "reconnectTitle": "Bạn đang ở trong một trò chơi", + "playersJoined": "Người chơi đã tham gia: ", + "questionPrefix": "Câu hỏi #", + "rank": { + "1": "Top 1", + "2": "Top 2", + "3": "Top 3", + "other": "Top {{rank}}" + }, + "rankSuffix": "", + "resultBehind": ", sau ", + "resultTop": "Bạn xếp ", + "startGame": "Bắt đầu trò chơi", + "usernamePlaceholder": "Nhập tên người dùng", + "waitingForAnswers": "Đang chờ người chơi trả lời", + "waitingForPlayers": "Đang chờ người chơi", + "wrong": "Sai rồi", + "confirm": "Xác nhận" +} \ No newline at end of file diff --git a/packages/web/src/locales/vi/manager.json b/packages/web/src/locales/vi/manager.json new file mode 100644 index 000000000..eb177ff62 --- /dev/null +++ b/packages/web/src/locales/vi/manager.json @@ -0,0 +1,45 @@ +{ + "configurationsTitle": "Cấu hình", + "logout": "Đăng xuất", + "passwordPlaceholder": "Mật khẩu", + "quizz": { + "create": "Tạo trò chơi", + "delete": "Xóa trò chơi", + "deleteConfirm": "Bạn có chắc muốn xóa \"{{name}}\"? Trò chơi này sẽ bị xóa vĩnh viễn.", + "deleted": "Đã xóa trò chơi", + "export": "Xuất trò chơi dạng JSON", + "import": "Nhập trò chơi từ JSON", + "none": "Chưa có trò chơi nào", + "notFound": "Không tìm thấy trò chơi", + "pleaseCreate": "Vui lòng tạo trò chơi trước", + "pleaseSelect": "Vui lòng chọn một trò chơi", + "startGame": "Bắt đầu trò chơi" + }, + "result": { + "delete": "Xóa kết quả", + "deleteConfirm": "Xóa kết quả của \"{{name}}\"? Kết quả này sẽ bị xóa vĩnh viễn.", + "deleted": "Đã xóa kết quả", + "noAnswer": "Không có câu trả lời", + "none": "Chưa có kết quả, hoàn thành một trò chơi để xem báo cáo tại đây", + "paginationOf": " / ", + "stats": { + "correctAnswers": "Câu trả lời đúng", + "playersAnswered": "Người chơi đã trả lời" + }, + "table": { + "answered": "Đã trả lời", + "correct": " Đúng", + "correctIncorrect": "Đúng / Sai", + "incorrect": " Sai", + "player": "Người chơi", + "points": "Điểm" + }, + "playerCount": "{{count}} người chơi", + "timeLimitSuffix": "s giới hạn thời gian" + }, + "tabs": { + "play": "Trò chơi", + "quizz": "Cấu hình", + "results": "Kết quả" + } +} \ No newline at end of file diff --git a/packages/web/src/locales/vi/quizz.json b/packages/web/src/locales/vi/quizz.json new file mode 100644 index 000000000..1bd76fd12 --- /dev/null +++ b/packages/web/src/locales/vi/quizz.json @@ -0,0 +1,48 @@ +{ + "addAnswerPlaceholder": "Thêm câu trả lời...", + "addQuestion": "Thêm câu hỏi", + "answersCountSuffix": " câu trả lời", + "deleteQuestion": "Xóa câu hỏi", + "noQuestionYet": "Chưa có câu hỏi", + "question": { + "addMediaHint": "Thêm ảnh, video hoặc âm thanh vào câu hỏi", + "config": { + "answerTime": "Thời gian trả lời", + "answerTimeHint": "Thời gian người chơi có để trả lời", + "noTimeLimit": "Không giới hạn thời gian", + "noTimeLimitHint": "Điểm được tính theo thứ tự trả lời. Không có giới hạn thời gian để trả lời.", + "questionDisplay": "Hiển thị câu hỏi", + "questionDisplayHint": "Thời gian trước khi hiện câu trả lời", + "timings": "Thời gian", + "answerMode": "Chế độ trả lời", + "scoring": "Tính điểm", + "maxPoints": "Điểm tối đa", + "maxPointsHint": "Điểm tối đa khi trả lời đúng", + "penalty": "Phạt", + "penaltyHint": "Điểm bị trừ khi chọn đáp án sai (tổng điểm không dưới 0)", + "scoringMode": "Chế độ tính điểm", + "scoringMode.strict": "Nghiêm ngặt", + "scoringMode.balanced": "Cân bằng", + "scoringMode.lenient": "Dễ dàng", + "scoringModeHint.strict": "Điểm đầy đủ chỉ khi chọn đủ đáp án đúng và không chọn đáp án sai", + "scoringModeHint.balanced": "Điểm một phần: đáp án đúng, bị phạt đáp án sai", + "scoringModeHint.lenient": "Điểm theo từng đáp án đúng, không phạt đáp án sai" + }, + "deleteQuestion": "Xóa câu hỏi", + "deleteQuestionConfirm": "Bạn có chắc muốn xóa câu hỏi này?", + "media": { + "audio": "Âm thanh", + "image": "Hình ảnh", + "video": "Video" + }, + "mediaUrlPlaceholder": "Nhập URL...", + "placeholder": "Nhập câu hỏi của bạn..." + }, + "quizzSaved": "Đã lưu trò chơi thành công", + "quizzUpdated": "Đã cập nhật trò chơi thành công", + "questionType": { + "single": "Chọn một đáp án", + "multi": "Chọn nhiều đáp án" + }, + "titleQuizzPlaceholder": "Tiêu đề trò chơi..." +} \ No newline at end of file From 23b2ae89f357bfec2a926f76913b5b166cac2111 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Fri, 31 Jul 2026 10:21:19 +0700 Subject: [PATCH 2/4] fix: update rank key for game results to use correct namespace --- packages/web/src/features/game/components/states/Result.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/features/game/components/states/Result.tsx b/packages/web/src/features/game/components/states/Result.tsx index 077f85a69..93999d18c 100644 --- a/packages/web/src/features/game/components/states/Result.tsx +++ b/packages/web/src/features/game/components/states/Result.tsx @@ -21,7 +21,7 @@ const Result = ({ 2: "game:rank.2", 3: "game:rank.3", } - const rankKey = rankKeyMap[rank] ?? "rank.other" + const rankKey = rankKeyMap[rank] ?? "game.rank.other" const [sfxResults] = useSound(SFX.RESULTS_SOUND, { volume: 0.2, From f4ace26a6315b05b454164641d98a4813c68b8d1 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Fri, 31 Jul 2026 10:32:42 +0700 Subject: [PATCH 3/4] fix: adjust text wrapping styles in AnswerButton and Room components --- packages/web/src/features/game/components/AnswerButton.tsx | 2 +- packages/web/src/features/game/components/states/Room.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/features/game/components/AnswerButton.tsx b/packages/web/src/features/game/components/AnswerButton.tsx index 674362116..990915738 100644 --- a/packages/web/src/features/game/components/AnswerButton.tsx +++ b/packages/web/src/features/game/components/AnswerButton.tsx @@ -30,7 +30,7 @@ const AnswerButton = ({ {label} -

+

{children}

{correct !== undefined && ( diff --git a/packages/web/src/features/game/components/states/Room.tsx b/packages/web/src/features/game/components/states/Room.tsx index 63cd2edc9..3b9d8f368 100644 --- a/packages/web/src/features/game/components/states/Room.tsx +++ b/packages/web/src/features/game/components/states/Room.tsx @@ -66,7 +66,7 @@ const Room = ({ data: { text, inviteCode } }: Props) => {

{t("game:joinInstruction")}

-

+

{webUrl}

From 01a270a49ecf961f709d41809eb74947cd6705b3 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Fri, 31 Jul 2026 10:52:56 +0700 Subject: [PATCH 4/4] fix: correct namespace for rank key in game results --- packages/web/src/features/game/components/states/Result.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/features/game/components/states/Result.tsx b/packages/web/src/features/game/components/states/Result.tsx index 93999d18c..844587347 100644 --- a/packages/web/src/features/game/components/states/Result.tsx +++ b/packages/web/src/features/game/components/states/Result.tsx @@ -21,7 +21,7 @@ const Result = ({ 2: "game:rank.2", 3: "game:rank.3", } - const rankKey = rankKeyMap[rank] ?? "game.rank.other" + const rankKey = rankKeyMap[rank] ?? "game:rank.other" const [sfxResults] = useSound(SFX.RESULTS_SOUND, { volume: 0.2,