diff --git a/api/src/__tests__/email-service.test.ts b/api/src/__tests__/email-service.test.ts index 162ac63..105e15b 100644 --- a/api/src/__tests__/email-service.test.ts +++ b/api/src/__tests__/email-service.test.ts @@ -1,4 +1,4 @@ -import { getEmailServiceStatus, initializeEmailService, escapeHtml } from '../shared/email-service' +import { getEmailServiceStatus, initializeEmailService, escapeHtml, generateOrganizerEmailContent } from '../shared/email-service' // Mock the EmailClient jest.mock('@azure/communication-email', () => ({ @@ -108,4 +108,25 @@ describe('Email Service', () => { expect(escapeHtml('')).toBe('') }) }) + + describe('Vietnamese language support', () => { + it('should gracefully fallback to English email copy for unsupported Vietnamese templates', () => { + const game = { + code: '123456', + name: 'Holiday Exchange', + date: '2026-12-24', + location: 'Main Office', + amount: '20', + organizerToken: 'org-token', + participants: [{ id: '1' }, { id: '2' }, { id: '3' }] + } as any + + const viContent = generateOrganizerEmailContent({ game, language: 'vi' }) + const enContent = generateOrganizerEmailContent({ game, language: 'en' }) + + expect(viContent.subject).toBe(enContent.subject) + expect(viContent.html).toBe(enContent.html) + expect(viContent.plainText).toBe(enContent.plainText) + }) + }) }) diff --git a/api/src/shared/email-service.ts b/api/src/shared/email-service.ts index 4a22722..11a0782 100644 --- a/api/src/shared/email-service.ts +++ b/api/src/shared/email-service.ts @@ -15,12 +15,12 @@ export function escapeHtml(text: string | undefined): string { } // Helper function to get translation with fallback to English -function getTranslation>(translations: T, language: Language): T[Language] { - if (language in translations) { - return translations[language] +function getTranslation(translations: Partial>, language: Language): T { + const translation = translations[language] ?? translations.en + if (!translation) { + throw new Error(`English email translation is missing for language "${language}"`) } - // Fallback to English if translation not available - return translations['en'] + return translation } // Email notification types @@ -179,13 +179,13 @@ export function generateOrganizerEmailContent(data: GameEmailTemplateData): { su const participantLink = hasUrl ? getParticipantLink(baseUrl, game.code) : '' const organizerLink = hasUrl ? getOrganizerLink(baseUrl, game.code, game.organizerToken) : '' - const translations: Record = { + }>> = { es: { subject: `🎁 Tu juego de Zava Gift Exchange "${game.name}" ha sido creado`, greeting: '¡Hola!', @@ -368,7 +368,7 @@ export function generateOrganizerEmailContent(data: GameEmailTemplateData): { su } } - const t = translations[language] + const t = getTranslation(translations, language) const currencySymbol = getCurrencySymbol(game.currency) const html = ` @@ -473,13 +473,13 @@ export function generateParticipantEmailContent(data: ParticipantEmailTemplateDa const hasUrl = hasBaseUrl() const participantLink = hasUrl ? getParticipantLink(baseUrl, game.code) : '' - const translations: Record = { + }>> = { es: { subject: `🎁 Tu asignación de Zava Gift Exchange para "${game.name}"`, greeting: `¡Hola ${participant.name}!`, @@ -698,7 +698,7 @@ export function generateParticipantEmailContent(data: ParticipantEmailTemplateDa } } - const t = translations[language] + const t = getTranslation(translations, language) const currencySymbol = getCurrencySymbol(game.currency) const html = ` @@ -853,7 +853,7 @@ export function generateOrganizerRecoveryEmailContent(data: OrganizerRecoveryEma const organizerLink = getOrganizerLink(baseUrl, game.code, game.organizerToken) const hasUrl = baseUrl !== '' - const translations: Record = { + }>> = { es: { subject: `🔑 Recuperación de enlace - "${game.name}"`, greeting: '¡Hola!', @@ -995,7 +995,7 @@ export function generateOrganizerRecoveryEmailContent(data: OrganizerRecoveryEma } } - const t = translations[language] || translations.es + const t = getTranslation(translations, language) const html = ` @@ -1092,7 +1092,7 @@ export function generateParticipantRecoveryEmailContent(data: ParticipantRecover const hasUrl = baseUrl !== '' const hasToken = !!participant.token - const translations: Record = { + }>> = { es: { subject: `🔑 Recuperación de enlace - "${game.name}"`, greeting: `¡Hola ${participant.name}!`, @@ -1244,7 +1244,7 @@ export function generateParticipantRecoveryEmailContent(data: ParticipantRecover } } - const t = translations[language] || translations.es + const t = getTranslation(translations, language) // If game is not protected, participant doesn't have a token const showLink = hasUrl && hasToken @@ -1397,11 +1397,11 @@ export function generateParticipantConfirmedEmailContent(data: ParticipantConfir const baseUrl = getBaseUrl() const organizerLink = getOrganizerLink(baseUrl, game.code, game.organizerToken) - const translations: Record = { + }>> = { es: { subject: `✅ ${participant.name} ha confirmado su asignación en "${game.name}"`, greeting: '¡Hola Organizador!', @@ -1503,7 +1503,7 @@ export function generateParticipantConfirmedEmailContent(data: ParticipantConfir } } - const t = translations[language] + const t = getTranslation(translations, language) const confirmedCount = game.participants.filter(p => p.hasConfirmedAssignment).length const totalCount = game.participants.length @@ -1576,10 +1576,10 @@ export function generateReassignmentRequestedEmailContent(data: ReassignmentRequ const baseUrl = getBaseUrl() const organizerLink = getOrganizerLink(baseUrl, game.code, game.organizerToken) - const translations: Record = { + }>> = { es: { subject: `🔄 ${participant.name} solicita una nueva asignación en "${game.name}"`, greeting: '¡Hola Organizador!', @@ -1681,7 +1681,7 @@ export function generateReassignmentRequestedEmailContent(data: ReassignmentRequ } } - const t = translations[language] + const t = getTranslation(translations, language) const pendingCount = game.reassignmentRequests?.length || 0 const html = ` @@ -1755,10 +1755,10 @@ export interface ReassignmentResultEmailData { export function generateReassignmentResultEmailContent(data: ReassignmentResultEmailData): { subject: string; html: string; plainText: string } { const { game, participant, approved, newReceiver, language } = data - const translations: Record = { + }>> = { es: { subjectApproved: `✅ Tu solicitud de reasignación fue aprobada - "${game.name}"`, subjectRejected: `❌ Tu solicitud de reasignación fue rechazada - "${game.name}"`, @@ -1860,7 +1860,7 @@ export function generateReassignmentResultEmailContent(data: ReassignmentResultE } } - const t = translations[language] + const t = getTranslation(translations, language) const subject = approved ? t.subjectApproved : t.subjectRejected const html = ` @@ -1942,10 +1942,10 @@ export interface WishUpdatedEmailData { export function generateWishUpdatedEmailContent(data: WishUpdatedEmailData): { subject: string; html: string; plainText: string } { const { game, giver, receiver, language } = data - const translations: Record = { + }>> = { es: { subject: `💡 ${receiver.name} actualizó su lista de deseos - "${game.name}"`, greeting: `¡Hola ${giver.name}!`, @@ -2029,7 +2029,7 @@ export function generateWishUpdatedEmailContent(data: WishUpdatedEmailData): { s } } - const t = translations[language] + const t = getTranslation(translations, language) const html = ` @@ -2104,11 +2104,11 @@ export function generateEventDetailsChangedEmailContent(data: EventDetailsChange const baseUrl = getBaseUrl() const participantLink = getParticipantLink(baseUrl, game.code) - const translations: Record = { + }>> = { es: { subject: `📝 Los detalles del evento han cambiado - "${game.name}"`, greeting: recipientName ? `¡Hola ${recipientName}!` : '¡Hola!', @@ -2246,7 +2246,7 @@ export function generateEventDetailsChangedEmailContent(data: EventDetailsChange } } - const t = translations[language] + const t = getTranslation(translations, language) let changesHtml = '' let changesText = '' @@ -2367,12 +2367,12 @@ export function generateReminderEmailContent(data: ReminderEmailData, recipientN const participantLink = getParticipantLink(baseUrl, game.code) const currencySymbol = getCurrencySymbol(game.currency) - const translations: Record = { + }>> = { es: { subject: `🔔 Recordatorio: Zava Gift Exchange "${game.name}"`, greeting: `¡Hola ${recipientName}!`, @@ -2528,7 +2528,7 @@ export function generateReminderEmailContent(data: ReminderEmailData, recipientN } } - const t = translations[language] + const t = getTranslation(translations, language) const html = ` @@ -2643,11 +2643,11 @@ export function generateParticipantInvitationEmailContent(data: ParticipantInvit const participantLink = getParticipantLink(baseUrl, game.code) const currencySymbol = getCurrencySymbol(game.currency) - const translations: Record = { + }>> = { es: { subject: `🎁 Has sido invitado al Zava Gift Exchange "${game.name}"`, greeting: `¡Hola ${participant.name}!`, @@ -2785,7 +2785,7 @@ export function generateParticipantInvitationEmailContent(data: ParticipantInvit } } - const t = translations[language] + const t = getTranslation(translations, language) const html = ` @@ -2867,11 +2867,11 @@ export function generateFullReassignmentEmailContent(data: FullReassignmentEmail const hasUrl = hasBaseUrl() const participantLink = hasUrl ? getParticipantLink(baseUrl, game.code) : '' - const translations: Record = { + }>> = { es: { subject: `🔄 Nueva asignación en "${game.name}" - Reasignación del organizador`, greeting: `¡Hola ${participant.name}!`, @@ -3018,7 +3018,7 @@ export function generateFullReassignmentEmailContent(data: FullReassignmentEmail } } - const t = translations[language] + const t = getTranslation(translations, language) const html = ` @@ -3158,7 +3158,7 @@ export interface ParticipantRemovedEmailData { export function generateParticipantRemovedEmailContent(data: ParticipantRemovedEmailData): { subject: string; html: string; plainText: string } { const { gameName, participantName, organizerName, language } = data - const translations: Record = { + }>> = { es: { subject: `🎄 Has sido eliminado del juego "${gameName}"`, greeting: `Hola ${participantName},`, @@ -3336,7 +3336,7 @@ export interface GameDeletedEmailData { export function generateGameDeletedEmailContent(data: GameDeletedEmailData): { subject: string; html: string; plainText: string } { const { gameName, participantName, eventDate, organizerName, language } = data - const translations: Record = { + }>> = { es: { subject: `❌ El intercambio "${gameName}" ha sido cancelado`, greeting: `Hola ${participantName},`, @@ -3573,7 +3573,7 @@ export function generateEventUpcomingEmailContent(data: EventUpcomingEmailData): const assignment = game.assignments.find(a => a.giverId === participant.id) const receiver = assignment ? game.participants.find(p => p.id === assignment.receiverId) : null - const translations: Record = { + }>> = { es: { subject: `⏰ ¡Recordatorio! "${game.name}" es mañana`, greeting: `¡Hola ${participant.name}!`, @@ -3877,7 +3877,7 @@ export function generateAllConfirmedEmailContent(data: AllConfirmedEmailData): { const confirmedCount = game.participants.filter(p => p.hasConfirmedAssignment).length - const translations: Record = { + }>> = { es: { subject: `✅ ¡Todos confirmados! "${game.name}" está listo`, greeting: '¡Hola Organizador!', @@ -4118,7 +4118,7 @@ export function generateNewOrganizerLinkEmailContent(data: NewOrganizerLinkEmail const hasUrl = hasBaseUrl() const organizerLink = hasUrl ? getOrganizerLink(baseUrl, game.code, game.organizerToken) : '' - const translations: Record = { + }>> = { es: { subject: `🔐 Nuevo enlace de organizador - "${game.name}"`, greeting: '¡Hola Organizador!', @@ -4332,7 +4332,7 @@ export async function sendNewOrganizerLinkEmail( export function generateEmailUpdatedEmailContent(data: EmailUpdatedEmailData): { subject: string; html: string; plainText: string } { const { gameName, participantName, oldEmail, newEmail, language } = data - const translations: Record = { + }>> = { es: { subject: `🔔 Tu email ha sido actualizado - "${gameName}"`, greeting: `Hola ${participantName},`, diff --git a/api/src/shared/types.ts b/api/src/shared/types.ts index 13f3d36..552ebd9 100644 --- a/api/src/shared/types.ts +++ b/api/src/shared/types.ts @@ -1,5 +1,5 @@ // Supported languages for email notifications -export type Language = 'en' | 'es' | 'pt' | 'fr' | 'it' | 'ja' | 'zh' | 'de' | 'nl' +export type Language = 'en' | 'es' | 'pt' | 'fr' | 'it' | 'ja' | 'ko' | 'zh' | 'de' | 'nl' | 'vi' // Input length limits to prevent abuse and stay within Cosmos DB 2MB item limit export const INPUT_LIMITS = { diff --git a/src/lib/translations/README.md b/src/lib/translations/README.md index c19c672..130730a 100644 --- a/src/lib/translations/README.md +++ b/src/lib/translations/README.md @@ -16,6 +16,7 @@ Each language has its own file: - `zh.ts` - Chinese (中文) - `de.ts` - German (Deutsch) - `nl.ts` - Dutch (Nederlands) +- `vi.ts` - Vietnamese (Tiếng Việt) The `index.ts` file aggregates all translations and exports them as a single object. diff --git a/src/lib/translations/index.ts b/src/lib/translations/index.ts index 1bb4052..5228279 100644 --- a/src/lib/translations/index.ts +++ b/src/lib/translations/index.ts @@ -8,6 +8,7 @@ import { ko } from './ko' import { zh } from './zh' import { de } from './de' import { nl } from './nl' +import { vi } from './vi' export const translations = { en, @@ -19,7 +20,8 @@ export const translations = { ko, zh, de, - nl + nl, + vi } export type TranslationKey = keyof typeof translations.en diff --git a/src/lib/translations/vi.ts b/src/lib/translations/vi.ts new file mode 100644 index 0000000..f5b7047 --- /dev/null +++ b/src/lib/translations/vi.ts @@ -0,0 +1,547 @@ +export const vi = { + appName: "Zava Gift Exchange", + welcome: "Chào mừng!", + welcomeDesc: "Tổ chức trao đổi quà dễ dàng và vui vẻ", + loading: "Đang tải...", + createGame: "Tạo trò chơi mới", + joinGame: "Tham gia trò chơi", + enterCode: "Nhập mã", + codePlaceholder: "Mã 6 chữ số", + continue: "Tiếp tục", + back: "Quay lại", + next: "Tiếp theo", + finish: "Hoàn tất", + cancel: "Hủy", + confirm: "Xác nhận", + yes: "Có", + no: "Không", + + step1: "Chi tiết sự kiện", + step2: "Người tham gia", + step3: "Cấu hình", + + eventName: "Tên sự kiện", + eventNamePlaceholder: "Ví dụ: Giáng Sinh Văn Phòng 2025", + giftAmount: "Giá trị quà", + giftAmountPlaceholder: "Ví dụ: 20", + currency: "Tiền tệ", + selectCurrency: "Chọn tiền tệ", + eventDate: "Ngày sự kiện", + eventTime: "Giờ sự kiện", + eventLocation: "Địa điểm sự kiện", + eventLocationPlaceholder: "Ví dụ: Văn phòng chính", + + participants: "Người tham gia", + participantsDesc: "Thêm ít nhất 3 người tham gia", + participantName: "Tên người tham gia", + participantNamePlaceholder: "Ví dụ: Nguyễn Văn A", + participantEmail: "Email người tham gia", + participantEmailPlaceholder: "Ví dụ: abc@email.com (Tab để tiếp tục)", + participantEmailOptional: "(Tùy chọn - để nhận thông báo)", + yourEmail: "Email của bạn", + yourEmailPlaceholder: "Ví dụ: ban@email.com", + yourEmailDesc: "Tùy chọn - để nhận thông báo sự kiện", + emailUpdated: "Đã cập nhật email!", + tabToAddEmail: "Nhấn Tab để thêm email", + tabToAddGift: "Nhấn Tab để thêm quà", + tabToAddAnother: "Tab/Enter để thêm mục khác", + tabToFinish: "Tab/Enter để hoàn tất", + pressTabOrEnter: "Nhấn Tab hoặc Enter", + desiredGift: "Quà mong muốn", + desiredGiftPlaceholder: "Ví dụ: Sách, trò chơi điện tử, quần áo...", + desiredGiftOptional: "(Tùy chọn)", + theirDesiredGift: "Quà họ muốn:", + addParticipant: "Thêm người tham gia", + removeParticipant: "Xóa", + minParticipants: "Cần ít nhất 3 người tham gia", + organizerNote: "Lưu ý: Nếu bạn muốn tham gia, bạn cần thêm chính mình vào danh sách người tham gia!", + regenerateToken: "Tạo lại liên kết", + regenerateTokenTitle: "Tạo lại liên kết người tham gia?", + regenerateTokenDesc: "Một liên kết mới duy nhất sẽ được tạo cho người tham gia này. Liên kết cũ sẽ không còn hoạt động.", + regenerateTokenConfirm: "Tạo liên kết mới", + tokenRegenerated: "Đã tạo liên kết mới!", + + allowReassignment: "Cho phép đổi phân công", + allowReassignmentDesc: "Người tham gia có thể yêu cầu phân công mới (người tổ chức phải duyệt)", + protectParticipants: "Bảo vệ danh sách người tham gia", + protectParticipantsDesc: "Mỗi người tham gia nhận một liên kết riêng. Họ sẽ không thấy dữ liệu của người khác.", + protectParticipantsWarning: "⚠️ Bạn cần chia sẻ từng liên kết riêng cho từng người tham gia", + generalNotes: "Ghi chú và hướng dẫn chung", + generalNotesPlaceholder: "Ví dụ: Mang quà đã gói. Đừng chi quá mức đã thống nhất.", + + // Email notification settings + emailNotifications: "Thông báo email", + emailNotificationsDesc: "Nhận chi tiết trò chơi qua email", + organizerEmail: "Email của bạn (Người tổ chức)", + organizerEmailPlaceholder: "Ví dụ: ban@email.com", + organizerEmailDesc: "Bạn sẽ nhận liên kết trò chơi và bảng điều khiển người tổ chức", + sendEmailsOnCreate: "Gửi email khi tạo trò chơi", + sendEmailsOnCreateDesc: "Tự động gửi chi tiết cho tất cả người tham gia có email", + sendEmailNotifications: "Gửi thông báo email", + sendEmailNotificationsDesc: "Gửi email cho người tổ chức và người tham gia khi tạo trò chơi", + creating: "Đang tạo...", + emailSentToOrganizer: "Đã gửi email cho người tổ chức!", + emailsSentToParticipants: "Đã gửi {count} email cho người tham gia", + + gameCreated: "Đã tạo trò chơi!", + gameCreatedDesc: "Chia sẻ liên kết này với tất cả người tham gia", + gameCode: "Mã trò chơi", + copyCode: "Sao chép mã", + codeCopied: "Đã sao chép mã!", + participantLink: "Liên kết người tham gia", + participantLinkDesc: "Chia sẻ liên kết này với tất cả người tham gia trao đổi quà", + individualParticipantLinks: "Liên kết cá nhân của người tham gia", + individualParticipantLinksDesc: "Mỗi người tham gia có một liên kết riêng. Chỉ chia sẻ đúng liên kết cho đúng người.", + copyParticipantLink: "Sao chép liên kết", + participantLinkCopied: "Đã sao chép liên kết!", + shareOnSocialMedia: "Chia sẻ trên mạng xã hội", + shareMessageTitle: "Tham gia Zava Gift Exchange của chúng tôi!", + shareMessage: "Chào bạn! Hãy tham gia Zava Gift Exchange \"{eventName}\" của chúng tôi. Nhấn vào đây: {link}", + shareOnWhatsApp: "Chia sẻ trên WhatsApp", + shareOnFacebook: "Chia sẻ trên Facebook", + shareOnX: "Chia sẻ trên X", + shareOnMessenger: "Chia sẻ trên Messenger", + organizerLink: "Liên kết người tổ chức", + copyOrganizerLink: "Sao chép liên kết", + linkCopied: "Đã sao chép liên kết!", + copyNewLink: "Sao chép liên kết mới", + done: "Xong", + organizerLinkDesc: "Lưu liên kết này để xem thống kê và quản lý sự kiện", + emailNotConfiguredSaveLink: "Dịch vụ email chưa được cấu hình. Hãy lưu liên kết này để quản lý sự kiện", + useOrganizerPanelLink: "Dùng liên kết này để truy cập bảng điều khiển người tổ chức", + emailAlreadyExists: "Email này đã được dùng bởi người tham gia khác", + invalidEmailFormat: "Vui lòng nhập địa chỉ email hợp lệ", + invalidDate: "Ngày không hợp lệ. Vui lòng nhập ngày hợp lệ trong lịch (ví dụ: không phải 31/2 hoặc 31/4)", + participantNameExists: "Tên người tham gia này đã tồn tại", + goToGame: "Đến bảng điều khiển người tổ chức", + + // Invitation link + invitationLink: "Liên kết mời", + invitationLinkDesc: "Chia sẻ liên kết này để người mới tham gia trò chơi", + copyInvitationLink: "Sao chép liên kết mời", + invitationLinkCopied: "Đã sao chép liên kết mời!", + joinViaInvitation: "Tham gia qua lời mời", + joinGameTitle: "Tham gia Zava Gift Exchange", + joinGameDesc: "Điền thông tin của bạn để tham gia trao đổi quà này", + yourName: "Tên của bạn", + yourNamePlaceholder: "Ví dụ: Nguyễn Văn B", + yourGiftWish: "Mong muốn quà của bạn (Tùy chọn)", + yourGiftWishPlaceholder: "Chi tiết cụ thể về món quà bạn muốn...", + joinGameButton: "Tham gia trò chơi", + joining: "Đang tham gia...", + joinSuccess: "Tham gia thành công!", + joinError: "Không thể tham gia trò chơi", + invalidInvitationToken: "Liên kết mời không hợp lệ hoặc đã hết hạn", + participantNameAlreadyExists: "Tên này đã được dùng trong trò chơi", + emailAlreadyInUse: "Email này đã được dùng trong trò chơi", + + // Email actions + sendOrganizerEmail: "Gửi email cho người tổ chức", + sendOrganizerEmailDesc: "Nhận chi tiết trò chơi qua email", + sendParticipantEmails: "Gửi email cho người tham gia", + sendParticipantEmailsDesc: "Gửi phân công cho người tham gia có email", + emailSent: "Đã gửi email!", + emailsSent: "email đã gửi", + emailsFailed: "email gửi thất bại", + sendingEmail: "Đang gửi email...", + sendingEmails: "Đang gửi email...", + emailServiceNotConfigured: "Dịch vụ email chưa được cấu hình", + noParticipantsWithEmail: "Không có người tham gia nào được cấu hình email", + + selectParticipant: "Chọn tên của bạn", + selectParticipantDesc: "Chọn tên bạn từ danh sách", + invalidCode: "Không tìm thấy mã", + invalidCodeDesc: "Hãy kiểm tra mã với người tổ chức", + + yourAssignment: "Phân công của bạn", + youGiftTo: "Bạn tặng quà cho:", + theirWish: "Mong muốn của họ:", + noWishYet: "Chưa thêm mong muốn", + addYourWish: "Thêm mong muốn của tôi", + yourWish: "Mong muốn quà của bạn", + yourWishPlaceholder: "Ví dụ: Tôi muốn một cuốn sách khoa học viễn tưởng", + saveWish: "Lưu mong muốn", + wishSaved: "Đã lưu mong muốn!", + editWish: "Sửa mong muốn", + wishDescription: "Hãy cho Zava Gift Exchange biết bạn muốn nhận gì", + wishHint: "Mong muốn này sẽ hiển thị cho người tặng quà cho bạn", + skipWish: "Bỏ qua", + saveAndContinue: "Lưu và tiếp tục", + updateAndContinue: "Cập nhật và tiếp tục", + hasWish: "Đã có mong muốn", + noWish: "Không có mong muốn", + noWishAddedYet: "Bạn chưa thêm mong muốn quà tặng", + wishDialogDesc: "Viết món quà bạn muốn nhận", + wishChangeWarningTitle: "Cảnh báo thay đổi mong muốn", + wishChangeWarningDesc: "Người tặng quà cho bạn đã xác nhận phân công. Nếu bạn đổi mong muốn lúc này, họ sẽ được thông báo qua email để xem cập nhật.", + proceedWithChange: "Tiếp tục thay đổi", + wishesAdded: "Mong muốn đã thêm", + giftWish: "Mong muốn quà", + giftWishDesc: "Món quà người này muốn nhận", + + eventDetails: "Chi tiết sự kiện", + amount: "Số tiền", + date: "Ngày", + location: "Địa điểm", + instructions: "Hướng dẫn", + noInstructions: "Không có hướng dẫn thêm", + notSpecified: "Chưa chỉ định", + + requestReassignment: "Yêu cầu phân công mới", + requestReassignmentTitle: "Yêu cầu phân công mới?", + requestReassignmentDesc: "Yêu cầu của bạn sẽ được gửi cho người tổ chức để duyệt.", + cannotReassignConfirmed: "Bạn không thể yêu cầu đổi phân công sau khi đã xác nhận", + reassignmentSuccess: "Đã gửi yêu cầu! Người tổ chức sẽ xem xét yêu cầu của bạn.", + reassignmentUsed: "Bạn đã có một yêu cầu đang chờ", + reassignmentNotAllowed: "Người tổ chức không cho phép đổi phân công", + reassignmentPending: "Yêu cầu đang chờ", + reassignmentPendingDesc: "Người tổ chức sẽ sớm xem xét yêu cầu của bạn.", + + // Organizer reassignment translations + pendingReassignmentRequests: "Yêu cầu đổi phân công đang chờ", + noPendingRequests: "Không có yêu cầu đang chờ", + approveReassignment: "Duyệt", + cancelRequest: "Hủy yêu cầu", + reassignAll: "Phân công lại tất cả", + reassignAllDesc: "Tạo phân công mới chỉ cho người tham gia chưa xác nhận", + confirmedParticipantsLocked: "người tham gia đã xác nhận sẽ giữ nguyên phân công hiện tại", + unconfirmedWillReceiveNew: "người tham gia sẽ nhận phân công mới", + confirmedParticipantsWarning: "người tham gia đã xác nhận phân công", + confirmedParticipantsWarningDesc: "Trạng thái xác nhận của họ sẽ bị xóa khi tạo phân công lại.", + forceReassignParticipant: "Buộc phân công lại", + forceReassignConfirm: "Buộc phân công lại người tham gia?", + forceReassignWarning: "Người tham gia này đã xác nhận phân công.", + forceReassignWarningDesc: "Họ sẽ nhận phân công mới và được thông báo qua email (nếu có cấu hình). Hành động này không thể hoàn tác.", + forceReassignSuccess: "Đã phân công lại người tham gia và gửi thông báo", + approveAllPending: "Duyệt tất cả yêu cầu chờ", + approveAllPendingDesc: "Duyệt tất cả yêu cầu đổi phân công đang chờ. Mỗi người tham gia sẽ nhận phân công mới.", + allPendingApproved: "Tất cả yêu cầu đang chờ đã được duyệt", + reassignmentApproved: "Đã duyệt đổi phân công", + reassignmentFailed: "Không thể đổi phân công: không có hoán đổi hợp lệ. Hãy thử tạo lại tất cả phân công.", + reassignmentFailedSuggestAll: "Không thể đổi phân công riêng cho người này. Quá nhiều người đã xác nhận. Hãy thử dùng 'Phân công lại tất cả'.", + reassignmentRequestCancelled: "Đã hủy yêu cầu", + allReassigned: "Tất cả phân công đã được tạo lại", + requestedAt: "Thời điểm yêu cầu", + hasPendingRequest: "Yêu cầu đang chờ", + reassignParticipant: "Phân công lại", + pendingAssignmentSingular: "chưa có phân công", + pendingAssignmentPlural: "chưa có phân công", + pendingAssignment: "Phân công đang chờ", + pendingAssignmentDesc: "Phân công của bạn đang được chuẩn bị. Người tổ chức sẽ sớm hoàn tất phân công.", + allConfirmedReassignNeeded: "Tất cả người tham gia khác đã xác nhận phân công. Bạn cần dùng 'Phân công lại tất cả' để thêm người mới.", + qrInviteOnly: "Quét để mời người tham gia mới", + qrCodeAriaLabel: "Mã QR lời mời", + + downloadCard: "Tải thẻ", + downloadingCard: "Đang tạo thẻ...", + cardDownloaded: "Đã tải thẻ!", + eventCountdown: "Thời gian đến sự kiện", + addToCalendar: "Thêm vào lịch", + darkMode: "Chế độ tối", + lightMode: "Chế độ sáng", + + organizerPanel: "Bảng điều khiển người tổ chức", + organizerPanelDesc: "Liên kết này chỉ dành cho người tổ chức trò chơi", + organizerOnly: "🔒 Chỉ người tổ chức", + statistics: "Thống kê", + addedWish: "Đã thêm mong muốn", + usedReassignment: "Đã dùng đổi phân công", + editEvent: "Chỉnh sửa sự kiện", + shareCode: "Chia sẻ mã", + gameManagement: "Quản lý trò chơi", + participantStatus: "Trạng thái người tham gia", + hasConfirmed: "Đã xác nhận", + notConfirmed: "Chưa xác nhận", + hasReassigned: "Đã đổi phân công", + hasEmail: "Có email", + noEmail: "Không có email", + addNewParticipant: "Thêm người tham gia mới", + confirmAssignment: "Xác nhận phân công", + confirmAssignmentDesc: "Xác nhận rằng bạn đã nhận phân công và sẵn sàng tham gia", + assignmentConfirmed: "Đã xác nhận phân công!", + assignmentsConfirmed: "Các phân công đã xác nhận", + removeParticipantConfirm: "Xóa người tham gia?", + participantRemoved: "Đã xóa người tham gia", + participantAdded: "Đã thêm người tham gia", + saveChanges: "Lưu thay đổi", + changesSaved: "Đã lưu thay đổi", + totalParticipants: "Tổng số người tham gia", + reassignmentsUsed: "Số lần đổi phân công đã dùng", + editGameDetails: "Chỉnh sửa chi tiết trò chơi", + gameUpdated: "Đã cập nhật trò chơi", + noParticipantsYet: "Chưa có người tham gia", + assignedTo: "Được phân cho", + editParticipant: "Chỉnh sửa người tham gia", + editParticipantDesc: "Sửa thông tin người tham gia", + participantUpdated: "Đã cập nhật người tham gia!", + + invalidOrganizer: "Liên kết người tổ chức không hợp lệ", + + errorTitle: "Đã xảy ra lỗi", + tryAgain: "Thử lại", + + apiUnavailableWarning: "⚠️ API không khả dụng", + apiUnavailableDesc: "Máy chủ không khả dụng. Dữ liệu chỉ được lưu cục bộ trên trình duyệt này.", + databaseUnavailableWarning: "⚠️ Cơ sở dữ liệu không khả dụng", + databaseUnavailableDesc: "Máy chủ không thể kết nối cơ sở dữ liệu. Dữ liệu sẽ không được lưu vĩnh viễn.", + gameCreatedLocally: "Trò chơi đã được tạo ở chế độ demo (chỉ lưu cục bộ)", + + // Protected game translations + protectedGameTitle: "Trò chơi được bảo vệ", + protectedGameDesc: "Trò chơi này yêu cầu liên kết riêng cho từng người tham gia. Vui lòng dùng liên kết do người tổ chức chia sẻ.", + invalidParticipantToken: "Token người tham gia không hợp lệ. Hãy kiểm tra liên kết với người tổ chức.", + copyIndividualLink: "Sao chép liên kết", + individualLinkCopied: "Đã sao chép liên kết!", + + // Refresh translations + refreshData: "Làm mới dữ liệu", + dataRefreshed: "Đã làm mới dữ liệu!", + apiUnavailable: "Máy chủ không khả dụng", + + // Reminder email translations + sendReminder: "Gửi nhắc nhở", + sendReminderAll: "Gửi cho tất cả", + sendReminderToParticipant: "Gửi nhắc nhở", + reminderSent: "Đã gửi nhắc nhở!", + remindersSent: "nhắc nhở đã gửi", + remindersFailed: "nhắc nhở gửi thất bại", + sendingReminder: "Đang gửi nhắc nhở...", + sendingReminders: "Đang gửi nhắc nhở...", + customMessage: "Tin nhắn tùy chỉnh", + customMessagePlaceholder: "Tin nhắn tùy chọn để thêm vào email nhắc nhở...", + reminderEmailDesc: "Gửi nhắc nhở kèm chi tiết sự kiện", + reminderToAllDesc: "Gửi nhắc nhở đến tất cả người tham gia có email", + noEmailConfigured: "Chưa cấu hình email", + + // Export participants translations + exportParticipants: "Xuất danh sách người tham gia", + exportParticipantsDesc: "Tải danh sách người tham gia với các chi tiết đã chọn", + exportDialogTitle: "Xuất danh sách người tham gia", + exportDialogDesc: "Chọn các chi tiết cần đưa vào tệp xuất", + exportIncludeAssignments: "Bao gồm phân công", + exportIncludeGameDetails: "Bao gồm chi tiết trò chơi", + exportIncludeWishes: "Bao gồm mong muốn", + exportIncludeInstructions: "Bao gồm hướng dẫn", + exportIncludeConfirmationStatus: "Bao gồm trạng thái xác nhận", + exportIncludeEmails: "Bao gồm email", + exportButton: "Xuất CSV", + exportingData: "Đang xuất...", + exportSuccess: "Đã xuất danh sách người tham gia!", + exportError: "Không thể xuất danh sách người tham gia", + exportNoFieldsSelected: "Vui lòng chọn ít nhất một trường để xuất", + exportHeaderName: "Tên", + exportHeaderEmail: "Email", + exportHeaderAssignedTo: "Phân cho", + exportHeaderGiftWish: "Mong muốn quà", + exportHeaderConfirmed: "Đã xác nhận", + exportHeaderEventName: "Tên sự kiện", + exportHeaderAmount: "Số tiền", + exportHeaderCurrency: "Tiền tệ", + exportHeaderDate: "Ngày", + exportHeaderLocation: "Địa điểm", + exportHeaderInstructions: "Hướng dẫn", + + // Privacy page translations + privacyTitle: "Chính sách quyền riêng tư", + privacySubtitle: "Cách chúng tôi xử lý dữ liệu của bạn", + privacyDataCollectionTitle: "Dữ liệu chúng tôi thu thập", + privacyDataCollectionDesc: "Khi sử dụng Zava Gift Exchange, chúng tôi thu thập các thông tin sau để việc trao đổi quà hoạt động:", + privacyDataItem1: "Tên người tham gia", + privacyDataItem2: "Địa chỉ email (tùy chọn, để nhận thông báo)", + privacyDataItem3: "Mong muốn quà tặng (tùy chọn)", + privacyDataItem4: "Chi tiết sự kiện (ngày, địa điểm, giá trị quà)", + privacyDataUsageTitle: "Mục đích sử dụng dữ liệu", + privacyDataUsageDesc: "Dữ liệu của bạn được lưu trong cơ sở dữ liệu an toàn và chỉ dùng để vận hành ứng dụng này. Bao gồm:", + privacyDataUsagePromise: "Dữ liệu trò chơi của bạn được lưu trữ an toàn và chỉ phục vụ vận hành ứng dụng này. Chúng tôi không bán hoặc chia sẻ thông tin của bạn cho mục đích tiếp thị.", + privacyDataRetentionTitle: "Lưu giữ dữ liệu", + privacyDataRetentionDesc: "Để bảo vệ quyền riêng tư của bạn, chúng tôi áp dụng các chính sách lưu giữ sau:", + privacyDataRetentionItem1: "Trò chơi được tự động lưu trữ sau 3 ngày kể từ ngày sự kiện", + privacyDataRetentionItem2: "Dữ liệu trò chơi đã lưu trữ sẽ bị xóa vĩnh viễn sau 30 ngày", + privacyDataRetentionItem3: "Người tổ chức có thể lưu trữ toàn bộ dữ liệu trò chơi bất kỳ lúc nào", + privacyThirdPartiesTitle: "Bên thứ ba", + privacyThirdPartiesDesc: "Chúng tôi sử dụng Azure Communication Services để gửi email thông báo khi bạn yêu cầu rõ ràng. Chúng tôi sử dụng Azure Application Insights để theo dõi tình trạng ứng dụng, lỗi và độ tin cậy — dữ liệu này được thu thập tự động và không cần sự đồng ý.", + privacySecurityTitle: "Bảo mật & Bảo vệ dữ liệu", + privacySecurityDesc: "Chúng tôi coi trọng bảo mật dữ liệu của bạn. Các biện pháp sau bảo vệ thông tin của bạn:", + privacySecurityItem1: "Tất cả token và mã trò chơi được tạo bằng nguồn ngẫu nhiên an toàn mật mã", + privacySecurityItem2: "So sánh token sử dụng thuật toán an toàn thời gian để ngăn tấn công kênh kề", + privacySecurityItem3: "Giới hạn tốc độ API giúp chống lạm dụng và thử brute-force", + privacySecurityItem4: "Xác thực đầu vào và giới hạn độ dài được áp dụng cho mọi trường dữ liệu", + privacySecurityItem5: "Ứng dụng có thể cài dưới dạng PWA; dữ liệu bộ nhớ đệm chỉ gồm tài nguyên tĩnh và không bao gồm thông tin cá nhân", + privacyLastUpdated: "Cập nhật lần cuối", + privacyLink: "Chính sách quyền riêng tư", + + // Game not found translations + gameNotFoundTitle: "Không tìm thấy trò chơi", + gameNotFoundDesc: "Trò chơi bạn đang tìm không tồn tại hoặc đã được lưu trữ.", + gameNotFoundReason1: "Trò chơi có thể đã được người tổ chức lưu trữ", + gameNotFoundReason2: "Trò chơi tự động được lưu trữ sau 3 ngày kể từ sự kiện", + gameNotFoundReason3: "Mã trò chơi có thể không chính xác", + goHome: "Về trang chủ", + + // Delete game translations + deleteGame: "Lưu trữ trò chơi", + deleteGameTitle: "Lưu trữ trò chơi này?", + deleteGameWarning: "⚠️ Trò chơi sẽ không còn truy cập được với người tham gia", + deleteGameDesc: "Trò chơi và dữ liệu của nó sẽ được lưu trữ, bao gồm:", + deleteGameItem1: "Toàn bộ thông tin người tham gia", + deleteGameItem2: "Phân công quà tặng", + deleteGameItem3: "Mong muốn và sở thích", + deleteGameItem4: "Lịch sử yêu cầu", + deleteGameConfirm: "Lưu trữ trò chơi", + gameDeleted: "Đã lưu trữ trò chơi thành công", + + // Regenerate organizer token + regenerateOrganizerTokenLink: "Tạo lại quyền truy cập người tổ chức", + regenerateOrganizerTokenLinkTitle: "Tạo lại liên kết truy cập của bạn?", + regenerateOrganizerTokenLinkDesc: "Một liên kết người tổ chức mới sẽ được gửi đến email của bạn. Quyền truy cập hiện tại sẽ ngừng hoạt động ngay lập tức.", + regenerateOrganizerTokenLinkWarning: "⚠️ Bạn sẽ bị đăng xuất sau khi tạo lại", + regenerateOrganizerTokenLinkConfirm: "Tạo lại & Gửi email", + organizerTokenRegenerated: "Đã gửi liên kết người tổ chức mới đến email của bạn!", + checkEmailForNewLink: "Hãy kiểm tra email để lấy liên kết người tổ chức mới", + organizerTokenRequiresEmail: "Cần có dịch vụ email để tạo lại token người tổ chức", + + // Date validation + dateInPast: "Ngày sự kiện phải là hôm nay hoặc trong tương lai", + + // Error pages + errorPageTitle: "Rất tiếc, đã xảy ra lỗi!", + errorInvalidToken: "Liên kết không còn hợp lệ", + errorInvalidTokenDesc: "Liên kết bạn dùng không hợp lệ hoặc đã hết hạn. Nếu bạn có liên kết hoặc token mới, hãy nhập bên dưới.", + errorProtectedGame: "Trò chơi được bảo vệ", + errorProtectedGameDesc: "Trò chơi này yêu cầu token truy cập riêng. Nếu bạn có token, hãy nhập bên dưới.", + errorUnexpected: "Đã xảy ra lỗi không mong muốn", + errorUnexpectedDesc: "Chúng tôi không thể hoàn tất yêu cầu của bạn. Vui lòng thử lại hoặc liên hệ người tổ chức.", + enterAccessToken: "Nhập token truy cập của bạn", + accessTokenPlaceholder: "Dán token của bạn vào đây", + accessTokenHint: "Token là phần sau 'participant=' hoặc 'organizer=' trong liên kết của bạn", + submitToken: "Truy cập trò chơi", + orAskOrganizer: "Hoặc liên hệ người tổ chức để lấy liên kết mới", + + // Error recovery + participantAccessTitle: "Truy cập người tham gia", + noTokenQuestion: "Bạn không có token?", + contactOrganizerForLink: "Hãy nhờ người tổ chức gửi liên kết cá nhân của bạn.", + participantRecoveryOption: "Nếu bạn đã đăng ký bằng email, bạn có thể khôi phục liên kết.", + recoverParticipantLink: "Khôi phục liên kết của tôi", + participantRecoveryDesc: "Nhập email bạn đã dùng khi đăng ký. Nếu khớp, chúng tôi sẽ gửi liên kết truy cập cho bạn.", + participantEmailRecoveryPlaceholder: "Nhập email của bạn", + participantRecoveryEmailSentDesc: "Nếu email khớp với người tham gia, liên kết khôi phục đã được gửi. Hãy kiểm tra hộp thư.", + organizerAccessTitle: "Truy cập người tổ chức", + organizerForgotLink: "Bạn là người tổ chức và bị mất liên kết?", + recoverOrganizerLink: "Khôi phục liên kết người tổ chức", + organizerRecoveryTitle: "Khôi phục quyền truy cập người tổ chức", + organizerRecoveryDesc: "Nhập email bạn đã dùng khi tạo trò chơi. Nếu khớp, chúng tôi sẽ gửi liên kết người tổ chức.", + recoveryEmailLabel: "Email của bạn", + recoveryEmailPlaceholder: "Nhập email người tổ chức", + sendRecoveryEmail: "Gửi email khôi phục", + sending: "Đang gửi...", + recoveryEmailSent: "Đã gửi email khôi phục!", + recoveryEmailSentDesc: "Nếu email khớp với email người tổ chức, liên kết khôi phục đã được gửi. Hãy kiểm tra hộp thư.", + recoveryNotAvailable: "Không có email đăng ký cho người tổ chức trò chơi này", + recoveryFailed: "Không thể gửi email khôi phục. Vui lòng thử lại.", + recoveryNotAvailableNoEmail: "Khôi phục qua email không khả dụng cho trò chơi này. Vui lòng liên hệ trực tiếp người tổ chức nếu bạn bị mất liên kết.", + + // Guide pages + guideOrganizerLink: "Hướng dẫn cho người tổ chức", + guideParticipantLink: "Hướng dẫn cho người tham gia", + + // Organizer Guide + guideOrganizerTitle: "Hướng dẫn cho người tổ chức", + guideOrganizerSubtitle: "Tìm hiểu cách tạo và quản lý sự kiện Zava Gift Exchange của bạn", + guideStep1Title: "Bước 1: Tạo trò chơi", + guideStep1Desc: "Nhấn 'Tạo trò chơi mới' ở trang chủ để bắt đầu thiết lập trao đổi quà.", + guideStep1Details: "Bạn cần cung cấp:", + guideStep1Item1: "Tên sự kiện (ví dụ: 'Giáng Sinh Văn Phòng 2025')", + guideStep1Item2: "Giá trị quà và tiền tệ", + guideStep1Item3: "Ngày, giờ và địa điểm sự kiện", + guideStep1Item4: "Ghi chú hoặc hướng dẫn tùy chọn cho người tham gia", + guideStep2Title: "Bước 2: Thêm người tham gia", + guideStep2Desc: "Thêm ít nhất 3 người tham gia vào trao đổi quà. Càng đông càng vui!", + guideStep2Details: "Với mỗi người tham gia:", + guideStep2Item1: "Nhập tên (bắt buộc)", + guideStep2Item2: "Thêm email để nhận thông báo (tùy chọn)", + guideStep2Item3: "Tùy chọn thêm mong muốn quà cho họ - họ sẽ thấy và có thể giữ hoặc đổi", + guideStep2Item4: "Mẹo: Đừng quên thêm chính bạn nếu muốn tham gia!", + guideStep3Title: "Bước 3: Cấu hình tùy chọn", + guideStep3Desc: "Tùy chỉnh cách trò chơi Zava Gift Exchange hoạt động.", + guideStep3Option1: "Cho phép đổi phân công", + guideStep3Option1Desc: "Cho phép người tham gia yêu cầu phân công mới khi cần", + guideStep3Option2: "Bảo vệ danh sách người tham gia", + guideStep3Option2Desc: "Mỗi người tham gia nhận liên kết riêng - họ sẽ không thấy người khác", + guideStep3Option3: "Thông báo email", + guideStep3Option3Desc: "Tự động gửi phân công cho người tham gia qua email", + guideStep4Title: "Bước 4: Chia sẻ liên kết", + guideStep4Desc: "Sau khi tạo trò chơi, hãy chia sẻ liên kết với người tham gia.", + guideStep4Item1: "Với trò chơi bảo vệ: Chia sẻ riêng từng liên kết duy nhất", + guideStep4Item2: "Với trò chơi không bảo vệ: Chia sẻ mã trò chơi cho mọi người", + guideStep4Item3: "Lưu liên kết người tổ chức - bạn sẽ cần nó để quản lý sự kiện!", + guideOrganizerPanelTitle: "Tính năng bảng điều khiển người tổ chức", + guideOrganizerPanelDesc: "Sau khi tạo trò chơi, bạn có thể quản lý mọi thứ từ bảng điều khiển người tổ chức.", + guideFeatureStats: "Xem thống kê", + guideFeatureStatsDesc: "Xem bao nhiêu người đã xác nhận, thêm mong muốn hoặc yêu cầu đổi phân công.", + guideFeatureManage: "Quản lý người tham gia", + guideFeatureManageDesc: "Thêm người mới, sửa chi tiết hoặc xóa ai đó khỏi trò chơi. Khi thêm người mới, các phân công đã xác nhận vẫn được giữ nguyên.", + guideFeatureReassign: "Xử lý yêu cầu đổi phân công", + guideFeatureReassignDesc: "Đổi phân công cho từng người hoặc tất cả cùng lúc. Duyệt hoặc từ chối yêu cầu từ người tham gia. Nếu quá nhiều người đã xác nhận, bạn có thể cần dùng 'Phân công lại tất cả'.", + guideFeatureLinks: "Tạo lại liên kết", + guideFeatureLinksDesc: "Nếu người tham gia làm mất liên kết, bạn có thể tạo liên kết mới cho họ.", + guideFeatureOrganizerToken: "Tạo lại liên kết truy cập của bạn", + guideFeatureOrganizerTokenDesc: "Nếu bạn nghi ngờ liên kết người tổ chức bị lộ hoặc muốn bảo mật tài khoản, bạn có thể tạo lại. Liên kết mới sẽ được gửi qua email và liên kết cũ ngừng hoạt động ngay lập tức. Tính năng này yêu cầu dịch vụ email đã cấu hình.", + guideFeatureDelete: "Lưu trữ trò chơi", + guideFeatureDeleteDesc: "Lưu trữ trò chơi và toàn bộ dữ liệu khi không còn cần. Quản trị viên có thể khôi phục dữ liệu.", + guideNewFeaturesTitle: "Tính năng bổ sung", + guideFeatureQrCode: "Chia sẻ bằng mã QR", + guideFeatureQrCodeDesc: "Khi trò chơi được tạo, một mã QR cho liên kết mời sẽ được tạo. Mã này chỉ để mời. Người tham gia có thể quét bằng camera điện thoại để vào ngay — rất phù hợp khi chia sẻ trực tiếp trong cuộc họp hoặc tiệc.", + guideFeatureExclusions: "Quy tắc loại trừ", + guideFeatureExclusionsDesc: "Ngăn các cặp cụ thể được ghép với nhau. Ví dụ, loại trừ cặp đôi hoặc người trong gia đình để họ không bốc trúng nhau. Tính năng này hiện khả dụng qua API và cấu hình trên giao diện sẽ có trong bản cập nhật sau.", + guideFeatureDarkMode: "Chế độ tối", + guideFeatureDarkModeDesc: "Chuyển giữa giao diện sáng và tối bằng biểu tượng trăng/mặt trời ở đầu trang. Tùy chọn của bạn sẽ được lưu và giữ nguyên cho các lần truy cập sau. Ứng dụng cũng tôn trọng cài đặt giao diện của hệ điều hành.", + guideFeatureCountdown: "Đếm ngược sự kiện", + guideFeatureCountdownDesc: "Bộ đếm ngược trực tiếp hiển thị thời gian còn lại đến sự kiện. Nó tự động cập nhật và xuất hiện ở trang phân công để bạn không bỏ lỡ ngày diễn ra.", + guideFeatureCalendar: "Thêm vào lịch", + guideFeatureCalendarDesc: "Tải tệp lịch .ics để thêm sự kiện Zava Gift Exchange vào ứng dụng lịch của bạn (Google Calendar, Outlook, Apple Calendar, v.v.). Có sẵn ở trang phân công.", + guideTipsTitle: "Mẹo cho người tổ chức", + guideTip1: "Tạo trò chơi trước sự kiện vài ngày để người tham gia có thời gian chuẩn bị", + guideTip2: "Dùng chế độ bảo vệ cho nhóm lớn để giữ riêng tư phân công", + guideTip3: "Gửi nhắc nhở khi gần đến ngày sự kiện", + guideTip4: "Trò chơi sẽ tự động được lưu trữ sau 3 ngày kể từ ngày sự kiện", + + // Participant Guide + guideParticipantTitle: "Hướng dẫn cho người tham gia", + guideParticipantSubtitle: "Tìm hiểu cách tham gia Zava Gift Exchange", + guideJoinTitle: "Cách tham gia", + guideJoinDesc: "Có hai cách để tham gia một trò chơi Zava Gift Exchange:", + guideJoinOption1Title: "Cách 1: Liên kết trực tiếp", + guideJoinOption1Desc: "Nhấn vào liên kết do người tổ chức chia sẻ. Bạn sẽ được đưa thẳng đến phân công của mình.", + guideJoinOption2Title: "Cách 2: Mã trò chơi", + guideJoinOption2Desc: "Nhập mã trò chơi 6 chữ số ở trang chủ, rồi chọn tên bạn từ danh sách.", + guideViewAssignmentTitle: "Xem phân công của bạn", + guideViewAssignmentDesc: "Khi tham gia, bạn sẽ thấy người mà bạn cần mua quà. Nếu phân công của bạn vẫn đang được chuẩn bị, bạn sẽ thấy trang phân công đang chờ cho đến khi người tổ chức hoàn tất.", + guideViewAssignmentItem1: "Xem tên người bạn tặng quà", + guideViewAssignmentItem2: "Xem danh sách mong muốn của họ nếu họ đã thêm", + guideViewAssignmentItem3: "Xem chi tiết sự kiện như ngày, địa điểm và giá trị quà", + guideWishTitle: "Thêm mong muốn quà của bạn", + guideWishDesc: "Hãy giúp Zava Gift Exchange bằng cách thêm món quà bạn muốn nhận!", + guideWishItem1: "Nếu người tổ chức đã thêm mong muốn cho bạn, nó sẽ hiện tự động - bạn có thể giữ hoặc đổi", + guideWishItem2: "Hãy cụ thể nhưng linh hoạt - đưa ra lựa chọn ở nhiều mức giá", + guideWishItem3: "Bạn có thể sửa mong muốn bất cứ lúc nào", + guideWishItem4: "Khi bạn đổi mong muốn, Zava Gift Exchange của bạn sẽ được thông báo qua email (nếu họ đã cung cấp địa chỉ email)", + guideReassignTitle: "Yêu cầu đổi phân công", + guideReassignDesc: "Không hài lòng với phân công của bạn? Bạn có thể yêu cầu thay đổi.", + guideReassignItem1: "Chỉ khả dụng nếu người tổ chức bật tùy chọn này", + guideReassignItem2: "Người tổ chức phải duyệt yêu cầu của bạn", + guideReassignItem3: "Không thể yêu cầu sau khi đã xác nhận phân công", + guideConfirmTitle: "Xác nhận phân công của bạn", + guideConfirmDesc: "Hãy cho người tổ chức biết bạn đã xem phân công bằng cách xác nhận. Điều này giúp họ theo dõi mức độ tham gia.", + guideProtectedTitle: "Về trò chơi được bảo vệ", + guideProtectedDesc: "Một số người tổ chức dùng chế độ bảo vệ để tăng riêng tư:", + guideProtectedItem1: "Bạn nhận một liên kết cá nhân duy nhất", + guideProtectedItem2: "Bạn chỉ có thể xem phân công của chính mình", + guideProtectedItem3: "Giữ liên kết của bạn riêng tư - đừng chia sẻ cho người khác", + guideEmailTitle: "Thông báo email", + guideEmailDesc: "Nếu bạn cung cấp email, bạn có thể nhận thông báo về sự kiện, nhắc nhở hoặc chi tiết phân công.", + guideParticipantTipsTitle: "Mẹo cho người tham gia", + guideParticipantTip1: "Thêm mong muốn sớm để Zava Gift Exchange của bạn có thời gian mua quà", + guideParticipantTip2: "Giữ bí mật phân công của bạn - đó là phần thú vị nhất!", + guideParticipantTip3: "Đánh dấu liên kết hoặc lưu mã trò chơi để truy cập nhanh", +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 8e08782..1d0b729 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -80,7 +80,7 @@ export interface Game { archivedAt?: number // Unix timestamp in milliseconds since epoch when the game was archived (Date.now()) } -export type Language = 'en' | 'es' | 'pt' | 'fr' | 'it' | 'ja' | 'ko' | 'zh' | 'de' | 'nl' +export type Language = 'en' | 'es' | 'pt' | 'fr' | 'it' | 'ja' | 'ko' | 'zh' | 'de' | 'nl' | 'vi' export interface LanguageOption { code: Language @@ -100,6 +100,7 @@ export const LANGUAGES: LanguageOption[] = [ { code: 'zh', name: 'Chinese', nativeName: '中文', flag: '🇨🇳' }, { code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' }, { code: 'nl', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱' }, + { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳' }, ] export interface Participant {