From baa0f60a5aa655b76e5a0b840bef04a4c7a196dd Mon Sep 17 00:00:00 2001 From: e-macgregor Date: Thu, 3 Sep 2026 02:36:40 +0000 Subject: [PATCH] feat(i18n): add Mexican Spanish localization --- backend/src/i18n/locales/es-mx.json | 39 +++ backend/src/i18n/translations.ts | 2 + backend/src/i18n/translations_test.ts | 29 +++ frontend/src/lib/i18n/locales/es-mx.json | 297 ++++++++++++++++++++++ frontend/src/lib/i18n/mod.ts | 2 + frontend/src/routes/settings/+page.svelte | 1 + 6 files changed, 370 insertions(+) create mode 100644 backend/src/i18n/locales/es-mx.json create mode 100644 backend/src/i18n/translations_test.ts create mode 100644 frontend/src/lib/i18n/locales/es-mx.json diff --git a/backend/src/i18n/locales/es-mx.json b/backend/src/i18n/locales/es-mx.json new file mode 100644 index 0000000..81d600f --- /dev/null +++ b/backend/src/i18n/locales/es-mx.json @@ -0,0 +1,39 @@ +{ + "invoiceTitle": "Factura", + "invoiceNumberLabel": "Número de factura", + "invoiceNumberShortLabel": "Factura núm.", + "invoiceDateLabel": "Fecha de la factura", + "dateLabel": "Fecha", + "dueDateLabel": "Fecha de vencimiento", + "dueShortLabel": "Vence", + "referenceLabel": "Referencia", + "billToHeading": "Facturar a", + "itemsHeading": "Conceptos de la factura", + "itemHeaderDescription": "Descripción", + "itemHeaderQuantity": "Cantidad", + "itemHeaderQuantityShort": "Cant.", + "itemHeaderUnit": "Unidad", + "itemHeaderUnitPrice": "Precio unitario", + "itemHeaderUnitPriceShort": "Precio", + "itemHeaderAmount": "Importe", + "itemHeaderTax": "IVA", + "summaryHeading": "Resumen de la factura", + "subtotalLabel": "Subtotal", + "discountLabel": "Descuento", + "taxLabel": "IVA", + "totalLabel": "Total", + "statusLabel": "Estado", + "taxSummaryHeading": "Resumen de impuestos", + "taxableLabel": "Base gravable", + "taxAmountLabel": "Importe del IVA", + "taxIdLabel": "RFC", + "outstandingBalanceLabel": "Saldo pendiente", + "paymentInformationHeading": "Información de pago", + "paymentMethodsLabel": "Métodos de pago", + "paymentMethodsPrefix": "Métodos:", + "bankAccountLabel": "Cuenta bancaria", + "bankAccountPrefix": "Banco:", + "paymentTermsLabel": "Condiciones de pago", + "notesHeading": "Notas", + "thankYouNote": "Gracias por tu preferencia." +} diff --git a/backend/src/i18n/translations.ts b/backend/src/i18n/translations.ts index 765677d..cfd8544 100644 --- a/backend/src/i18n/translations.ts +++ b/backend/src/i18n/translations.ts @@ -4,6 +4,7 @@ import deRaw from "./locales/de.json" with { type: "json" }; import ptBrRaw from "./locales/pt-br.json" with { type: "json" }; import trRaw from "./locales/tr.json" with { type: "json" }; import esCoRaw from "./locales/es-co.json" with { type: "json" }; +import esMxRaw from "./locales/es-mx.json" with { type: "json" }; export type InvoiceLabels = { invoiceTitle: string; @@ -108,6 +109,7 @@ const catalogs: Record = Object.freeze({ pt: coerceLabels("pt", ptBrRaw), // alias for pt-br tr: coerceLabels("tr", trRaw), "es-co": coerceLabels("es-co", esCoRaw), + "es-mx": coerceLabels("es-mx", esMxRaw), es: coerceLabels("es", esCoRaw), // alias for es-co }); diff --git a/backend/src/i18n/translations_test.ts b/backend/src/i18n/translations_test.ts new file mode 100644 index 0000000..568169e --- /dev/null +++ b/backend/src/i18n/translations_test.ts @@ -0,0 +1,29 @@ +import { availableInvoiceLocales, getInvoiceLabels } from "./translations.ts"; + +function assert( + condition: boolean, + message = "Expected condition to be true", +): asserts condition { + if (!condition) throw new Error(message); +} + +function assertEquals(actual: unknown, expected: unknown): void { + if (actual !== expected) { + throw new Error(`Expected ${String(expected)}, received ${String(actual)}`); + } +} + +Deno.test("es-MX invoice labels use Mexican terminology", () => { + const { locale, labels } = getInvoiceLabels("es-MX"); + + assertEquals(locale, "es-mx"); + assertEquals(labels.invoiceTitle, "Factura"); + assertEquals(labels.taxIdLabel, "RFC"); + assertEquals(labels.taxLabel, "IVA"); + assertEquals(labels.taxableLabel, "Base gravable"); + assertEquals(labels.taxAmountLabel, "Importe del IVA"); +}); + +Deno.test("es-MX is an explicit invoice locale", () => { + assert(availableInvoiceLocales().includes("es-mx")); +}); diff --git a/frontend/src/lib/i18n/locales/es-mx.json b/frontend/src/lib/i18n/locales/es-mx.json new file mode 100644 index 0000000..c676af9 --- /dev/null +++ b/frontend/src/lib/i18n/locales/es-mx.json @@ -0,0 +1,297 @@ +{ + "Active": "Activo", + "Paid with": "Pagado con", + "Active (Available in dropdowns)": "Activo (disponible en listas desplegables)", + "Add Category": "Agregar categoría", + "Add Unit": "Agregar unidad", + "Add item": "Agregar concepto", + "Add tax": "Agregar impuesto", + "Address": "Dirección", + "2FA code": "Código 2FA", + "2FA QR code": "Código QR 2FA", + "Adjust the look and feel of the application.": "Ajuste la apariencia de la aplicación.", + "Allow editing and deleting sent/paid invoices": "Permitir editar y eliminar facturas enviadas o pagadas", + "Allowing edits/deletes for sent or paid invoices can violate invoice retention laws. Only enable this if you understand the legal impact.": "Permitir editar o eliminar facturas enviadas o pagadas puede infringir las normas de conservación de documentos fiscales. Habilite esto únicamente si comprende las implicaciones legales.", + "Admin": "Admin", + "Administrator": "Administrador", + "Administrators have full access to all features.": "Los administradores tienen acceso completo a todas las funciones.", + "All": "Todos", + "Appearance": "Apariencia", + "Are you sure you want to delete this customer? This cannot be undone.": "¿Está seguro de que desea eliminar este cliente? Esta acción no se puede deshacer.", + "Confirm disable two-factor authentication": "Confirmar la desactivación de la autenticación en dos pasos", + "Enter your current 2FA code to confirm.": "Ingrese su código 2FA actual para confirmar.", + "Are you sure you want to delete this product?": "¿Está seguro de que desea eliminar este producto?", + "Are you sure you want to delete this template?": "¿Está seguro de que desea eliminar esta plantilla?", + "Are you sure you want to delete this user? This action cannot be undone.": "¿Está seguro de que desea eliminar este usuario? Esta acción no se puede deshacer.", + "Bank Account": "Cuenta bancaria", + "Branding": "Marca", + "Cancel": "Cancelar", + "Category": "Categoría", + "Category Code": "Código de categoría", + "Category Name": "Nombre de la categoría", + "City": "Ciudad", + "Clear All": "Limpiar todo", + "Company Address": "Dirección de la empresa", + "Company Information": "Información de la empresa", + "Company Name": "Razón social", + "Contact Name": "Nombre de contacto", + "Copy link": "Copiar enlace", + "Country Code": "Código de país", + "Country Code (ISO alpha-2)": "Código de país (ISO alfa-2)", + "Country code": "Código de país", + "Country code placeholder": "p. ej., MX, US, CA", + "Create": "Crear", + "Create Customer": "Crear cliente", + "Create Invoice": "Crear factura", + "Create Product": "Crear producto", + "Create User": "Crear usuario", + "Create your first customer": "Cree su primer cliente", + "Create your first invoice": "Cree su primera factura", + "Create your first product": "Cree su primer producto", + "Ctrl/Cmd+Enter to add": "Ctrl/Cmd+Enter para agregar", + "Currency": "Moneda", + "Customer": "Cliente", + "Customer data hidden": "Datos del cliente ocultos", + "Customers": "Clientes", + "Dark": "Oscuro", + "Dashboard": "Panel principal", + "Date Format": "Formato de fecha", + "Default": "Predeterminado", + "Default Notes": "Notas predeterminadas", + "Default Prices Include Tax": "Los precios predeterminados incluyen impuestos", + "Default Tax Rate": "Tasa de impuesto predeterminada", + "Delete": "Eliminar", + "Delete User": "Eliminar usuario", + "Delete this invoice? This cannot be undone.": "¿Desea eliminar esta factura? Esta acción no se puede deshacer.", + "Demo mode warning": "Modo de demostración: la aplicación es totalmente funcional, pero la base de datos se restablece cada 30 minutos; sus cambios no son permanentes.", + "Description": "Descripción", + "Deutsch": "Alemán", + "Disabled users cannot log in.": "Los usuarios desactivados no pueden iniciar sesión.", + "Discount": "Descuento", + "Display Name": "Nombre visible", + "Display name": "Nombre para mostrar", + "Display name placeholder": "p. ej., Impuesto al valor agregado (IVA)", + "Download PDF": "Descargar PDF", + "Download XML": "Descargar XML", + "Draft": "Borrador", + "Due Date": "Fecha de vencimiento", + "Duplicate": "Duplicar", + "Edit": "Editar", + "Edit Customer": "Editar cliente", + "Edit Invoice": "Editar factura", + "Edit Product": "Editar producto", + "Edit User": "Editar usuario", + "Email": "Correo electrónico", + "Embed XML in HTML": "Incrustar XML en HTML", + "Embed XML in PDF": "Incrustar XML en PDF", + "Enable Automatic Invoice Numbering": "Habilitar numeración automática de facturas", + "English": "Inglés", + "Enter 6-digit code": "Ingrese el código de 6 dígitos", + "Enter a manifest URL": "Ingrese la URL del manifiesto", + "Enter your password": "Ingrese su contraseña", + "Enter your username": "Ingrese su nombre de usuario", + "FacturX / ZUGFeRD 2.2": "FacturX / ZUGFeRD 2.2", + "Disable 2FA": "Desactivar 2FA", + "Failed to delete": "Error al eliminar", + "Failed to disable two-factor authentication": "Error al desactivar la autenticación en dos pasos", + "Failed to enable two-factor authentication": "Error al activar la autenticación en dos pasos", + "Failed to generate 2FA setup": "Error al generar la configuración de 2FA", + "Failed to save settings": "Error al guardar la configuración", + "Failed to update": "Error al actualizar", + "File must be a .zip archive": "El archivo debe ser un paquete .zip", + "File too large (max 5MB)": "Archivo demasiado grande (máx. 5MB)", + "Format": "Formato", + "Highlight Color": "Color de resaltado", + "Inactive": "Inactivo", + "Install": "Instalar", + "Install failed": "Error en la instalación", + "Install from URL": "Instalar desde URL", + "Installed Templates": "Plantillas instaladas", + "Installing...": "Instalando...", + "Invalid URL or data format": "URL o formato de datos no válido", + "Invio": "Invio", + "Invoice": "Factura", + "Invoice #": "Factura N.º", + "Invoice No": "N.º de factura", + "Invoice Number": "Número de factura", + "Invoice Number Pattern": "Formato de numeración de facturas", + "Invoice data hidden": "Datos de la factura ocultos", + "Invoice published": "Factura publicada", + "Invoice total": "Total de la factura", + "Invoices": "Facturas", + "Issue Date": "Fecha de emisión", + "Item": "Concepto", + "Items": "Conceptos", + "Language": "Idioma", + "Leave blank to keep current password": "Dejar en blanco para conservar la contraseña actual", + "Light": "Claro", + "Link copied!": "¡Enlace copiado!", + "Localization": "Configuración regional", + "Login": "Iniciar sesión", + "Logo URL or Path": "URL o ruta del logo", + "Logout": "Cerrar sesión", + "Make public and mark as sent": "Hacer público y marcar como enviada", + "Manage invoice HTML templates.": "Administre las plantillas HTML de las facturas.", + "Manifest URL": "URL del manifiesto", + "Complete": "Completado", + "Mark as Complete": "Marcar como completado", + "Mark as Paid": "Marcar como pagada", + "Mark as Sent": "Marcar como enviada", + "Minimum 8 characters": "Mínimo 8 caracteres", + "More": "Más", + "Name": "Nombre", + "Nederlands": "Neerlandés", + "Türkçe": "Turco", + "New Customer": "Nuevo cliente", + "New Password": "Nueva contraseña", + "New Product": "Nuevo producto", + "New Template": "Nueva plantilla", + "New User": "Nuevo usuario", + "No": "No", + "No categories defined": "No hay categorías definidas", + "No custom templates installed.": "No hay plantillas personalizadas instaladas.", + "No customers yet.": "Aún no hay clientes registrados.", + "No default tax": "Sin impuesto predeterminado", + "No description": "Sin descripción", + "No invoices found": "No se encontraron facturas", + "No invoices match this filter": "Ninguna factura coincide con este filtro", + "No products yet.": "Aún no hay productos registrados.", + "No tax definitions yet.": "Aún no hay impuestos definidos.", + "No templates found.": "No se encontraron plantillas.", + "No units defined": "No hay unidades de medida definidas", + "No users found.": "No se encontraron usuarios.", + "Notes": "Notas", + "Number Format": "Formato de número", + "Numbering": "Numeración", + "Open": "Abierta", + "Open Invoices": "Facturas abiertas", + "Open in new tab": "Abrir en una pestaña nueva", + "Optional": "Opcional", + "Optionally record how this invoice was paid.": "Opcionalmente, registre el medio con el que se pagó esta factura.", + "Outstanding": "Pendiente", + "Overdue": "Vencida", + "Paid": "Pagada", + "Password": "Contraseña", + "Payment Method": "Método de pago", + "Payment Methods": "Métodos de pago", + "Payment Terms": "Términos de pago", + "Payments & Texts": "Pagos y textos", + "Per line": "Por línea", + "Permissions": "Permisos", + "Phone": "Teléfono", + "Please select a .zip file": "Por favor seleccione un archivo .zip", + "Please select a valid image file.": "Por favor seleccione un archivo de imagen válido.", + "Postal Code": "Código postal", + "Preview": "Vista previa", + "Price": "Precio", + "Prices include tax": "Los precios incluyen impuesto", + "Prices include tax?": "¿Los precios incluyen impuesto?", + "Product": "Producto", + "Product Categories": "Categorías de producto", + "Product Units": "Unidades de medida", + "Products": "Productos", + "Public link": "Enlace público", + "Publish": "Publicar", + "Qty": "Cant.", + "Quantity": "Cantidad", + "Reactivate": "Reactivar", + "Recent Invoices": "Facturas recientes", + "Recovery code": "Código de recuperación", + "Recovery codes (save these now)": "Códigos de recuperación (guárdelos ahora)", + "Reconfigure 2FA": "Reconfigurar 2FA", + "Remove item": "Eliminar concepto", + "Resource": "Recurso", + "Role": "Rol", + "Round on total": "Redondear en el total", + "Round per line": "Redondear por línea", + "Rounding": "Redondeo", + "Rounding Mode": "Modo de redondeo", + "Rounding mode": "Modo de redondeo", + "SKU": "SKU / Código", + "Save": "Guardar", + "Save Changes": "Guardar cambios", + "Save Settings": "Guardar configuración", + "Scan this QR code with your authenticator app": "Escanee este código QR con su aplicación de autenticación", + "Select .zip archive": "Seleccionar archivo .zip", + "Select All": "Seleccionar todo", + "Select category": "Seleccionar categoría", + "Select customer": "Seleccionar cliente", + "Select unit": "Seleccionar unidad", + "Select which resources and actions this user can access. Admins bypass these checks.": "Seleccione a qué recursos y acciones puede acceder este usuario. Los administradores omiten estas restricciones.", + "Sent": "Enviada", + "Settings": "Configuración", + "Settings updated successfully": "Configuración actualizada con éxito", + "Set up 2FA": "Configurar 2FA", + "Standard UBL XML": "XML UBL estándar", + "Status": "Estado", + "Status History": "Historial de estados", + "Subtotal": "Subtotal", + "Suggested colors from logo:": "Colores sugeridos a partir del logo:", + "Tax": "IVA", + "Tax %": "% de IVA", + "Tax Definition": "Definición de impuesto", + "Tax Definitions": "Impuestos", + "Tax ID": "RFC", + "Tax Label": "Nombre del impuesto", + "Tax Mode": "Modo de impuesto", + "Tax Rate (%)": "Tasa de impuesto (%)", + "Tax Settings": "Configuración de impuestos", + "Tax code": "Código de impuesto", + "Tax code placeholder": "p. ej., IVA, IEPS", + "Tax mode": "Modo de impuesto", + "Tax rate": "Tasa de impuesto", + "Template": "Plantilla", + "Templates": "Plantillas", + "These codes are shown once and can be used if you lose access to your authenticator app.": "Estos códigos se muestran solo una vez y pueden utilizarse si pierde el acceso a su aplicación de autenticación.", + "Toggle light/dark theme": "Alternar tema claro/oscuro", + "Total": "Total", + "Total Billed": "Total facturado", + "Two-factor authentication disabled": "Autenticación en dos pasos desactivada", + "Two-factor authentication enabled": "Autenticación en dos pasos activada", + "Two-factor authentication is enabled": "La autenticación en dos pasos está activada", + "Two-factor authentication is not enabled": "La autenticación en dos pasos no está activada", + "UBL 2.1 (PEPPOL)": "UBL 2.1 (PEPPOL)", + "Unit": "Unidad", + "Unit Code": "Código de unidad", + "Unit Name": "Nombre de la unidad", + "Unit Price": "Precio unitario", + "Unknown Customer": "Cliente desconocido", + "Unnamed": "Sin nombre", + "Unpublish": "Despublicar", + "Update": "Actualizar", + "Updated": "Actualizado", + "Upload": "Subir", + "Upload Template": "Subir plantilla", + "Upload failed": "Error al subir el archivo", + "Uploading...": "Subiendo...", + "User": "Usuario", + "User not found": "Usuario no encontrado", + "User saved successfully.": "Usuario guardado con éxito.", + "Username": "Nombre de usuario", + "Users": "Usuarios", + "Version": "Versión", + "Verify and Enable 2FA": "Verificar y activar 2FA", + "View HTML": "Ver HTML", + "View public link": "Ver enlace público", + "Void Invoice": "Anular factura", + "Warning: you are editing a sent/paid invoice. Ensure this is legally allowed in your jurisdiction.": "Advertencia: está editando una factura enviada o pagada. Asegúrese de que esto esté permitido legalmente según su jurisdicción fiscal.", + "Void this invoice?": "¿Desea anular esta factura?", + "Voided": "Anulada", + "Welcome to Invio": "Bienvenido a Invio", + "XML": "XML", + "XML Data": "Datos XML", + "XML Export": "Exportación XML", + "XML Profile ID": "ID de perfil XML", + "Yes": "Sí", + "Yes, disable": "Sí, desactivar", + "You are about to delete a sent/paid invoice. This may violate invoice retention laws and cannot be undone. Continue?": "Está a punto de eliminar una factura enviada o pagada. Esto puede infringir las normas de retención de facturas y no se puede deshacer. ¿Desea continuar?", + "You are about to edit a sent/paid invoice. Ensure this is legally allowed in your jurisdiction. Continue?": "Está a punto de editar una factura enviada o pagada. Asegúrese de que esto esté permitido por las normas fiscales de su jurisdicción. ¿Desea continuar?", + "You do not have permission to modify settings.": "No tiene permisos para modificar la configuración.", + "You do not have permission to view customers. Contact an administrator to request access.": "No tiene permisos para ver clientes. Póngase en contacto con un administrador para solicitar acceso.", + "You do not have permission to view invoices. Contact an administrator to request access.": "No tiene permisos para ver facturas. Póngase en contacto con un administrador para solicitar acceso.", + "Protect your account with two-factor authentication.": "Proteja su cuenta con autenticación en dos pasos.", + "e.g. Bank Transfer, PayPal": "p. ej., Transferencia bancaria, tarjeta, efectivo", + "e.g. INV-2025-001": "p. ej., FAC-2026-001", + "e.g. US, NL, DE": "p. ej., MX, US, CA", + "item(s)": "concepto(s)" +} diff --git a/frontend/src/lib/i18n/mod.ts b/frontend/src/lib/i18n/mod.ts index e8c4724..c30a183 100644 --- a/frontend/src/lib/i18n/mod.ts +++ b/frontend/src/lib/i18n/mod.ts @@ -4,6 +4,7 @@ import deMessages from "./locales/de.json" with { type: "json" }; import ptMessages from "./locales/pt-br.json" with { type: "json" }; import trMessages from "./locales/tr.json" with { type: "json" }; import esMessages from "./locales/es-co.json" with { type: "json" }; +import esMxMessages from "./locales/es-mx.json" with { type: "json" }; export type UiMessages = Record; export type TranslateParams = Record; @@ -15,6 +16,7 @@ const catalogs: Record = { pt: ptMessages as UiMessages, tr: trMessages as UiMessages, es: esMessages as UiMessages, + "es-mx": esMxMessages as UiMessages, }; const DEFAULT_LOCALE = "en"; diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index ef84aee..476c7b2 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -525,6 +525,7 @@ +