From be968d053914407beebdd677f90dfc0ac09333a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:56:36 +0000 Subject: [PATCH 1/3] Initial plan From 794359fb2919fb946f1f29d3774fabbcf036e10b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:05:28 +0000 Subject: [PATCH 2/3] feat: add vietnamese localization support wiring --- api/src/__tests__/email-service.test.ts | 21 +- api/src/shared/email-service.ts | 106 ++--- api/src/shared/types.ts | 2 +- src/lib/translations/README.md | 1 + src/lib/translations/index.ts | 4 +- src/lib/translations/vi.ts | 547 ++++++++++++++++++++++++ src/lib/types.ts | 3 +- 7 files changed, 627 insertions(+), 57 deletions(-) create mode 100644 src/lib/translations/vi.ts diff --git a/api/src/__tests__/email-service.test.ts b/api/src/__tests__/email-service.test.ts index 162ac63..3603207 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,23 @@ 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 content = generateOrganizerEmailContent({ game, language: 'vi' }) + expect(content.subject).toContain('Holiday Exchange') + expect(content.subject).toContain('Your Zava Gift Exchange game') + expect(content.html).toContain('Game details') + }) + }) }) diff --git a/api/src/shared/email-service.ts b/api/src/shared/email-service.ts index 4a22722..021fc2d 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) { + return translation } - // Fallback to English if translation not available - return translations['en'] + throw new Error('English email translation is missing') } // 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..27e851d 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' | '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..7137cd9 --- /dev/null +++ b/src/lib/translations/vi.ts @@ -0,0 +1,547 @@ +export const vi = { + appName: "Zava Gift Exchange", + welcome: "Welcome!", + welcomeDesc: "Organize your gift exchange easily and fun", + loading: "Loading...", + createGame: "Create New Game", + joinGame: "Join a Game", + enterCode: "Enter the code", + codePlaceholder: "6-digit code", + continue: "Continue", + back: "Back", + next: "Next", + finish: "Finish", + cancel: "Cancel", + confirm: "Confirm", + yes: "Yes", + no: "No", + + step1: "Event Details", + step2: "Participants", + step3: "Configuration", + + eventName: "Event Name", + eventNamePlaceholder: "E.g: Office Christmas 2025", + giftAmount: "Gift Amount", + giftAmountPlaceholder: "E.g: 20", + currency: "Currency", + selectCurrency: "Select currency", + eventDate: "Event Date", + eventTime: "Event Time", + eventLocation: "Event Location", + eventLocationPlaceholder: "E.g: Main Office", + + participants: "Participants", + participantsDesc: "Add at least 3 participants", + participantName: "Participant name", + participantNamePlaceholder: "E.g: Mary Smith", + participantEmail: "Participant email", + participantEmailPlaceholder: "E.g: mary@email.com (Tab to continue)", + participantEmailOptional: "(Optional - for notifications)", + yourEmail: "Your Email", + yourEmailPlaceholder: "E.g: youremail@email.com", + yourEmailDesc: "Optional - to receive event notifications", + emailUpdated: "Email updated!", + tabToAddEmail: "Tab to add email", + tabToAddGift: "Tab to add gift", + tabToAddAnother: "Tab/Enter to add another", + tabToFinish: "Tab/Enter to finish", + pressTabOrEnter: "Press Tab or Enter", + desiredGift: "Desired Gift", + desiredGiftPlaceholder: "E.g: Book, video game, clothing...", + desiredGiftOptional: "(Optional)", + theirDesiredGift: "Gift they want:", + addParticipant: "Add Participant", + removeParticipant: "Remove", + minParticipants: "At least 3 participants needed", + organizerNote: "Note: If you want to play, you'll need to add yourself to the participant list!", + regenerateToken: "Regenerate Link", + regenerateTokenTitle: "Regenerate participant link?", + regenerateTokenDesc: "A new unique link will be created for this participant. The old link will no longer work.", + regenerateTokenConfirm: "Generate New Link", + tokenRegenerated: "New link created!", + + allowReassignment: "Allow Reassignment", + allowReassignmentDesc: "Participants can request a new assignment (organizer must approve)", + protectParticipants: "Protect Participant List", + protectParticipantsDesc: "Each participant receives a unique link. They won't be able to see other participants' data.", + protectParticipantsWarning: "⚠️ You'll need to share each link individually with each participant", + generalNotes: "General Notes and Instructions", + generalNotesPlaceholder: "E.g: Bring the gift wrapped. Don't spend more than agreed amount.", + + // Email notification settings + emailNotifications: "Email Notifications", + emailNotificationsDesc: "Receive game details via email", + organizerEmail: "Your Email (Organizer)", + organizerEmailPlaceholder: "E.g: youremail@email.com", + organizerEmailDesc: "You'll receive the game links and organizer panel", + sendEmailsOnCreate: "Send emails when creating game", + sendEmailsOnCreateDesc: "Automatically send details to all participants with email", + sendEmailNotifications: "Send Email Notifications", + sendEmailNotificationsDesc: "Send emails to organizer and participants when creating the game", + creating: "Creating...", + emailSentToOrganizer: "Email sent to organizer!", + emailsSentToParticipants: "{count} emails sent to participants", + + gameCreated: "Game Created!", + gameCreatedDesc: "Share this link with all participants", + gameCode: "Game Code", + copyCode: "Copy Code", + codeCopied: "Code copied!", + participantLink: "Participant Link", + participantLinkDesc: "Share this link with all gift exchange participants", + individualParticipantLinks: "Individual Participant Links", + individualParticipantLinksDesc: "Each participant has a unique link. Share each link only with its intended recipient.", + copyParticipantLink: "Copy Link", + participantLinkCopied: "Link copied!", + shareOnSocialMedia: "Share on Social Media", + shareMessageTitle: "Join our Zava Gift Exchange!", + shareMessage: "Hi! Join our Zava Gift Exchange \"{eventName}\". Click here: {link}", + shareOnWhatsApp: "Share on WhatsApp", + shareOnFacebook: "Share on Facebook", + shareOnX: "Share on X", + shareOnMessenger: "Share on Messenger", + organizerLink: "Organizer Link", + copyOrganizerLink: "Copy Link", + linkCopied: "Link copied!", + copyNewLink: "Copy New Link", + done: "Done", + organizerLinkDesc: "Save this link to view statistics and manage the event", + emailNotConfiguredSaveLink: "Email service not configured. Save this link to manage the event", + useOrganizerPanelLink: "Use this link to access the organizer panel", + emailAlreadyExists: "This email is already used by another participant", + invalidEmailFormat: "Please enter a valid email address", + invalidDate: "Invalid date. Please enter a valid calendar date (e.g., not February 31 or April 31)", + participantNameExists: "This participant name already exists", + goToGame: "Go to Organizer Panel", + + // Invitation link + invitationLink: "Invitation Link", + invitationLinkDesc: "Share this link to allow new participants to join the game", + copyInvitationLink: "Copy Invitation Link", + invitationLinkCopied: "Invitation link copied!", + joinViaInvitation: "Join via Invitation", + joinGameTitle: "Join Zava Gift Exchange", + joinGameDesc: "Fill in your information to join this gift exchange", + yourName: "Your Name", + yourNamePlaceholder: "E.g: John Smith", + yourGiftWish: "Your Gift Wish (Optional)", + yourGiftWishPlaceholder: "Any specific details about what you'd like...", + joinGameButton: "Join Game", + joining: "Joining...", + joinSuccess: "Successfully joined!", + joinError: "Failed to join game", + invalidInvitationToken: "Invalid or expired invitation link", + participantNameAlreadyExists: "This name is already taken in this game", + emailAlreadyInUse: "This email is already in use in this game", + + // Email actions + sendOrganizerEmail: "Send Organizer Email", + sendOrganizerEmailDesc: "Receive game details in your email", + sendParticipantEmails: "Send Participant Emails", + sendParticipantEmailsDesc: "Send assignments to participants with email", + emailSent: "Email sent!", + emailsSent: "emails sent", + emailsFailed: "emails failed", + sendingEmail: "Sending email...", + sendingEmails: "Sending emails...", + emailServiceNotConfigured: "Email service is not configured", + noParticipantsWithEmail: "No participants have email configured", + + selectParticipant: "Select Your Name", + selectParticipantDesc: "Choose your name from the list", + invalidCode: "Code not found", + invalidCodeDesc: "Check the code with the organizer", + + yourAssignment: "Your Assignment", + youGiftTo: "You're gifting to:", + theirWish: "Their wish:", + noWishYet: "Haven't added a wish yet", + addYourWish: "Add My Wish", + yourWish: "Your Gift Wish", + yourWishPlaceholder: "E.g: I'd like a science fiction book", + saveWish: "Save Wish", + wishSaved: "Wish saved!", + editWish: "Edit Wish", + wishDescription: "Tell your Zava Gift Exchange what you'd like to receive", + wishHint: "This wish will be visible to whoever is gifting you", + skipWish: "Skip", + saveAndContinue: "Save and Continue", + updateAndContinue: "Update and Continue", + hasWish: "Has wish", + noWish: "No wish", + noWishAddedYet: "You haven't added your gift wish yet", + wishDialogDesc: "Write what you'd like to receive as a gift", + wishChangeWarningTitle: "Wish Change Warning", + wishChangeWarningDesc: "Your gift giver has already confirmed their assignment. If you change your wish now, they will be notified by email so they can see your updated preference.", + proceedWithChange: "Proceed with Change", + wishesAdded: "Wishes Added", + giftWish: "Gift Wish", + giftWishDesc: "What this participant would like to receive", + + eventDetails: "Event Details", + amount: "Amount", + date: "Date", + location: "Location", + instructions: "Instructions", + noInstructions: "No additional instructions", + notSpecified: "Not specified", + + requestReassignment: "Request New Assignment", + requestReassignmentTitle: "Request New Assignment?", + requestReassignmentDesc: "Your request will be sent to the organizer for approval.", + cannotReassignConfirmed: "You cannot request a reassignment after confirming your assignment", + reassignmentSuccess: "Request sent! The organizer will review your request.", + reassignmentUsed: "You already have a pending request", + reassignmentNotAllowed: "Organizer doesn't allow reassignments", + reassignmentPending: "Request pending", + reassignmentPendingDesc: "The organizer will review your request soon.", + + // Organizer reassignment translations + pendingReassignmentRequests: "Pending Reassignment Requests", + noPendingRequests: "No pending requests", + approveReassignment: "Approve", + cancelRequest: "Cancel Request", + reassignAll: "Reassign All", + reassignAllDesc: "Generate new assignments for unconfirmed participants only", + confirmedParticipantsLocked: "participant(s) with confirmed assignments will keep their current assignments", + unconfirmedWillReceiveNew: "participant(s) will receive new assignments", + confirmedParticipantsWarning: "participant(s) have confirmed their assignment", + confirmedParticipantsWarningDesc: "Their confirmation status will be cleared when reassignments are generated.", + forceReassignParticipant: "Force Reassign", + forceReassignConfirm: "Force Reassign Participant?", + forceReassignWarning: "This participant has confirmed their assignment.", + forceReassignWarningDesc: "They will receive a new assignment and will be notified by email (if configured). This action cannot be undone.", + forceReassignSuccess: "Participant reassigned and notified", + approveAllPending: "Approve All Pending", + approveAllPendingDesc: "Approve all pending reassignment requests. Each participant will receive a new assignment.", + allPendingApproved: "All pending requests were approved", + reassignmentApproved: "Reassignment approved", + reassignmentFailed: "Cannot reassign: no valid swap available. Try regenerating all assignments.", + reassignmentFailedSuggestAll: "Cannot reassign this participant individually. Too many participants have confirmed. Try 'Reassign All' instead.", + reassignmentRequestCancelled: "Request cancelled", + allReassigned: "All assignments were regenerated", + requestedAt: "Requested", + hasPendingRequest: "Request pending", + reassignParticipant: "Reassign", + pendingAssignmentSingular: "has no assignment yet", + pendingAssignmentPlural: "have no assignments yet", + pendingAssignment: "Pending assignment", + pendingAssignmentDesc: "Your assignment is being prepared. The organizer will finalize assignments soon.", + allConfirmedReassignNeeded: "All other participants have confirmed their assignments. You need to use 'Reassign All' to include new participants.", + qrInviteOnly: "Scan to invite new participants", + qrCodeAriaLabel: "Invitation QR code", + + downloadCard: "Download Card", + downloadingCard: "Generating card...", + cardDownloaded: "Card downloaded!", + eventCountdown: "Time until event", + addToCalendar: "Add to Calendar", + darkMode: "Dark mode", + lightMode: "Light mode", + + organizerPanel: "Organizer Panel", + organizerPanelDesc: "This link is exclusive for game organizers", + organizerOnly: "🔒 Organizers Only", + statistics: "Statistics", + addedWish: "Have added their wish", + usedReassignment: "Have used reassignment", + editEvent: "Edit Event", + shareCode: "Share Code", + gameManagement: "Game Management", + participantStatus: "Participant Status", + hasConfirmed: "Confirmed", + notConfirmed: "Not confirmed", + hasReassigned: "Reassigned", + hasEmail: "Has email", + noEmail: "No email", + addNewParticipant: "Add New Participant", + confirmAssignment: "Confirm assignment", + confirmAssignmentDesc: "Confirm that you received your assignment and are ready to participate", + assignmentConfirmed: "Assignment confirmed!", + assignmentsConfirmed: "Assignments Confirmed", + removeParticipantConfirm: "Remove participant?", + participantRemoved: "Participant removed", + participantAdded: "Participant added", + saveChanges: "Save Changes", + changesSaved: "Changes saved", + totalParticipants: "Total Participants", + reassignmentsUsed: "Reassignments Used", + editGameDetails: "Edit Game Details", + gameUpdated: "Game updated", + noParticipantsYet: "No participants yet", + assignedTo: "Assigned to", + editParticipant: "Edit Participant", + editParticipantDesc: "Modify participant details", + participantUpdated: "Participant updated!", + + invalidOrganizer: "Invalid organizer link", + + errorTitle: "Something went wrong", + tryAgain: "Try again", + + apiUnavailableWarning: "⚠️ API Unavailable", + apiUnavailableDesc: "Server is not available. Data is only saved locally in this browser.", + databaseUnavailableWarning: "⚠️ Database Unavailable", + databaseUnavailableDesc: "Server cannot connect to database. Data will not be saved permanently.", + gameCreatedLocally: "Game created in demo mode (local storage only)", + + // Protected game translations + protectedGameTitle: "Protected Game", + protectedGameDesc: "This game requires a unique link for each participant. Please use the link shared by the organizer.", + invalidParticipantToken: "Invalid participant token. Check the link with the organizer.", + copyIndividualLink: "Copy Link", + individualLinkCopied: "Link copied!", + + // Refresh translations + refreshData: "Refresh data", + dataRefreshed: "Data refreshed!", + apiUnavailable: "Server is not available", + + // Reminder email translations + sendReminder: "Send Reminder", + sendReminderAll: "Send to All", + sendReminderToParticipant: "Send Reminder", + reminderSent: "Reminder sent!", + remindersSent: "reminders sent", + remindersFailed: "reminders failed", + sendingReminder: "Sending reminder...", + sendingReminders: "Sending reminders...", + customMessage: "Custom Message", + customMessagePlaceholder: "Optional message to include in the reminder...", + reminderEmailDesc: "Send a reminder with event details", + reminderToAllDesc: "Send a reminder to all participants with email", + noEmailConfigured: "No email configured", + + // Export participants translations + exportParticipants: "Export Participants", + exportParticipantsDesc: "Download participant list with selected details", + exportDialogTitle: "Export Participant List", + exportDialogDesc: "Choose which details to include in the export", + exportIncludeAssignments: "Include Assignments", + exportIncludeGameDetails: "Include Game Details", + exportIncludeWishes: "Include Wishes", + exportIncludeInstructions: "Include Instructions", + exportIncludeConfirmationStatus: "Include Confirmation Status", + exportIncludeEmails: "Include Emails", + exportButton: "Export as CSV", + exportingData: "Exporting...", + exportSuccess: "Participants exported!", + exportError: "Failed to export participants", + exportNoFieldsSelected: "Please select at least one field to export", + exportHeaderName: "Name", + exportHeaderEmail: "Email", + exportHeaderAssignedTo: "Assigned To", + exportHeaderGiftWish: "Gift Wish", + exportHeaderConfirmed: "Confirmed", + exportHeaderEventName: "Event Name", + exportHeaderAmount: "Amount", + exportHeaderCurrency: "Currency", + exportHeaderDate: "Date", + exportHeaderLocation: "Location", + exportHeaderInstructions: "Instructions", + + // Privacy page translations + privacyTitle: "Privacy Policy", + privacySubtitle: "How we handle your data", + privacyDataCollectionTitle: "Data We Collect", + privacyDataCollectionDesc: "When using Zava Gift Exchange, we collect the following information to make the gift exchange work:", + privacyDataItem1: "Participant names", + privacyDataItem2: "Email addresses (optional, for notifications)", + privacyDataItem3: "Gift wishes (optional)", + privacyDataItem4: "Event details (date, location, gift amount)", + privacyDataUsageTitle: "Data Usage", + privacyDataUsageDesc: "Your data is stored in a secure database and used exclusively for the operation of this application. This includes:", + privacyDataUsagePromise: "Your game data is stored securely and used exclusively for the operation of this application. We do not sell or share your information for marketing purposes.", + privacyDataRetentionTitle: "Data Retention", + privacyDataRetentionDesc: "To protect your privacy, we implement the following retention policies:", + privacyDataRetentionItem1: "Games are automatically archived 3 days after the event date", + privacyDataRetentionItem2: "Archived game data is permanently deleted after 30 days", + privacyDataRetentionItem3: "Organizers can archive all game data at any time", + privacyThirdPartiesTitle: "Third Parties", + privacyThirdPartiesDesc: "We use Azure Communication Services to send notification emails when you explicitly request it. We use Azure Application Insights to monitor application health, track errors, and ensure reliability — this telemetry is automatic and does not require consent.", + privacySecurityTitle: "Security & Data Protection", + privacySecurityDesc: "We take the security of your data seriously. The following measures protect your information:", + privacySecurityItem1: "All tokens and game codes are generated using cryptographically secure randomness", + privacySecurityItem2: "Token comparisons use timing-safe algorithms to prevent side-channel attacks", + privacySecurityItem3: "API rate limiting protects against abuse and brute-force attempts", + privacySecurityItem4: "Input validation and length limits are enforced on all data fields", + privacySecurityItem5: "The app can be installed as a PWA; cached data is limited to static assets and never includes personal information", + privacyLastUpdated: "Last updated", + privacyLink: "Privacy Policy", + + // Game not found translations + gameNotFoundTitle: "Game Not Found", + gameNotFoundDesc: "The game you're looking for doesn't exist or has been archived.", + gameNotFoundReason1: "The game may have been archived by the organizer", + gameNotFoundReason2: "Games are automatically archived 3 days after the event", + gameNotFoundReason3: "The game code might be incorrect", + goHome: "Go Home", + + // Delete game translations + deleteGame: "Archive Game", + deleteGameTitle: "Archive this game?", + deleteGameWarning: "⚠️ The game will no longer be accessible to participants", + deleteGameDesc: "The game and its data will be archived, including:", + deleteGameItem1: "All participant information", + deleteGameItem2: "Gift assignments", + deleteGameItem3: "Wishes and preferences", + deleteGameItem4: "Request history", + deleteGameConfirm: "Archive Game", + gameDeleted: "Game archived successfully", + + // Regenerate organizer token + regenerateOrganizerTokenLink: "Regenerate Organizer Access", + regenerateOrganizerTokenLinkTitle: "Regenerate your access link?", + regenerateOrganizerTokenLinkDesc: "A new organizer link will be sent to your email. Your current access will stop working immediately.", + regenerateOrganizerTokenLinkWarning: "⚠️ You will be logged out after regenerating", + regenerateOrganizerTokenLinkConfirm: "Regenerate & Send Email", + organizerTokenRegenerated: "New organizer link sent to your email!", + checkEmailForNewLink: "Check your email for the new organizer link", + organizerTokenRequiresEmail: "Email service required to regenerate organizer token", + + // Date validation + dateInPast: "Event date must be today or in the future", + + // Error pages + errorPageTitle: "Oops, something went wrong!", + errorInvalidToken: "Link no longer valid", + errorInvalidTokenDesc: "The link you used is invalid or has expired. If you have a new link or token, enter it below.", + errorProtectedGame: "Protected game", + errorProtectedGameDesc: "This game requires a unique access token. If you have your token, enter it below.", + errorUnexpected: "An unexpected error occurred", + errorUnexpectedDesc: "We couldn't complete your request. Please try again or contact the organizer.", + enterAccessToken: "Enter your access token", + accessTokenPlaceholder: "Paste your token here", + accessTokenHint: "The token is the part after 'participant=' or 'organizer=' in your link", + submitToken: "Access Game", + orAskOrganizer: "Or ask the organizer for a new link", + + // Error recovery + participantAccessTitle: "Participant Access", + noTokenQuestion: "Don't have a token?", + contactOrganizerForLink: "Ask the organizer to send you your personal link.", + participantRecoveryOption: "If you registered with an email, you can recover your link.", + recoverParticipantLink: "Recover My Link", + participantRecoveryDesc: "Enter the email you used when registering. If it matches, we'll send you your access link.", + participantEmailRecoveryPlaceholder: "Enter your email", + participantRecoveryEmailSentDesc: "If the email matches a participant, a recovery link has been sent. Check your inbox.", + organizerAccessTitle: "Organizer Access", + organizerForgotLink: "Are you the organizer and lost your link?", + recoverOrganizerLink: "Recover Organizer Link", + organizerRecoveryTitle: "Recover Organizer Access", + organizerRecoveryDesc: "Enter the email you used when creating the game. If it matches, we'll send you the organizer link.", + recoveryEmailLabel: "Your Email", + recoveryEmailPlaceholder: "Enter organizer email", + sendRecoveryEmail: "Send Recovery Email", + sending: "Sending...", + recoveryEmailSent: "Recovery email sent!", + recoveryEmailSentDesc: "If the email matches the organizer's email, a recovery link has been sent. Check your inbox.", + recoveryNotAvailable: "No email registered for this game's organizer", + recoveryFailed: "Failed to send recovery email. Please try again.", + recoveryNotAvailableNoEmail: "Email recovery is not available for this game. Please contact the organizer directly if you have lost your link.", + + // Guide pages + guideOrganizerLink: "Organizer Guide", + guideParticipantLink: "Participant Guide", + + // Organizer Guide + guideOrganizerTitle: "Organizer Guide", + guideOrganizerSubtitle: "Learn how to create and manage your Zava Gift Exchange event", + guideStep1Title: "Step 1: Create Your Game", + guideStep1Desc: "Click 'Create New Game' on the home page to start setting up your gift exchange.", + guideStep1Details: "You'll need to provide:", + guideStep1Item1: "Event name (e.g., 'Office Christmas 2025')", + guideStep1Item2: "Gift amount and currency", + guideStep1Item3: "Event date, time, and location", + guideStep1Item4: "Optional notes or instructions for participants", + guideStep2Title: "Step 2: Add Participants", + guideStep2Desc: "Add at least 3 participants to your gift exchange. The more the merrier!", + guideStep2Details: "For each participant:", + guideStep2Item1: "Enter their name (required)", + guideStep2Item2: "Add their email for notifications (optional)", + guideStep2Item3: "Optionally add a gift wish for them - they'll see it and can keep or change it", + guideStep2Item4: "Tip: Don't forget to add yourself if you want to participate!", + guideStep3Title: "Step 3: Configure Options", + guideStep3Desc: "Customize how your Zava Gift Exchange game works.", + guideStep3Option1: "Allow Reassignment", + guideStep3Option1Desc: "Let participants request a new assignment if needed", + guideStep3Option2: "Protect Participant List", + guideStep3Option2Desc: "Each participant gets a unique link - they won't see other participants", + guideStep3Option3: "Email Notifications", + guideStep3Option3Desc: "Automatically send assignments to participants via email", + guideStep4Title: "Step 4: Share Links", + guideStep4Desc: "After creating the game, share the links with your participants.", + guideStep4Item1: "For protected games: Share each unique link individually", + guideStep4Item2: "For non-protected games: Share the game code with everyone", + guideStep4Item3: "Save your organizer link - you'll need it to manage the event!", + guideOrganizerPanelTitle: "Organizer Panel Features", + guideOrganizerPanelDesc: "Once your game is created, you can manage everything from the organizer panel.", + guideFeatureStats: "View Statistics", + guideFeatureStatsDesc: "See how many participants have confirmed, added wishes, or requested reassignment.", + guideFeatureManage: "Manage Participants", + guideFeatureManageDesc: "Add new participants, edit details, or remove someone from the game. When adding new participants, confirmed assignments are preserved.", + guideFeatureReassign: "Handle Reassignment Requests", + guideFeatureReassignDesc: "Reassign any individual participant or all at once. Approve or deny reassignment requests from participants. If too many participants have confirmed, you may need to use 'Reassign All'.", + guideFeatureLinks: "Regenerate Links", + guideFeatureLinksDesc: "If a participant loses their link, you can generate a new one for them.", + guideFeatureOrganizerToken: "Regenerate Your Access Link", + guideFeatureOrganizerTokenDesc: "If you suspect your organizer link was compromised or want to secure your account, you can regenerate it. A new link will be sent to your email and the old one will stop working immediately. This feature requires email service to be configured.", + guideFeatureDelete: "Archive Game", + guideFeatureDeleteDesc: "Archive the game and all data when it's no longer needed. Data can be recovered by administrators.", + guideNewFeaturesTitle: "Additional Features", + guideFeatureQrCode: "QR Code Sharing", + guideFeatureQrCodeDesc: "When a game is created, a QR code is generated for the invitation link. It is labeled as invite-only. Participants can scan it with their phone camera to join instantly — perfect for sharing in person at meetings or parties.", + guideFeatureExclusions: "Exclusion Rules", + guideFeatureExclusionsDesc: "Prevent specific pairs from being matched. For example, exclude couples or family members so they don't draw each other. This feature is currently available via the API and UI configuration is planned for a future update.", + guideFeatureDarkMode: "Dark Mode", + guideFeatureDarkModeDesc: "Toggle between light and dark themes using the moon/sun icon in the header. Your preference is saved and persists across visits. The app also respects your operating system's theme preference.", + guideFeatureCountdown: "Event Countdown", + guideFeatureCountdownDesc: "A live countdown timer shows how much time is left until the event. It updates automatically and appears on the assignment page so you never miss the date.", + guideFeatureCalendar: "Add to Calendar", + guideFeatureCalendarDesc: "Download a .ics calendar file to add the Zava Gift Exchange event to your calendar app (Google Calendar, Outlook, Apple Calendar, etc.). Available on the assignment page.", + guideTipsTitle: "Tips for Organizers", + guideTip1: "Create the game at least a few days before the event to give participants time", + guideTip2: "Use the protected mode for larger groups to keep assignments private", + guideTip3: "Send reminders as the event date approaches", + guideTip4: "Games are automatically archived 3 days after the event date", + + // Participant Guide + guideParticipantTitle: "Participant Guide", + guideParticipantSubtitle: "Learn how to join and participate in Zava Gift Exchange", + guideJoinTitle: "How to Join", + guideJoinDesc: "There are two ways to join a Zava Gift Exchange game:", + guideJoinOption1Title: "Option 1: Direct Link", + guideJoinOption1Desc: "Click the link shared by your organizer. You'll be taken directly to your assignment.", + guideJoinOption2Title: "Option 2: Game Code", + guideJoinOption2Desc: "Enter the 6-digit game code on the home page, then select your name from the list.", + guideViewAssignmentTitle: "View Your Assignment", + guideViewAssignmentDesc: "Once you join, you'll see who you're buying a gift for. If your assignment is still being prepared, you'll see a pending assignment page until the organizer finalizes assignments.", + guideViewAssignmentItem1: "See the person's name you're gifting to", + guideViewAssignmentItem2: "View their wish list if they've added one", + guideViewAssignmentItem3: "See event details like date, location, and gift amount", + guideWishTitle: "Add Your Gift Wish", + guideWishDesc: "Help your Zava Gift Exchange by adding what you'd like to receive!", + guideWishItem1: "If the organizer added a wish for you, it will appear automatically - you can keep it or change it", + guideWishItem2: "Be specific but flexible - give options at different price points", + guideWishItem3: "You can edit your wish at any time", + guideWishItem4: "When you change your wish, your Zava Gift Exchange will be notified by email (if they have provided an email address)", + guideReassignTitle: "Request Reassignment", + guideReassignDesc: "Not happy with your assignment? You might be able to request a change.", + guideReassignItem1: "Only available if the organizer enabled this option", + guideReassignItem2: "The organizer must approve your request", + guideReassignItem3: "Cannot be requested after confirming your assignment", + guideConfirmTitle: "Confirm Your Assignment", + guideConfirmDesc: "Let the organizer know you've seen your assignment by confirming it. This helps them track participation.", + guideProtectedTitle: "About Protected Games", + guideProtectedDesc: "Some organizers use protected mode for extra privacy:", + guideProtectedItem1: "You receive a unique personal link", + guideProtectedItem2: "You can only see your own assignment", + guideProtectedItem3: "Keep your link private - don't share it with others", + guideEmailTitle: "Email Notifications", + guideEmailDesc: "If you provided your email, you may receive notifications about the event, reminders, or your assignment details.", + guideParticipantTipsTitle: "Tips for Participants", + guideParticipantTip1: "Add your wish early so your Zava Gift Exchange has time to shop", + guideParticipantTip2: "Keep your assignment secret - that's the fun part!", + guideParticipantTip3: "Bookmark your link or save the game code for easy access", +} 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 { From 4818def0a993d09dfe3452704e943cf3390c57ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:11:44 +0000 Subject: [PATCH 3/3] fix: complete vietnamese localization and email fallback handling --- api/src/__tests__/email-service.test.ts | 10 +- api/src/shared/email-service.ts | 6 +- api/src/shared/types.ts | 2 +- src/lib/translations/vi.ts | 986 ++++++++++++------------ 4 files changed, 503 insertions(+), 501 deletions(-) diff --git a/api/src/__tests__/email-service.test.ts b/api/src/__tests__/email-service.test.ts index 3603207..105e15b 100644 --- a/api/src/__tests__/email-service.test.ts +++ b/api/src/__tests__/email-service.test.ts @@ -121,10 +121,12 @@ describe('Email Service', () => { participants: [{ id: '1' }, { id: '2' }, { id: '3' }] } as any - const content = generateOrganizerEmailContent({ game, language: 'vi' }) - expect(content.subject).toContain('Holiday Exchange') - expect(content.subject).toContain('Your Zava Gift Exchange game') - expect(content.html).toContain('Game details') + 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 021fc2d..11a0782 100644 --- a/api/src/shared/email-service.ts +++ b/api/src/shared/email-service.ts @@ -17,10 +17,10 @@ export function escapeHtml(text: string | undefined): string { // Helper function to get translation with fallback to English function getTranslation(translations: Partial>, language: Language): T { const translation = translations[language] ?? translations.en - if (translation) { - return translation + if (!translation) { + throw new Error(`English email translation is missing for language "${language}"`) } - throw new Error('English email translation is missing') + return translation } // Email notification types diff --git a/api/src/shared/types.ts b/api/src/shared/types.ts index 27e851d..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' | 'vi' +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/vi.ts b/src/lib/translations/vi.ts index 7137cd9..f5b7047 100644 --- a/src/lib/translations/vi.ts +++ b/src/lib/translations/vi.ts @@ -1,547 +1,547 @@ export const vi = { appName: "Zava Gift Exchange", - welcome: "Welcome!", - welcomeDesc: "Organize your gift exchange easily and fun", - loading: "Loading...", - createGame: "Create New Game", - joinGame: "Join a Game", - enterCode: "Enter the code", - codePlaceholder: "6-digit code", - continue: "Continue", - back: "Back", - next: "Next", - finish: "Finish", - cancel: "Cancel", - confirm: "Confirm", - yes: "Yes", - no: "No", + 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: "Event Details", - step2: "Participants", - step3: "Configuration", + step1: "Chi tiết sự kiện", + step2: "Người tham gia", + step3: "Cấu hình", - eventName: "Event Name", - eventNamePlaceholder: "E.g: Office Christmas 2025", - giftAmount: "Gift Amount", - giftAmountPlaceholder: "E.g: 20", - currency: "Currency", - selectCurrency: "Select currency", - eventDate: "Event Date", - eventTime: "Event Time", - eventLocation: "Event Location", - eventLocationPlaceholder: "E.g: Main Office", + 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: "Participants", - participantsDesc: "Add at least 3 participants", - participantName: "Participant name", - participantNamePlaceholder: "E.g: Mary Smith", - participantEmail: "Participant email", - participantEmailPlaceholder: "E.g: mary@email.com (Tab to continue)", - participantEmailOptional: "(Optional - for notifications)", - yourEmail: "Your Email", - yourEmailPlaceholder: "E.g: youremail@email.com", - yourEmailDesc: "Optional - to receive event notifications", - emailUpdated: "Email updated!", - tabToAddEmail: "Tab to add email", - tabToAddGift: "Tab to add gift", - tabToAddAnother: "Tab/Enter to add another", - tabToFinish: "Tab/Enter to finish", - pressTabOrEnter: "Press Tab or Enter", - desiredGift: "Desired Gift", - desiredGiftPlaceholder: "E.g: Book, video game, clothing...", - desiredGiftOptional: "(Optional)", - theirDesiredGift: "Gift they want:", - addParticipant: "Add Participant", - removeParticipant: "Remove", - minParticipants: "At least 3 participants needed", - organizerNote: "Note: If you want to play, you'll need to add yourself to the participant list!", - regenerateToken: "Regenerate Link", - regenerateTokenTitle: "Regenerate participant link?", - regenerateTokenDesc: "A new unique link will be created for this participant. The old link will no longer work.", - regenerateTokenConfirm: "Generate New Link", - tokenRegenerated: "New link created!", + 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: "Allow Reassignment", - allowReassignmentDesc: "Participants can request a new assignment (organizer must approve)", - protectParticipants: "Protect Participant List", - protectParticipantsDesc: "Each participant receives a unique link. They won't be able to see other participants' data.", - protectParticipantsWarning: "⚠️ You'll need to share each link individually with each participant", - generalNotes: "General Notes and Instructions", - generalNotesPlaceholder: "E.g: Bring the gift wrapped. Don't spend more than agreed amount.", + 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: "Email Notifications", - emailNotificationsDesc: "Receive game details via email", - organizerEmail: "Your Email (Organizer)", - organizerEmailPlaceholder: "E.g: youremail@email.com", - organizerEmailDesc: "You'll receive the game links and organizer panel", - sendEmailsOnCreate: "Send emails when creating game", - sendEmailsOnCreateDesc: "Automatically send details to all participants with email", - sendEmailNotifications: "Send Email Notifications", - sendEmailNotificationsDesc: "Send emails to organizer and participants when creating the game", - creating: "Creating...", - emailSentToOrganizer: "Email sent to organizer!", - emailsSentToParticipants: "{count} emails sent to participants", + 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: "Game Created!", - gameCreatedDesc: "Share this link with all participants", - gameCode: "Game Code", - copyCode: "Copy Code", - codeCopied: "Code copied!", - participantLink: "Participant Link", - participantLinkDesc: "Share this link with all gift exchange participants", - individualParticipantLinks: "Individual Participant Links", - individualParticipantLinksDesc: "Each participant has a unique link. Share each link only with its intended recipient.", - copyParticipantLink: "Copy Link", - participantLinkCopied: "Link copied!", - shareOnSocialMedia: "Share on Social Media", - shareMessageTitle: "Join our Zava Gift Exchange!", - shareMessage: "Hi! Join our Zava Gift Exchange \"{eventName}\". Click here: {link}", - shareOnWhatsApp: "Share on WhatsApp", - shareOnFacebook: "Share on Facebook", - shareOnX: "Share on X", - shareOnMessenger: "Share on Messenger", - organizerLink: "Organizer Link", - copyOrganizerLink: "Copy Link", - linkCopied: "Link copied!", - copyNewLink: "Copy New Link", - done: "Done", - organizerLinkDesc: "Save this link to view statistics and manage the event", - emailNotConfiguredSaveLink: "Email service not configured. Save this link to manage the event", - useOrganizerPanelLink: "Use this link to access the organizer panel", - emailAlreadyExists: "This email is already used by another participant", - invalidEmailFormat: "Please enter a valid email address", - invalidDate: "Invalid date. Please enter a valid calendar date (e.g., not February 31 or April 31)", - participantNameExists: "This participant name already exists", - goToGame: "Go to Organizer Panel", + 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: "Invitation Link", - invitationLinkDesc: "Share this link to allow new participants to join the game", - copyInvitationLink: "Copy Invitation Link", - invitationLinkCopied: "Invitation link copied!", - joinViaInvitation: "Join via Invitation", - joinGameTitle: "Join Zava Gift Exchange", - joinGameDesc: "Fill in your information to join this gift exchange", - yourName: "Your Name", - yourNamePlaceholder: "E.g: John Smith", - yourGiftWish: "Your Gift Wish (Optional)", - yourGiftWishPlaceholder: "Any specific details about what you'd like...", - joinGameButton: "Join Game", - joining: "Joining...", - joinSuccess: "Successfully joined!", - joinError: "Failed to join game", - invalidInvitationToken: "Invalid or expired invitation link", - participantNameAlreadyExists: "This name is already taken in this game", - emailAlreadyInUse: "This email is already in use in this game", + 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: "Send Organizer Email", - sendOrganizerEmailDesc: "Receive game details in your email", - sendParticipantEmails: "Send Participant Emails", - sendParticipantEmailsDesc: "Send assignments to participants with email", - emailSent: "Email sent!", - emailsSent: "emails sent", - emailsFailed: "emails failed", - sendingEmail: "Sending email...", - sendingEmails: "Sending emails...", - emailServiceNotConfigured: "Email service is not configured", - noParticipantsWithEmail: "No participants have email configured", + 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: "Select Your Name", - selectParticipantDesc: "Choose your name from the list", - invalidCode: "Code not found", - invalidCodeDesc: "Check the code with the organizer", + 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: "Your Assignment", - youGiftTo: "You're gifting to:", - theirWish: "Their wish:", - noWishYet: "Haven't added a wish yet", - addYourWish: "Add My Wish", - yourWish: "Your Gift Wish", - yourWishPlaceholder: "E.g: I'd like a science fiction book", - saveWish: "Save Wish", - wishSaved: "Wish saved!", - editWish: "Edit Wish", - wishDescription: "Tell your Zava Gift Exchange what you'd like to receive", - wishHint: "This wish will be visible to whoever is gifting you", - skipWish: "Skip", - saveAndContinue: "Save and Continue", - updateAndContinue: "Update and Continue", - hasWish: "Has wish", - noWish: "No wish", - noWishAddedYet: "You haven't added your gift wish yet", - wishDialogDesc: "Write what you'd like to receive as a gift", - wishChangeWarningTitle: "Wish Change Warning", - wishChangeWarningDesc: "Your gift giver has already confirmed their assignment. If you change your wish now, they will be notified by email so they can see your updated preference.", - proceedWithChange: "Proceed with Change", - wishesAdded: "Wishes Added", - giftWish: "Gift Wish", - giftWishDesc: "What this participant would like to receive", + 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: "Event Details", - amount: "Amount", - date: "Date", - location: "Location", - instructions: "Instructions", - noInstructions: "No additional instructions", - notSpecified: "Not specified", + 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: "Request New Assignment", - requestReassignmentTitle: "Request New Assignment?", - requestReassignmentDesc: "Your request will be sent to the organizer for approval.", - cannotReassignConfirmed: "You cannot request a reassignment after confirming your assignment", - reassignmentSuccess: "Request sent! The organizer will review your request.", - reassignmentUsed: "You already have a pending request", - reassignmentNotAllowed: "Organizer doesn't allow reassignments", - reassignmentPending: "Request pending", - reassignmentPendingDesc: "The organizer will review your request soon.", + 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: "Pending Reassignment Requests", - noPendingRequests: "No pending requests", - approveReassignment: "Approve", - cancelRequest: "Cancel Request", - reassignAll: "Reassign All", - reassignAllDesc: "Generate new assignments for unconfirmed participants only", - confirmedParticipantsLocked: "participant(s) with confirmed assignments will keep their current assignments", - unconfirmedWillReceiveNew: "participant(s) will receive new assignments", - confirmedParticipantsWarning: "participant(s) have confirmed their assignment", - confirmedParticipantsWarningDesc: "Their confirmation status will be cleared when reassignments are generated.", - forceReassignParticipant: "Force Reassign", - forceReassignConfirm: "Force Reassign Participant?", - forceReassignWarning: "This participant has confirmed their assignment.", - forceReassignWarningDesc: "They will receive a new assignment and will be notified by email (if configured). This action cannot be undone.", - forceReassignSuccess: "Participant reassigned and notified", - approveAllPending: "Approve All Pending", - approveAllPendingDesc: "Approve all pending reassignment requests. Each participant will receive a new assignment.", - allPendingApproved: "All pending requests were approved", - reassignmentApproved: "Reassignment approved", - reassignmentFailed: "Cannot reassign: no valid swap available. Try regenerating all assignments.", - reassignmentFailedSuggestAll: "Cannot reassign this participant individually. Too many participants have confirmed. Try 'Reassign All' instead.", - reassignmentRequestCancelled: "Request cancelled", - allReassigned: "All assignments were regenerated", - requestedAt: "Requested", - hasPendingRequest: "Request pending", - reassignParticipant: "Reassign", - pendingAssignmentSingular: "has no assignment yet", - pendingAssignmentPlural: "have no assignments yet", - pendingAssignment: "Pending assignment", - pendingAssignmentDesc: "Your assignment is being prepared. The organizer will finalize assignments soon.", - allConfirmedReassignNeeded: "All other participants have confirmed their assignments. You need to use 'Reassign All' to include new participants.", - qrInviteOnly: "Scan to invite new participants", - qrCodeAriaLabel: "Invitation QR code", + 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: "Download Card", - downloadingCard: "Generating card...", - cardDownloaded: "Card downloaded!", - eventCountdown: "Time until event", - addToCalendar: "Add to Calendar", - darkMode: "Dark mode", - lightMode: "Light mode", + 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: "Organizer Panel", - organizerPanelDesc: "This link is exclusive for game organizers", - organizerOnly: "🔒 Organizers Only", - statistics: "Statistics", - addedWish: "Have added their wish", - usedReassignment: "Have used reassignment", - editEvent: "Edit Event", - shareCode: "Share Code", - gameManagement: "Game Management", - participantStatus: "Participant Status", - hasConfirmed: "Confirmed", - notConfirmed: "Not confirmed", - hasReassigned: "Reassigned", - hasEmail: "Has email", - noEmail: "No email", - addNewParticipant: "Add New Participant", - confirmAssignment: "Confirm assignment", - confirmAssignmentDesc: "Confirm that you received your assignment and are ready to participate", - assignmentConfirmed: "Assignment confirmed!", - assignmentsConfirmed: "Assignments Confirmed", - removeParticipantConfirm: "Remove participant?", - participantRemoved: "Participant removed", - participantAdded: "Participant added", - saveChanges: "Save Changes", - changesSaved: "Changes saved", - totalParticipants: "Total Participants", - reassignmentsUsed: "Reassignments Used", - editGameDetails: "Edit Game Details", - gameUpdated: "Game updated", - noParticipantsYet: "No participants yet", - assignedTo: "Assigned to", - editParticipant: "Edit Participant", - editParticipantDesc: "Modify participant details", - participantUpdated: "Participant updated!", + 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: "Invalid organizer link", + invalidOrganizer: "Liên kết người tổ chức không hợp lệ", - errorTitle: "Something went wrong", - tryAgain: "Try again", + errorTitle: "Đã xảy ra lỗi", + tryAgain: "Thử lại", - apiUnavailableWarning: "⚠️ API Unavailable", - apiUnavailableDesc: "Server is not available. Data is only saved locally in this browser.", - databaseUnavailableWarning: "⚠️ Database Unavailable", - databaseUnavailableDesc: "Server cannot connect to database. Data will not be saved permanently.", - gameCreatedLocally: "Game created in demo mode (local storage only)", + 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: "Protected Game", - protectedGameDesc: "This game requires a unique link for each participant. Please use the link shared by the organizer.", - invalidParticipantToken: "Invalid participant token. Check the link with the organizer.", - copyIndividualLink: "Copy Link", - individualLinkCopied: "Link copied!", + 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: "Refresh data", - dataRefreshed: "Data refreshed!", - apiUnavailable: "Server is not available", + 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: "Send Reminder", - sendReminderAll: "Send to All", - sendReminderToParticipant: "Send Reminder", - reminderSent: "Reminder sent!", - remindersSent: "reminders sent", - remindersFailed: "reminders failed", - sendingReminder: "Sending reminder...", - sendingReminders: "Sending reminders...", - customMessage: "Custom Message", - customMessagePlaceholder: "Optional message to include in the reminder...", - reminderEmailDesc: "Send a reminder with event details", - reminderToAllDesc: "Send a reminder to all participants with email", - noEmailConfigured: "No email configured", + 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: "Export Participants", - exportParticipantsDesc: "Download participant list with selected details", - exportDialogTitle: "Export Participant List", - exportDialogDesc: "Choose which details to include in the export", - exportIncludeAssignments: "Include Assignments", - exportIncludeGameDetails: "Include Game Details", - exportIncludeWishes: "Include Wishes", - exportIncludeInstructions: "Include Instructions", - exportIncludeConfirmationStatus: "Include Confirmation Status", - exportIncludeEmails: "Include Emails", - exportButton: "Export as CSV", - exportingData: "Exporting...", - exportSuccess: "Participants exported!", - exportError: "Failed to export participants", - exportNoFieldsSelected: "Please select at least one field to export", - exportHeaderName: "Name", + 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: "Assigned To", - exportHeaderGiftWish: "Gift Wish", - exportHeaderConfirmed: "Confirmed", - exportHeaderEventName: "Event Name", - exportHeaderAmount: "Amount", - exportHeaderCurrency: "Currency", - exportHeaderDate: "Date", - exportHeaderLocation: "Location", - exportHeaderInstructions: "Instructions", + 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: "Privacy Policy", - privacySubtitle: "How we handle your data", - privacyDataCollectionTitle: "Data We Collect", - privacyDataCollectionDesc: "When using Zava Gift Exchange, we collect the following information to make the gift exchange work:", - privacyDataItem1: "Participant names", - privacyDataItem2: "Email addresses (optional, for notifications)", - privacyDataItem3: "Gift wishes (optional)", - privacyDataItem4: "Event details (date, location, gift amount)", - privacyDataUsageTitle: "Data Usage", - privacyDataUsageDesc: "Your data is stored in a secure database and used exclusively for the operation of this application. This includes:", - privacyDataUsagePromise: "Your game data is stored securely and used exclusively for the operation of this application. We do not sell or share your information for marketing purposes.", - privacyDataRetentionTitle: "Data Retention", - privacyDataRetentionDesc: "To protect your privacy, we implement the following retention policies:", - privacyDataRetentionItem1: "Games are automatically archived 3 days after the event date", - privacyDataRetentionItem2: "Archived game data is permanently deleted after 30 days", - privacyDataRetentionItem3: "Organizers can archive all game data at any time", - privacyThirdPartiesTitle: "Third Parties", - privacyThirdPartiesDesc: "We use Azure Communication Services to send notification emails when you explicitly request it. We use Azure Application Insights to monitor application health, track errors, and ensure reliability — this telemetry is automatic and does not require consent.", - privacySecurityTitle: "Security & Data Protection", - privacySecurityDesc: "We take the security of your data seriously. The following measures protect your information:", - privacySecurityItem1: "All tokens and game codes are generated using cryptographically secure randomness", - privacySecurityItem2: "Token comparisons use timing-safe algorithms to prevent side-channel attacks", - privacySecurityItem3: "API rate limiting protects against abuse and brute-force attempts", - privacySecurityItem4: "Input validation and length limits are enforced on all data fields", - privacySecurityItem5: "The app can be installed as a PWA; cached data is limited to static assets and never includes personal information", - privacyLastUpdated: "Last updated", - privacyLink: "Privacy Policy", + 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: "Game Not Found", - gameNotFoundDesc: "The game you're looking for doesn't exist or has been archived.", - gameNotFoundReason1: "The game may have been archived by the organizer", - gameNotFoundReason2: "Games are automatically archived 3 days after the event", - gameNotFoundReason3: "The game code might be incorrect", - goHome: "Go Home", + 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: "Archive Game", - deleteGameTitle: "Archive this game?", - deleteGameWarning: "⚠️ The game will no longer be accessible to participants", - deleteGameDesc: "The game and its data will be archived, including:", - deleteGameItem1: "All participant information", - deleteGameItem2: "Gift assignments", - deleteGameItem3: "Wishes and preferences", - deleteGameItem4: "Request history", - deleteGameConfirm: "Archive Game", - gameDeleted: "Game archived successfully", + 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: "Regenerate Organizer Access", - regenerateOrganizerTokenLinkTitle: "Regenerate your access link?", - regenerateOrganizerTokenLinkDesc: "A new organizer link will be sent to your email. Your current access will stop working immediately.", - regenerateOrganizerTokenLinkWarning: "⚠️ You will be logged out after regenerating", - regenerateOrganizerTokenLinkConfirm: "Regenerate & Send Email", - organizerTokenRegenerated: "New organizer link sent to your email!", - checkEmailForNewLink: "Check your email for the new organizer link", - organizerTokenRequiresEmail: "Email service required to 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: "Event date must be today or in the future", + dateInPast: "Ngày sự kiện phải là hôm nay hoặc trong tương lai", // Error pages - errorPageTitle: "Oops, something went wrong!", - errorInvalidToken: "Link no longer valid", - errorInvalidTokenDesc: "The link you used is invalid or has expired. If you have a new link or token, enter it below.", - errorProtectedGame: "Protected game", - errorProtectedGameDesc: "This game requires a unique access token. If you have your token, enter it below.", - errorUnexpected: "An unexpected error occurred", - errorUnexpectedDesc: "We couldn't complete your request. Please try again or contact the organizer.", - enterAccessToken: "Enter your access token", - accessTokenPlaceholder: "Paste your token here", - accessTokenHint: "The token is the part after 'participant=' or 'organizer=' in your link", - submitToken: "Access Game", - orAskOrganizer: "Or ask the organizer for a new link", + 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: "Participant Access", - noTokenQuestion: "Don't have a token?", - contactOrganizerForLink: "Ask the organizer to send you your personal link.", - participantRecoveryOption: "If you registered with an email, you can recover your link.", - recoverParticipantLink: "Recover My Link", - participantRecoveryDesc: "Enter the email you used when registering. If it matches, we'll send you your access link.", - participantEmailRecoveryPlaceholder: "Enter your email", - participantRecoveryEmailSentDesc: "If the email matches a participant, a recovery link has been sent. Check your inbox.", - organizerAccessTitle: "Organizer Access", - organizerForgotLink: "Are you the organizer and lost your link?", - recoverOrganizerLink: "Recover Organizer Link", - organizerRecoveryTitle: "Recover Organizer Access", - organizerRecoveryDesc: "Enter the email you used when creating the game. If it matches, we'll send you the organizer link.", - recoveryEmailLabel: "Your Email", - recoveryEmailPlaceholder: "Enter organizer email", - sendRecoveryEmail: "Send Recovery Email", - sending: "Sending...", - recoveryEmailSent: "Recovery email sent!", - recoveryEmailSentDesc: "If the email matches the organizer's email, a recovery link has been sent. Check your inbox.", - recoveryNotAvailable: "No email registered for this game's organizer", - recoveryFailed: "Failed to send recovery email. Please try again.", - recoveryNotAvailableNoEmail: "Email recovery is not available for this game. Please contact the organizer directly if you have lost your link.", + 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: "Organizer Guide", - guideParticipantLink: "Participant Guide", + guideOrganizerLink: "Hướng dẫn cho người tổ chức", + guideParticipantLink: "Hướng dẫn cho người tham gia", // Organizer Guide - guideOrganizerTitle: "Organizer Guide", - guideOrganizerSubtitle: "Learn how to create and manage your Zava Gift Exchange event", - guideStep1Title: "Step 1: Create Your Game", - guideStep1Desc: "Click 'Create New Game' on the home page to start setting up your gift exchange.", - guideStep1Details: "You'll need to provide:", - guideStep1Item1: "Event name (e.g., 'Office Christmas 2025')", - guideStep1Item2: "Gift amount and currency", - guideStep1Item3: "Event date, time, and location", - guideStep1Item4: "Optional notes or instructions for participants", - guideStep2Title: "Step 2: Add Participants", - guideStep2Desc: "Add at least 3 participants to your gift exchange. The more the merrier!", - guideStep2Details: "For each participant:", - guideStep2Item1: "Enter their name (required)", - guideStep2Item2: "Add their email for notifications (optional)", - guideStep2Item3: "Optionally add a gift wish for them - they'll see it and can keep or change it", - guideStep2Item4: "Tip: Don't forget to add yourself if you want to participate!", - guideStep3Title: "Step 3: Configure Options", - guideStep3Desc: "Customize how your Zava Gift Exchange game works.", - guideStep3Option1: "Allow Reassignment", - guideStep3Option1Desc: "Let participants request a new assignment if needed", - guideStep3Option2: "Protect Participant List", - guideStep3Option2Desc: "Each participant gets a unique link - they won't see other participants", - guideStep3Option3: "Email Notifications", - guideStep3Option3Desc: "Automatically send assignments to participants via email", - guideStep4Title: "Step 4: Share Links", - guideStep4Desc: "After creating the game, share the links with your participants.", - guideStep4Item1: "For protected games: Share each unique link individually", - guideStep4Item2: "For non-protected games: Share the game code with everyone", - guideStep4Item3: "Save your organizer link - you'll need it to manage the event!", - guideOrganizerPanelTitle: "Organizer Panel Features", - guideOrganizerPanelDesc: "Once your game is created, you can manage everything from the organizer panel.", - guideFeatureStats: "View Statistics", - guideFeatureStatsDesc: "See how many participants have confirmed, added wishes, or requested reassignment.", - guideFeatureManage: "Manage Participants", - guideFeatureManageDesc: "Add new participants, edit details, or remove someone from the game. When adding new participants, confirmed assignments are preserved.", - guideFeatureReassign: "Handle Reassignment Requests", - guideFeatureReassignDesc: "Reassign any individual participant or all at once. Approve or deny reassignment requests from participants. If too many participants have confirmed, you may need to use 'Reassign All'.", - guideFeatureLinks: "Regenerate Links", - guideFeatureLinksDesc: "If a participant loses their link, you can generate a new one for them.", - guideFeatureOrganizerToken: "Regenerate Your Access Link", - guideFeatureOrganizerTokenDesc: "If you suspect your organizer link was compromised or want to secure your account, you can regenerate it. A new link will be sent to your email and the old one will stop working immediately. This feature requires email service to be configured.", - guideFeatureDelete: "Archive Game", - guideFeatureDeleteDesc: "Archive the game and all data when it's no longer needed. Data can be recovered by administrators.", - guideNewFeaturesTitle: "Additional Features", - guideFeatureQrCode: "QR Code Sharing", - guideFeatureQrCodeDesc: "When a game is created, a QR code is generated for the invitation link. It is labeled as invite-only. Participants can scan it with their phone camera to join instantly — perfect for sharing in person at meetings or parties.", - guideFeatureExclusions: "Exclusion Rules", - guideFeatureExclusionsDesc: "Prevent specific pairs from being matched. For example, exclude couples or family members so they don't draw each other. This feature is currently available via the API and UI configuration is planned for a future update.", - guideFeatureDarkMode: "Dark Mode", - guideFeatureDarkModeDesc: "Toggle between light and dark themes using the moon/sun icon in the header. Your preference is saved and persists across visits. The app also respects your operating system's theme preference.", - guideFeatureCountdown: "Event Countdown", - guideFeatureCountdownDesc: "A live countdown timer shows how much time is left until the event. It updates automatically and appears on the assignment page so you never miss the date.", - guideFeatureCalendar: "Add to Calendar", - guideFeatureCalendarDesc: "Download a .ics calendar file to add the Zava Gift Exchange event to your calendar app (Google Calendar, Outlook, Apple Calendar, etc.). Available on the assignment page.", - guideTipsTitle: "Tips for Organizers", - guideTip1: "Create the game at least a few days before the event to give participants time", - guideTip2: "Use the protected mode for larger groups to keep assignments private", - guideTip3: "Send reminders as the event date approaches", - guideTip4: "Games are automatically archived 3 days after the event date", + 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: "Participant Guide", - guideParticipantSubtitle: "Learn how to join and participate in Zava Gift Exchange", - guideJoinTitle: "How to Join", - guideJoinDesc: "There are two ways to join a Zava Gift Exchange game:", - guideJoinOption1Title: "Option 1: Direct Link", - guideJoinOption1Desc: "Click the link shared by your organizer. You'll be taken directly to your assignment.", - guideJoinOption2Title: "Option 2: Game Code", - guideJoinOption2Desc: "Enter the 6-digit game code on the home page, then select your name from the list.", - guideViewAssignmentTitle: "View Your Assignment", - guideViewAssignmentDesc: "Once you join, you'll see who you're buying a gift for. If your assignment is still being prepared, you'll see a pending assignment page until the organizer finalizes assignments.", - guideViewAssignmentItem1: "See the person's name you're gifting to", - guideViewAssignmentItem2: "View their wish list if they've added one", - guideViewAssignmentItem3: "See event details like date, location, and gift amount", - guideWishTitle: "Add Your Gift Wish", - guideWishDesc: "Help your Zava Gift Exchange by adding what you'd like to receive!", - guideWishItem1: "If the organizer added a wish for you, it will appear automatically - you can keep it or change it", - guideWishItem2: "Be specific but flexible - give options at different price points", - guideWishItem3: "You can edit your wish at any time", - guideWishItem4: "When you change your wish, your Zava Gift Exchange will be notified by email (if they have provided an email address)", - guideReassignTitle: "Request Reassignment", - guideReassignDesc: "Not happy with your assignment? You might be able to request a change.", - guideReassignItem1: "Only available if the organizer enabled this option", - guideReassignItem2: "The organizer must approve your request", - guideReassignItem3: "Cannot be requested after confirming your assignment", - guideConfirmTitle: "Confirm Your Assignment", - guideConfirmDesc: "Let the organizer know you've seen your assignment by confirming it. This helps them track participation.", - guideProtectedTitle: "About Protected Games", - guideProtectedDesc: "Some organizers use protected mode for extra privacy:", - guideProtectedItem1: "You receive a unique personal link", - guideProtectedItem2: "You can only see your own assignment", - guideProtectedItem3: "Keep your link private - don't share it with others", - guideEmailTitle: "Email Notifications", - guideEmailDesc: "If you provided your email, you may receive notifications about the event, reminders, or your assignment details.", - guideParticipantTipsTitle: "Tips for Participants", - guideParticipantTip1: "Add your wish early so your Zava Gift Exchange has time to shop", - guideParticipantTip2: "Keep your assignment secret - that's the fun part!", - guideParticipantTip3: "Bookmark your link or save the game code for easy access", + 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", }