feat: fix dutch translation
This commit is contained in:
+257
-124
@@ -2,6 +2,7 @@ import { createContext, useContext, useState, useEffect, ReactNode } from 'react
|
||||
|
||||
type Lang = 'en' | 'nl';
|
||||
|
||||
// Dutch wording follows the bar app: a tab is a "poef", informal "je"
|
||||
const translations: Record<Lang, Record<string, string>> = {
|
||||
en: {
|
||||
'nav.overview': 'Overview',
|
||||
@@ -13,31 +14,72 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'nav.logout': 'Logout',
|
||||
'realtime.live': 'Live updates on',
|
||||
'realtime.offline': 'Live updates unavailable — refreshing periodically',
|
||||
'common.refresh': 'Refresh',
|
||||
'common.loading': 'Loading...',
|
||||
'common.error': 'Something went wrong',
|
||||
'common.unknown_error': 'An error occurred',
|
||||
'common.failed': 'Failed',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.close': 'Close',
|
||||
'common.copied': 'Copied',
|
||||
'common.logged_out': 'Logged out',
|
||||
'time.just_now': 'just now',
|
||||
'time.minutes_ago': '{count}m ago',
|
||||
'time.hours_ago': '{count}h ago',
|
||||
'time.days_ago': '{count}d ago',
|
||||
'status.pending': 'Pending',
|
||||
'status.completed': 'Completed',
|
||||
'status.failed': 'Failed',
|
||||
'status.conflict': 'Conflict',
|
||||
'status.expired': 'Expired',
|
||||
'device_status.online': 'Online',
|
||||
'device_status.recently_seen': 'Recently seen',
|
||||
'device_status.stale': 'Stale',
|
||||
'device_status.offline': 'Offline',
|
||||
'device_status.revoked': 'Revoked',
|
||||
'payment.cash': 'Cash',
|
||||
'payment.payconiq': 'Payconiq',
|
||||
'payment.payment_pending': 'Payment pending',
|
||||
'tabs.title': 'Open Tabs',
|
||||
'tabs.count': '{count} open tab',
|
||||
'tabs.count_plural': '{count} open tabs',
|
||||
'tabs.empty': 'No open tabs',
|
||||
'tabs.empty_desc': 'Open tabs will appear here when the terminal syncs.',
|
||||
'tabs.empty_desc': 'Open tabs appear here when the terminal syncs.',
|
||||
'tabs.load_failed': 'Failed to load tabs',
|
||||
'tabs.customer': 'Customer',
|
||||
'tabs.email': 'Email',
|
||||
'tabs.amount': 'Amount',
|
||||
'tabs.items': 'Items',
|
||||
'tabs.version': 'Version',
|
||||
'tabs.reported': 'Last Reported',
|
||||
'tabs.reported': 'Last updated',
|
||||
'tabs.actions': 'Actions',
|
||||
'tabs.close': 'Close',
|
||||
'tabs.show_items': 'Show items for {name}',
|
||||
'tabs.hide_items': 'Hide items for {name}',
|
||||
'tabs.lines_unknown': "Items aren't reported by this terminal yet. Update the app on the tablet to see them.",
|
||||
'tabs.lines_empty': 'No items on this tab.',
|
||||
'tabs.each': 'each',
|
||||
'tabs.close_title': "Close {name}'s tab?",
|
||||
'tabs.close_desc': 'Send a close request to the terminal.',
|
||||
'tabs.payment': 'Payment method',
|
||||
'tabs.cash': 'Cash',
|
||||
'tabs.payconiq': 'Payconiq',
|
||||
'tabs.pending_payment': 'Payment pending',
|
||||
'tabs.select_payment': 'Please select a payment method',
|
||||
'tabs.send': 'Send close request',
|
||||
'tabs.sending': 'Sending...',
|
||||
'tabs.cancel': 'Cancel',
|
||||
'tabs.planned_title': 'Planned Customers',
|
||||
'tabs.planned_desc': 'These names are synced to the terminal. It will create tabs for these customers automatically.',
|
||||
'tabs.planned_add': 'Add customer name...',
|
||||
'tabs.planned_empty': 'No planned customers. Add names above and they\'ll sync to the terminal.',
|
||||
'tabs.request_sent': 'Close request sent',
|
||||
'tabs.request_sent_desc': 'Sent to the terminal',
|
||||
'tabs.request_result': 'Close request for {name}: {status}',
|
||||
'tabs.send_test': 'Send test email',
|
||||
'tabs.test_sent': 'Test email sent',
|
||||
'tabs.test_sent_desc': 'Sent to {email}',
|
||||
'tabs.test_failed': 'Test email failed',
|
||||
'planned.title': 'Planned Customers',
|
||||
'planned.name_placeholder': 'Add customer name...',
|
||||
'planned.email_placeholder': 'Email (optional)',
|
||||
'planned.add': 'Add customer',
|
||||
'planned.remove': 'Remove {name}',
|
||||
'planned.waiting_empty': 'No customers waiting for the terminal.',
|
||||
'planned.added': 'Added',
|
||||
'planned.added_desc': '"{name}" will get a tab on the terminal',
|
||||
'import.button': 'Import from Excel',
|
||||
'import.title': 'Import customers',
|
||||
'import.description': 'Upload the customer list. Every name becomes a planned customer and gets a tab on the terminal.',
|
||||
@@ -64,6 +106,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'users.title': 'Users',
|
||||
'users.count': '{count} user',
|
||||
'users.count_plural': '{count} users',
|
||||
'users.load_failed': 'Failed to load users',
|
||||
'users.empty': 'No users',
|
||||
'users.empty_desc': 'Add users who can manage tabs from this dashboard.',
|
||||
'users.add': 'Add User',
|
||||
@@ -73,13 +116,24 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'users.created': 'Created',
|
||||
'users.add_title': 'Add User',
|
||||
'users.add_desc': 'Create a new user who can access the dashboard.',
|
||||
'users.name_placeholder': 'Bartender name',
|
||||
'users.password': 'Password',
|
||||
'users.password_placeholder': 'At least 6 characters',
|
||||
'users.role_viewer': 'Viewer (can close tabs)',
|
||||
'users.role_admin': 'Admin (full access)',
|
||||
'users.role.viewer': 'Viewer',
|
||||
'users.role.admin': 'Admin',
|
||||
'users.creating': 'Creating...',
|
||||
'users.create': 'Create User',
|
||||
'users.created_toast': 'User created',
|
||||
'users.created_toast_desc': '{email} added as {role}',
|
||||
'users.create_failed': 'Failed to create user',
|
||||
'commands.title': 'Command History',
|
||||
'commands.count': '{count} command',
|
||||
'commands.count_plural': '{count} commands',
|
||||
'commands.load_failed': 'Failed to load commands',
|
||||
'commands.empty': 'No commands found',
|
||||
'commands.empty_desc': 'Commands appear here when you send close requests.',
|
||||
'commands.tab': 'Tab',
|
||||
'commands.requested': 'Requested By',
|
||||
'commands.payment': 'Payment',
|
||||
@@ -92,46 +146,37 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'commands.next': 'Next',
|
||||
'commands.page': 'Page {page} of {total}',
|
||||
'settings.title': 'Settings',
|
||||
'settings.desc': 'Venue and device management',
|
||||
'settings.desc': 'Venue and terminal',
|
||||
'settings.venue': 'Venue',
|
||||
'settings.device': 'Device',
|
||||
'settings.device': 'Terminal',
|
||||
'settings.paired': 'Paired as "{name}" — last seen {time}',
|
||||
'settings.not_paired': 'No device paired yet',
|
||||
'settings.not_paired': 'No terminal paired yet',
|
||||
'settings.connected': 'Connected',
|
||||
'settings.since': 'Since {date}',
|
||||
'settings.pair_desc': 'Pair a bar terminal to start receiving tabs.',
|
||||
'settings.pair': 'Pair Device',
|
||||
'settings.repair': 'Re-pair Device',
|
||||
'settings.pair_title': 'Pair a Device',
|
||||
'settings.pair': 'Pair Terminal',
|
||||
'settings.repair': 'Re-pair Terminal',
|
||||
'settings.generating': 'Generating...',
|
||||
'settings.pair_title': 'Pair a Terminal',
|
||||
'settings.pair_instructions': 'Enter this code on the bar terminal to connect it to your venue.',
|
||||
'settings.pair_code': 'Registration Code',
|
||||
'settings.pair_expires': 'Expires at {time}',
|
||||
'settings.pair_steps': 'Instructions:',
|
||||
'settings.pair_step1': 'Open the bar terminal app',
|
||||
'settings.pair_step2': 'Select your venue from the list',
|
||||
'settings.pair_step3': 'Enter this registration code',
|
||||
'settings.pair_step4': 'Give the terminal a name',
|
||||
'settings.pair_step5': 'Complete registration',
|
||||
'settings.copy': 'Copy Code',
|
||||
'settings.close': 'Close',
|
||||
'settings.api': 'API',
|
||||
'settings.base_url': 'Base URL',
|
||||
'settings.swagger': 'Swagger',
|
||||
'settings.copied_desc': 'Registration code copied to clipboard',
|
||||
'overview.title': 'Overview',
|
||||
'overview.desc': 'Real-time overview of your venue',
|
||||
'overview.open_tabs': 'Open Tabs',
|
||||
'overview.device': 'Device',
|
||||
'overview.device': 'Terminal',
|
||||
'overview.pending': 'Pending Commands',
|
||||
'overview.recent': 'Recent Commands',
|
||||
'overview.no_commands': 'No commands yet',
|
||||
'overview.not_connected': 'Not connected',
|
||||
'overview.last_seen': 'Last seen {time}',
|
||||
'common.refresh': 'Refresh',
|
||||
'common.loading': 'Loading...',
|
||||
'common.error': 'Something went wrong',
|
||||
'common.confirm': 'Confirm',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.processing': 'Processing...',
|
||||
'common.copied': 'Copied',
|
||||
'auth.login': 'Sign in',
|
||||
'auth.logging_in': 'Signing in...',
|
||||
'auth.email': 'Email',
|
||||
@@ -141,15 +186,17 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'lang.nl': 'Nederlands',
|
||||
'lang.label': 'Language',
|
||||
'reminders.title': 'Payment Reminders',
|
||||
'reminders.desc': 'Send payment reminders to customers with open tabs',
|
||||
'reminders.candidates': 'Eligible for reminder',
|
||||
'reminders.candidates_desc': 'These customers have open tabs and an email address on file. Reminders will be sent via Resend.',
|
||||
'reminders.desc': 'Send payment reminders to customers with an unpaid tab',
|
||||
'reminders.candidates': 'Eligible for a reminder',
|
||||
'reminders.candidates_desc': 'These customers have an unpaid tab and an email address on file. Reminders are sent via Resend.',
|
||||
'reminders.no_candidates': 'No eligible customers',
|
||||
'reminders.no_candidates_desc': 'Make sure planned customers have email addresses set.',
|
||||
'reminders.no_candidates_desc': 'Make sure planned customers have an email address.',
|
||||
'reminders.send': 'Send Reminders',
|
||||
'reminders.sending': 'Sending...',
|
||||
'reminders.result': 'Sent {sent} of {total} reminder(s)',
|
||||
'reminders.confirm': 'This emails a payment reminder to {count} customer(s) with an unpaid tab.',
|
||||
'reminders.sent_title': 'Reminders Sent',
|
||||
'reminders.partial': '{sent} of {total} sent',
|
||||
'reminders.error_detail': 'Error: {error}',
|
||||
'reminders.zero_sent': 'No reminders were sent. Check that Resend is configured.',
|
||||
'reminders.success': 'Successfully sent {sent} reminder email(s)',
|
||||
'reminders.history': 'Sent Reminders',
|
||||
@@ -159,47 +206,87 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'reminders.amount': 'Amount',
|
||||
'reminders.sent_at': 'Sent At',
|
||||
'reminders.status': 'Status',
|
||||
'tabs.email': 'Email (optional)',
|
||||
'tabs.planned_email_hint': 'Add email to enable payment reminders',
|
||||
'reminders.status_sent': 'Sent',
|
||||
},
|
||||
nl: {
|
||||
'nav.overview': 'Overzicht',
|
||||
'nav.tabs': 'Open Tabs',
|
||||
'nav.tabs': 'Open poefs',
|
||||
'nav.reminders': 'Herinneringen',
|
||||
'nav.users': 'Gebruikers',
|
||||
'nav.commands': 'Opdrachten',
|
||||
'nav.settings': 'Instellingen',
|
||||
'nav.logout': 'Uitloggen',
|
||||
'realtime.live': 'Live updates aan',
|
||||
'realtime.offline': 'Live updates niet beschikbaar — wordt periodiek vernieuwd',
|
||||
'tabs.title': 'Open Tabs',
|
||||
'tabs.count': '{count} open tab',
|
||||
'tabs.count_plural': '{count} open tabs',
|
||||
'tabs.empty': 'Geen open tabs',
|
||||
'tabs.empty_desc': 'Open tabs verschijnen hier wanneer de terminal synchroniseert.',
|
||||
'realtime.offline': 'Live updates niet beschikbaar — wordt regelmatig vernieuwd',
|
||||
'common.refresh': 'Vernieuwen',
|
||||
'common.loading': 'Laden...',
|
||||
'common.error': 'Er is iets misgegaan',
|
||||
'common.unknown_error': 'Er is een fout opgetreden',
|
||||
'common.failed': 'Mislukt',
|
||||
'common.cancel': 'Annuleren',
|
||||
'common.close': 'Sluiten',
|
||||
'common.copied': 'Gekopieerd',
|
||||
'common.logged_out': 'Uitgelogd',
|
||||
'time.just_now': 'zonet',
|
||||
'time.minutes_ago': '{count} min geleden',
|
||||
'time.hours_ago': '{count} u geleden',
|
||||
'time.days_ago': '{count} d geleden',
|
||||
'status.pending': 'In behandeling',
|
||||
'status.completed': 'Voltooid',
|
||||
'status.failed': 'Mislukt',
|
||||
'status.conflict': 'Conflict',
|
||||
'status.expired': 'Verlopen',
|
||||
'device_status.online': 'Online',
|
||||
'device_status.recently_seen': 'Recent gezien',
|
||||
'device_status.stale': 'Inactief',
|
||||
'device_status.offline': 'Offline',
|
||||
'device_status.revoked': 'Ingetrokken',
|
||||
'payment.cash': 'Contant',
|
||||
'payment.payconiq': 'Payconiq',
|
||||
'payment.payment_pending': 'Onbetaald laten',
|
||||
'tabs.title': 'Open poefs',
|
||||
'tabs.count': '{count} open poef',
|
||||
'tabs.count_plural': '{count} open poefs',
|
||||
'tabs.empty': 'Geen open poefs',
|
||||
'tabs.empty_desc': 'Open poefs verschijnen hier zodra de terminal synchroniseert.',
|
||||
'tabs.load_failed': 'Poefs konden niet geladen worden',
|
||||
'tabs.customer': 'Klant',
|
||||
'tabs.email': 'E-mail',
|
||||
'tabs.amount': 'Bedrag',
|
||||
'tabs.items': 'Artikelen',
|
||||
'tabs.items': 'Items',
|
||||
'tabs.version': 'Versie',
|
||||
'tabs.reported': 'Laatst gemeld',
|
||||
'tabs.reported': 'Laatst bijgewerkt',
|
||||
'tabs.actions': 'Acties',
|
||||
'tabs.close': 'Sluiten',
|
||||
'tabs.close_title': 'Sluit {name}\'s tab?',
|
||||
'tabs.close_desc': 'Stuur een sluitverzoek naar de terminal.',
|
||||
'tabs.show_items': 'Items van {name} tonen',
|
||||
'tabs.hide_items': 'Items van {name} verbergen',
|
||||
'tabs.lines_unknown': 'Deze terminal stuurt de items nog niet door. Werk de app op de tablet bij om ze te zien.',
|
||||
'tabs.lines_empty': 'Geen items op deze poef.',
|
||||
'tabs.each': 'per stuk',
|
||||
'tabs.close_title': 'Poef van {name} sluiten?',
|
||||
'tabs.close_desc': 'Stuur een verzoek naar de terminal om deze poef te sluiten.',
|
||||
'tabs.payment': 'Betaalmethode',
|
||||
'tabs.cash': 'Contant',
|
||||
'tabs.payconiq': 'Payconiq',
|
||||
'tabs.pending_payment': 'Betaling in behandeling',
|
||||
'tabs.select_payment': 'Selecteer een betaalmethode',
|
||||
'tabs.select_payment': 'Kies een betaalmethode',
|
||||
'tabs.send': 'Sluitverzoek versturen',
|
||||
'tabs.sending': 'Bezig met verzenden...',
|
||||
'tabs.cancel': 'Annuleren',
|
||||
'tabs.planned_title': 'Geplande Klanten',
|
||||
'tabs.planned_desc': 'Deze namen worden gesynchroniseerd met de terminal. Deze maakt automatisch tabs aan voor deze klanten.',
|
||||
'tabs.planned_add': 'Klantnaam toevoegen...',
|
||||
'tabs.planned_empty': 'Geen geplande klanten. Voeg namen toe en ze worden gesynchroniseerd met de terminal.',
|
||||
'tabs.sending': 'Bezig met versturen...',
|
||||
'tabs.request_sent': 'Sluitverzoek verstuurd',
|
||||
'tabs.request_sent_desc': 'Verstuurd naar de terminal',
|
||||
'tabs.request_result': 'Sluitverzoek voor {name}: {status}',
|
||||
'tabs.send_test': 'Testmail versturen',
|
||||
'tabs.test_sent': 'Testmail verstuurd',
|
||||
'tabs.test_sent_desc': 'Verstuurd naar {email}',
|
||||
'tabs.test_failed': 'Testmail mislukt',
|
||||
'planned.title': 'Geplande klanten',
|
||||
'planned.name_placeholder': 'Naam van de klant...',
|
||||
'planned.email_placeholder': 'E-mail (optioneel)',
|
||||
'planned.add': 'Klant toevoegen',
|
||||
'planned.remove': '{name} verwijderen',
|
||||
'planned.waiting_empty': 'Geen klanten die op de terminal wachten.',
|
||||
'planned.added': 'Toegevoegd',
|
||||
'planned.added_desc': '"{name}" krijgt een poef op de terminal',
|
||||
'import.button': 'Importeren uit Excel',
|
||||
'import.title': 'Klanten importeren',
|
||||
'import.description': 'Upload de klantenlijst. Elke naam wordt een geplande klant en krijgt een rekening op de terminal.',
|
||||
'import.description': 'Upload de klantenlijst. Elke naam wordt een geplande klant en krijgt een poef op de terminal.',
|
||||
'import.choose_file': 'Kies een bestand',
|
||||
'import.empty_file': 'Het bestand bevat geen rijen.',
|
||||
'import.unreadable': 'Dit bestand kon niet gelezen worden. Gebruik een .xlsx-, .xls- of .csv-bestand.',
|
||||
@@ -214,7 +301,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'import.more': 'en nog {count}…',
|
||||
'import.mode': 'Bestaande klanten',
|
||||
'import.mode_merge': 'Klanten die niet in het bestand staan behouden',
|
||||
'import.mode_replace': 'Klanten die niet in het bestand staan verwijderen (hun open rekeningen op de terminal blijven)',
|
||||
'import.mode_replace': 'Klanten die niet in het bestand staan verwijderen (hun open poefs op de terminal blijven bestaan)',
|
||||
'import.submit': '{count} klanten importeren',
|
||||
'import.importing': 'Bezig met importeren...',
|
||||
'import.done': 'Import voltooid',
|
||||
@@ -223,23 +310,35 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'users.title': 'Gebruikers',
|
||||
'users.count': '{count} gebruiker',
|
||||
'users.count_plural': '{count} gebruikers',
|
||||
'users.load_failed': 'Gebruikers konden niet geladen worden',
|
||||
'users.empty': 'Geen gebruikers',
|
||||
'users.empty_desc': 'Voeg gebruikers toe die tabs kunnen beheren vanaf dit dashboard.',
|
||||
'users.empty_desc': 'Voeg gebruikers toe die poefs kunnen beheren via dit dashboard.',
|
||||
'users.add': 'Gebruiker toevoegen',
|
||||
'users.name': 'Naam',
|
||||
'users.email': 'E-mail',
|
||||
'users.role': 'Rol',
|
||||
'users.created': 'Aangemaakt',
|
||||
'users.add_title': 'Gebruiker toevoegen',
|
||||
'users.add_desc': 'Maak een nieuwe gebruiker aan die toegang heeft tot het dashboard.',
|
||||
'users.add_desc': 'Maak een nieuwe gebruiker aan die toegang krijgt tot het dashboard.',
|
||||
'users.name_placeholder': 'Naam van de medewerker',
|
||||
'users.password': 'Wachtwoord',
|
||||
'users.creating': 'Bezig...',
|
||||
'users.password_placeholder': 'Minstens 6 tekens',
|
||||
'users.role_viewer': 'Medewerker (kan poefs sluiten)',
|
||||
'users.role_admin': 'Beheerder (volledige toegang)',
|
||||
'users.role.viewer': 'Medewerker',
|
||||
'users.role.admin': 'Beheerder',
|
||||
'users.creating': 'Bezig met aanmaken...',
|
||||
'users.create': 'Gebruiker aanmaken',
|
||||
'users.created_toast': 'Gebruiker aangemaakt',
|
||||
'users.created_toast_desc': '{email} toegevoegd als {role}',
|
||||
'users.create_failed': 'Gebruiker aanmaken mislukt',
|
||||
'commands.title': 'Opdrachtgeschiedenis',
|
||||
'commands.count': '{count} opdracht',
|
||||
'commands.count_plural': '{count} opdrachten',
|
||||
'commands.load_failed': 'Opdrachten konden niet geladen worden',
|
||||
'commands.empty': 'Geen opdrachten gevonden',
|
||||
'commands.tab': 'Tab',
|
||||
'commands.empty_desc': 'Opdrachten verschijnen hier zodra je een sluitverzoek verstuurt.',
|
||||
'commands.tab': 'Poef',
|
||||
'commands.requested': 'Aangevraagd door',
|
||||
'commands.payment': 'Betaling',
|
||||
'commands.created': 'Aangemaakt',
|
||||
@@ -251,46 +350,37 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'commands.next': 'Volgende',
|
||||
'commands.page': 'Pagina {page} van {total}',
|
||||
'settings.title': 'Instellingen',
|
||||
'settings.desc': 'Locatie- en apparaatbeheer',
|
||||
'settings.venue': 'Locatie',
|
||||
'settings.device': 'Apparaat',
|
||||
'settings.desc': 'Zaak en terminal',
|
||||
'settings.venue': 'Zaak',
|
||||
'settings.device': 'Terminal',
|
||||
'settings.paired': 'Gekoppeld als "{name}" — laatst gezien {time}',
|
||||
'settings.not_paired': 'Nog geen apparaat gekoppeld',
|
||||
'settings.not_paired': 'Nog geen terminal gekoppeld',
|
||||
'settings.connected': 'Verbonden',
|
||||
'settings.since': 'Sinds {date}',
|
||||
'settings.pair_desc': 'Koppel een barterminal om tabs te ontvangen.',
|
||||
'settings.pair': 'Apparaat koppelen',
|
||||
'settings.repair': 'Opnieuw koppelen',
|
||||
'settings.pair_title': 'Apparaat koppelen',
|
||||
'settings.pair_instructions': 'Voer deze code in op de barterminal om deze te verbinden met uw locatie.',
|
||||
'settings.pair_desc': 'Koppel een barterminal om poefs te ontvangen.',
|
||||
'settings.pair': 'Terminal koppelen',
|
||||
'settings.repair': 'Terminal opnieuw koppelen',
|
||||
'settings.generating': 'Code aanmaken...',
|
||||
'settings.pair_title': 'Terminal koppelen',
|
||||
'settings.pair_instructions': 'Voer deze code in op de barterminal om hem met je zaak te verbinden.',
|
||||
'settings.pair_code': 'Registratiecode',
|
||||
'settings.pair_expires': 'Verloopt om {time}',
|
||||
'settings.pair_step1': 'Open de barterminal app',
|
||||
'settings.pair_step2': 'Selecteer uw locatie uit de lijst',
|
||||
'settings.pair_steps': 'Stappen:',
|
||||
'settings.pair_step1': 'Open de app op de barterminal',
|
||||
'settings.pair_step2': 'Kies je zaak uit de lijst',
|
||||
'settings.pair_step3': 'Voer deze registratiecode in',
|
||||
'settings.pair_step4': 'Geef de terminal een naam',
|
||||
'settings.pair_step5': 'Voltooi de registratie',
|
||||
'settings.pair_step5': 'Rond de registratie af',
|
||||
'settings.copy': 'Code kopiëren',
|
||||
'settings.close': 'Sluiten',
|
||||
'settings.api': 'API',
|
||||
'settings.base_url': 'Basis-URL',
|
||||
'settings.swagger': 'Swagger',
|
||||
'settings.copied_desc': 'Registratiecode gekopieerd naar het klembord',
|
||||
'overview.title': 'Overzicht',
|
||||
'overview.desc': 'Real-time overzicht van uw locatie',
|
||||
'overview.open_tabs': 'Open Tabs',
|
||||
'overview.device': 'Apparaat',
|
||||
'overview.open_tabs': 'Open poefs',
|
||||
'overview.device': 'Terminal',
|
||||
'overview.pending': 'Openstaande opdrachten',
|
||||
'overview.recent': 'Recente opdrachten',
|
||||
'overview.no_commands': 'Nog geen opdrachten',
|
||||
'overview.not_connected': 'Niet verbonden',
|
||||
'overview.last_seen': 'Laatst gezien {time}',
|
||||
'common.refresh': 'Verversen',
|
||||
'common.loading': 'Laden...',
|
||||
'common.error': 'Er is iets misgegaan',
|
||||
'common.confirm': 'Bevestigen',
|
||||
'common.cancel': 'Annuleren',
|
||||
'common.processing': 'Bezig...',
|
||||
'common.copied': 'Gekopieerd',
|
||||
'auth.login': 'Inloggen',
|
||||
'auth.logging_in': 'Bezig met inloggen...',
|
||||
'auth.email': 'E-mail',
|
||||
@@ -300,67 +390,110 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
'lang.nl': 'Nederlands',
|
||||
'lang.label': 'Taal',
|
||||
'reminders.title': 'Betalingsherinneringen',
|
||||
'reminders.desc': 'Stuur betalingsherinneringen naar klanten met openstaande tabs',
|
||||
'reminders.candidates': 'In aanmerking voor herinnering',
|
||||
'reminders.candidates_desc': 'Deze klanten hebben openstaande tabs en een e-mailadres. Herinneringen worden verzonden via Resend.',
|
||||
'reminders.no_candidates': 'Geen geschikte klanten',
|
||||
'reminders.desc': 'Stuur betalingsherinneringen naar klanten met een openstaande poef',
|
||||
'reminders.candidates': 'Komen in aanmerking',
|
||||
'reminders.candidates_desc': 'Deze klanten hebben een openstaande poef en een gekend e-mailadres. Herinneringen worden verstuurd via Resend.',
|
||||
'reminders.no_candidates': 'Geen klanten die in aanmerking komen',
|
||||
'reminders.no_candidates_desc': 'Zorg dat geplande klanten een e-mailadres hebben.',
|
||||
'reminders.send': 'Verstuur herinneringen',
|
||||
'reminders.send': 'Herinneringen versturen',
|
||||
'reminders.sending': 'Bezig met versturen...',
|
||||
'reminders.result': '{sent} van {total} herinnering(en) verzonden',
|
||||
'reminders.sent_title': 'Herinneringen verzonden',
|
||||
'reminders.zero_sent': 'Geen herinneringen verzonden. Controleer of Resend is geconfigureerd.',
|
||||
'reminders.success': '{sent} herinnering(en) succesvol verzonden',
|
||||
'reminders.history': 'Verzonden herinneringen',
|
||||
'reminders.history_empty': 'Nog geen herinneringen verzonden',
|
||||
'reminders.confirm': 'Er wordt een betalingsherinnering gemaild naar {count} klant(en) met een openstaande poef.',
|
||||
'reminders.sent_title': 'Herinneringen verstuurd',
|
||||
'reminders.partial': '{sent} van {total} verstuurd',
|
||||
'reminders.error_detail': 'Fout: {error}',
|
||||
'reminders.zero_sent': 'Er zijn geen herinneringen verstuurd. Controleer of Resend is ingesteld.',
|
||||
'reminders.success': '{sent} herinnering(en) verstuurd',
|
||||
'reminders.history': 'Verstuurde herinneringen',
|
||||
'reminders.history_empty': 'Nog geen herinneringen verstuurd',
|
||||
'reminders.customer': 'Klant',
|
||||
'reminders.email': 'E-mail',
|
||||
'reminders.amount': 'Bedrag',
|
||||
'reminders.sent_at': 'Verzonden op',
|
||||
'reminders.sent_at': 'Verstuurd op',
|
||||
'reminders.status': 'Status',
|
||||
'tabs.email': 'E-mail (optioneel)',
|
||||
'tabs.planned_email_hint': 'Voeg e-mail toe om betalingsherinneringen in te schakelen',
|
||||
'reminders.status_sent': 'Verstuurd',
|
||||
},
|
||||
};
|
||||
|
||||
const LOCALES: Record<Lang, string> = { en: 'en-GB', nl: 'nl-BE' };
|
||||
|
||||
type Params = Record<string, string | number>;
|
||||
|
||||
interface LangContextType {
|
||||
lang: Lang;
|
||||
setLang: (lang: Lang) => void;
|
||||
t: (key: string, params?: Record<string, string | number>) => string;
|
||||
t: (key: string, params?: Params) => string;
|
||||
/** Uses `<key>_plural` unless `count` is 1; `{count}` is filled in. */
|
||||
tn: (key: string, count: number, params?: Params) => string;
|
||||
/** "just now", "5m ago"…, falling back to a date after a week. */
|
||||
relativeTime: (date: string | Date) => string;
|
||||
dateTime: (date: string | Date) => string;
|
||||
time: (date: string | Date) => string;
|
||||
}
|
||||
|
||||
const LangContext = createContext<LangContextType>({
|
||||
lang: 'en',
|
||||
setLang: () => {},
|
||||
t: (key) => key,
|
||||
});
|
||||
function translate(lang: Lang, key: string, params?: Params): string {
|
||||
let s = translations[lang][key] ?? translations.en[key] ?? key;
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
s = s.split(`{${k}}`).join(String(v));
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function helpers(lang: Lang): Omit<LangContextType, 'setLang'> {
|
||||
const t = (key: string, params?: Params) => translate(lang, key, params);
|
||||
const locale = LOCALES[lang];
|
||||
return {
|
||||
lang,
|
||||
t,
|
||||
tn: (key, count, params) => t(count === 1 ? key : `${key}_plural`, { count, ...params }),
|
||||
relativeTime: (value) => {
|
||||
const date = new Date(value);
|
||||
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
if (seconds < 60) return t('time.just_now');
|
||||
if (minutes < 60) return t('time.minutes_ago', { count: minutes });
|
||||
if (hours < 24) return t('time.hours_ago', { count: hours });
|
||||
if (days < 7) return t('time.days_ago', { count: days });
|
||||
return date.toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
},
|
||||
dateTime: (value) =>
|
||||
new Date(value).toLocaleString(locale, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }),
|
||||
time: (value) => new Date(value).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }),
|
||||
};
|
||||
}
|
||||
|
||||
const LangContext = createContext<LangContextType>({ ...helpers('en'), setLang: () => {} });
|
||||
|
||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||
const [lang, setLangState] = useState<Lang>('en');
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('kooltap-lang') as Lang | null;
|
||||
if (stored === 'en' || stored === 'nl') setLangState(stored);
|
||||
try {
|
||||
const stored = localStorage.getItem('kooltap-lang') as Lang | null;
|
||||
if (stored === 'en' || stored === 'nl') setLangState(stored);
|
||||
} catch {
|
||||
// Storage unavailable (e.g. private mode): keep the default language
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = lang;
|
||||
}, [lang]);
|
||||
|
||||
function setLang(l: Lang) {
|
||||
setLangState(l);
|
||||
localStorage.setItem('kooltap-lang', l);
|
||||
}
|
||||
|
||||
function t(key: string, params?: Record<string, string | number>): string {
|
||||
let s = translations[lang][key];
|
||||
if (!s) s = translations['en'][key] || key;
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
s = s.replace(`{${k}}`, String(v));
|
||||
}
|
||||
try {
|
||||
localStorage.setItem('kooltap-lang', l);
|
||||
} catch {
|
||||
// Not persisted, but the choice still applies for this visit
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
return (
|
||||
<LangContext.Provider value={{ lang, setLang, t }}>
|
||||
<LangContext.Provider value={{ ...helpers(lang), setLang }}>
|
||||
{children}
|
||||
</LangContext.Provider>
|
||||
);
|
||||
@@ -368,4 +501,4 @@ export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
export function useLang() {
|
||||
return useContext(LangContext);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user