10 Commits

Author SHA1 Message Date
Mert Demir
8742bd59da Fix format for integer amounts (#231)
* support both fractions and integer values

* test fractions

* prettier
2024-09-29 09:24:10 -04:00
Kostas Vrouvas
8eea062218 Fix form columns not working properly (#224) 2024-09-28 18:41:19 -04:00
Mert Demir
9a5674e239 Fix amount preview for scanned receipts (#227)
* no division of amount

* use gpt-4-turbo

* testing setup and naive test

* test multiple variants

* document

* correct locale names

* test large amounts

* test wth strings

* prettier
2024-09-28 18:39:01 -04:00
Tobias Genannt
50b3a2e431 Fix: Correctly display loaded expense (#210)
* Fix #209: Correctly display loaded expense

- Don't load default split options after displaying an existing expense
- Re-validate form after changing the "paidFor" selection.
  This fixes the error message "The expense must be paid for at least one
  participant." after clicking "Select None" and the selecting one participant.

* Fix Paid For Field reset in Edit Expense Page for split Mode 'Unevenly - By amount'

---------

Co-authored-by: partho.kunda <partho.kunda@chaldal.net>
2024-09-28 18:28:27 -04:00
Nikita Utkin
e8d46cd4f3 Add russian localization (#216) 2024-09-28 18:23:17 -04:00
Zack_Z
8f896f7412 Add Chinese translation (#215)
* Added Chinese translation

* Add home page translation

* Fix translations

---------

Co-authored-by: Sebastien Castiel <sebastien@castiel.me>
2024-09-28 18:18:27 -04:00
chrisschuller
504631454a feat: add German language support (#207)
* feat: add German language support

* fix: translate other locale names to German

* chore: integrate recommendations from the PR review

* i18n: add translation recommendations from the PR

* Fix translations

---------

Co-authored-by: Christian Schuller <christianschuller.biz@gmail.com>
Co-authored-by: Sebastien Castiel <sebastien@castiel.me>
2024-09-28 18:11:30 -04:00
Pau Sansa
345f3716c9 feature: add Spanish language support (#214)
* create ES i18n json

* add ES locale to i18n and existing locales

* capitalize words at es.json

* Add missing translation

---------

Co-authored-by: Sebastien Castiel <sebastien@castiel.me>
2024-09-28 18:06:15 -04:00
Sebastien Castiel
5fff8da08d Fix translation 2024-09-28 17:58:04 -04:00
Strekol
07e24f7fcb Add French translation (#196)
* Added french version and title/description from json messages

* Revert back default language to en-US

* Code reviewed with prettier :)

* Updated json to add information field

* Updated json to add information block (missed on previous)

* Reviewed code language

* correction traduction "groupes étoilés" en "groupes favoris"

---------

Co-authored-by: Andy Trouvé <andy@strekol.eu>
2024-09-28 17:56:55 -04:00
18 changed files with 6192 additions and 90 deletions

18
jest.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { Config } from 'jest'
import nextJest from 'next/jest.js'
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
})
// Add any custom config to be passed to Jest
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config)

396
messages/de-DE.json Normal file
View File

@@ -0,0 +1,396 @@
{
"Homepage": {
"title": "Teile <strong>Ausgaben</strong> mit <strong>Freunden & Familie</strong>",
"description": "Willkommen zu deiner neuen <strong>Spliit</strong>-Instanz!",
"button": {
"groups": "Zu den Gruppen",
"github": "GitHub"
}
},
"Header": {
"groups": "Gruppen"
},
"Footer": {
"madeIn": "Entwickelt in Montréal, Québec 🇨🇦",
"builtBy": "Erstellt von <author>Sebastien Castiel</author> und <source>Mitwirkenden</source>"
},
"Expenses": {
"title": "Ausgaben",
"description": "Hier sind die Ausgaben, die du für deine Gruppe erstellt hast.",
"create": "Ausgabe hinzufügen",
"createFirst": "Erstelle die Erste",
"noExpenses": "Deine Gruppe hat noch keine Ausgaben.",
"exportJson": "Als JSON exportieren",
"searchPlaceholder": "Suche nach einer Ausgabe…",
"ActiveUserModal": {
"title": "Wer bist du?",
"description": "Sag uns, welcher Teilnehmer du bist, um die angezeigten Informationen auf dich anzupassen.",
"nobody": "Ich will niemanden auswählen",
"save": "Änderungen speichern",
"footer": "Diese Einstellung kann später in den Gruppeneinstellungen geändert werden."
},
"Groups": {
"upcoming": "Bevorstehend",
"thisWeek": "Diese Woche",
"earlierThisMonth": "Diesen Monat",
"lastMonth": "Letzten Monat",
"earlierThisYear": "Dieses Jahr",
"lastYera": "Letztes Jahr",
"older": "Älter"
}
},
"ExpenseCard": {
"paidBy": "Gezahlt von <strong>{paidBy}</strong> für <paidFor></paidFor>",
"receivedBy": "Empfangen von <strong>{paidBy}</strong> für <paidFor></paidFor>",
"yourBalance": "Deine Bilanz:"
},
"Groups": {
"myGroups": "Meine Gruppen",
"create": "Erstellen",
"loadingRecent": "Lade letzte Gruppen…",
"NoRecent": {
"description": "Du hast in der letzten Zeit keine Gruppe besucht.",
"create": "Erstelle eine",
"orAsk": "oder bitte einen Freund, dir einen Link zu einer Existierenden zu schicken."
},
"recent": "Letzte Gruppen",
"starred": "Favorisierte Gruppen",
"archived": "Archivierte Gruppen",
"archive": "Gruppe archivieren",
"unarchive": "Gruppe wiederherstellen",
"removeRecent": "Aus letzten Gruppen entfernen",
"RecentRemovedToast": {
"title": "Gruppe wurde entfernt",
"description": "Die Gruppe wurde von deiner Liste der letzten Gruppen entfernt.",
"undoAlt": "Gruppe entfernen rückgängig machen",
"undo": "Rückgängig machen"
},
"AddByURL": {
"button": "Mit URL hinzufügen",
"title": "Gruppe mit URL hinzufügen",
"description": "Wenn eine Gruppe mit dir geteilt wurde, kannst du ihre URL hier einfügen, um sie zu deiner Liste hinzuzufügen.",
"error": "Ups, wir können die Gruppe mit der angegebenen URL nicht finden…"
},
"NotFound": {
"text": "Diese Gruppe existiert nicht.",
"link": "Gehe zu zuletzt besuchten Gruppen"
}
},
"GroupForm": {
"title": "Gruppeninformationen",
"NameField": {
"label": "Gruppenname",
"placeholder": "Sommerurlaub",
"description": "Gib deiner Gruppe einen Namen."
},
"InformationField": {
"label": "Gruppeninformationen",
"placeholder": "Welche Informationen sind relevant für Gruppenmitglieder?"
},
"CurrencyField": {
"label": "Währungssymbol",
"placeholder": "€, $, £…",
"description": "Wir benutzen es, um Beträge anzuzeigen."
},
"Participants": {
"title": "Mitglieder",
"description": "Füge einen Namen für jedes Gruppenmitglied hinzu.",
"protectedParticipant": "Dieses Mitglied ist Teil der Ausgaben und kann nicht entfernt werden.",
"new": "Neu",
"add": "Mitglied hinzufügen",
"John": "Johannes",
"Jane": "Janina",
"Jack": "Jakob"
},
"Settings": {
"title": "Lokale Einstellungen",
"description": "Dies sind Einstellungen pro Gerät, die verwendet werden, um deine Benutzererfahrung zu verbessern.",
"ActiveUserField": {
"label": "Aktiver Nutzer",
"placeholder": "Wähle ein Mitglied",
"none": "Keiner",
"description": "Standardnutzer, der die Ausgaben übernimmt."
},
"save": "Speichern",
"saving": "Speichert…",
"create": "Erstellen",
"creating": "Erstellt…",
"cancel": "Abbrechen"
}
},
"ExpenseForm": {
"Income": {
"create": "Einnahme erstellen",
"edit": "Einnahme bearbeiten",
"TitleField": {
"label": "Titel der Einnahme",
"placeholder": "Montagabend Restaurant",
"description": "Füge eine Beschreibung für die Einnahme hinzu."
},
"DateField": {
"label": "Datum der Einnahme",
"description": "Füge ein Datum hinzu für wann die Einnahme erhalten wurde."
},
"categoryFieldDescription": "Wähle die Kategorie der Einnahme.",
"paidByField": {
"label": "Empfangen von",
"description": "Wähle das Mitglied, das die Einnahme erhalten hat."
},
"paidFor": {
"title": "Empfangen für",
"description": "Wähle für wen die Einnahme empfangen wurde."
},
"splitModeDescription": "Wähle, wie die Einnahme aufgeteilt werden soll.",
"attachDescription": "Füge der Einnahme einen Beleg hinzu."
},
"Expense": {
"create": "Augabe erstellen",
"edit": "Ausgabe bearbeiten",
"TitleField": {
"label": "Titel der Ausgabe",
"placeholder": "Montagabend Restaurant",
"description": "Füge eine Beschreibung für die Ausgabe hinzu."
},
"DateField": {
"label": "Datum der Ausgabe",
"description": "Füge das Datum ein, zu dem die Ausgabe getätigt wurde."
},
"categoryFieldDescription": "Wähle eine Kategorie für die Ausgabe.",
"paidByField": {
"label": "Gezahlt von",
"description": "Wähle das Mitglied, das die Ausgabe bezahlt hat."
},
"paidFor": {
"title": "Gezahlt für",
"description": "Wähle für wen die Ausgabe gezahlt wurde."
},
"splitModeDescription": "Wähle, wie die Ausgabe aufgeteilt werden soll.",
"attachDescription": "Füge der Ausgabe einen Beleg hinzu."
},
"amountField": {
"label": "Betrag"
},
"isReimbursementField": {
"label": "Das ist eine Rückzahlung"
},
"categoryField": {
"label": "Kategorie"
},
"notesField": {
"label": "Notizen"
},
"selectNone": "Keine auswählen",
"selectAll": "Alle auswählen",
"shares": "Anteil(e)",
"advancedOptions": "Fortgeschrittene Aufteilungsoptionen…",
"SplitModeField": {
"label": "Aufteilungsart",
"evenly": "Gleich verteilt",
"byShares": "Ungleich Nach Anteilen",
"byPercentage": "Ungleich Prozentual",
"byAmount": "Ungleich Nach Betrag",
"saveAsDefault": "Als Standardoptionen zur Aufteilung speichern"
},
"DeletePopup": {
"label": "Löschen",
"title": "Diese Ausgabe löschen?",
"description": "Willst du diese Ausgabe wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"yes": "Ja",
"cancel": "Abbrechen"
},
"attachDocuments": "Dokument hinzufügen",
"create": "Erstellen",
"creating": "Erstellt…",
"save": "Speichern",
"saving": "Speichert…",
"cancel": "Abbrechen"
},
"ExpenseDocumentsInput": {
"TooBigToast": {
"title": "Die Datei ist zu groß",
"description": "Die maximale Dateigröße ist {maxSize}. Deine ist ${size}."
},
"ErrorToast": {
"title": "Fehler beim Hochladen der Datei",
"description": "Beim Hochladen der Datei ist etwas schiefgelaufen. Versuche es später nochmal oder wähle eine andere Datei.",
"retry": "Wiederholen"
}
},
"CreateFromReceipt": {
"Dialog": {
"triggerTitle": "Ausgabe von Rechnungsbeleg erstellen",
"title": "Von Rechnungsbeleg erstellen",
"description": "Ausgabeninformationen von einem Foto einer Rechnung lesen.",
"body": "Lade ein Foto der Rechnung hoch und wir versuchen die Ausgabeinformationen zu extrahieren",
"selectImage": "Bild wählen…",
"titleLabel": "Titel:",
"categoryLabel": "Kategorie:",
"amountLabel": "Betrag:",
"dateLabel": "Datum:",
"editNext": "Als nächstes kannst du die Informationen zur Ausgabe editieren.",
"continue": "Weiter"
},
"unknown": "Unbekannt",
"TooBigToast": {
"title": "Die Datei ist zu groß",
"description": "Die maximale Dateigröße ist {maxSize}. Deine ist ${size}."
},
"ErrorToast": {
"title": "Fehler beim Hochladen der Datei",
"description": "Beim Hochladen der Datei ist etwas schiefgelaufen. Versuche es später nochmal oder wähle eine andere Datei.",
"retry": "Wiederholen"
}
},
"Balances": {
"title": "Bilanz",
"description": "Das sind die Beträge, die jedes Mitglied bezahlt oder empfangen hat.",
"Reimbursements": {
"title": "Vorgeschlagene Rückzahlungen",
"description": "Hier sind Vorschläge für optimierte Rückzahlungen zwischen Mitgliedern.",
"noImbursements": "Es sieht aus, als seien in der Gruppe keine Rückzahlungen nötig 😁",
"owes": "<strong>{from}</strong> schuldet <strong>{to}</strong>",
"markAsPaid": "Als gezahlt markieren"
}
},
"Stats": {
"title": "Statistiken",
"Totals": {
"title": "Gesamtausgaben",
"description": "Zusammenfassung der Ausgaben der gesamten Gruppe.",
"groupSpendings": "Gesamte Ausgaben der Gruppe",
"groupEarnings": "Gesamte Einnahmen der Gruppe",
"yourSpendings": "Deine gesamten Ausgaben",
"yourEarnings": "Deine gesamten Einnahmen",
"yourShare": "Dein gesamter Anteil"
}
},
"Activity": {
"title": "Aktivitäten",
"description": "Zusammenfassung aller Aktivitäten in dieser Gruppe.",
"noActivity": "Es gab noch keine Aktivität in dieser Gruppe.",
"someone": "Jemand",
"settingsModified": "Die Gruppeneinstellungen wurden von <strong>{participant}</strong> verändert.",
"expenseCreated": "Augabe <em>{expense}</em> wurde von <strong>{participant}</strong> erstellt.",
"expenseUpdated": "Ausgabe <em>{expense}</em> wurde von <strong>{participant}</strong> aktualisiert.",
"expenseDeleted": "Ausgabe <em>{expense}</em> wurde von <strong>{participant}</strong> gelöscht.",
"Groups": {
"today": "Heute",
"yesterday": "Gestern",
"earlierThisWeek": "Diese Woche",
"lastWeek": "Letze Woche",
"earlierThisMonth": "Diesen Monat",
"lastMonth": "Letzen Monat",
"earlierThisYear": "Dieses Jahr",
"lastYear": "Letztes Jahr",
"older": "Älter"
}
},
"Information": {
"title": "Informationen",
"description": "Nutze diesen Ort, um Informationen hinzuzufügen, die für die Gruppenmitglieder wichtig sein könnten.",
"empty": "Noch keine Gruppeninformationen vorhanden."
},
"Settings": {
"title": "Einstellungen"
},
"Locale": {
"en-US": "English",
"fi": "Suomi",
"fr-FR": "Français",
"es": "Español",
"de-DE": "Deutsch",
"zh-CN": "Chinese (Simplified)",
"ru-RU": "Русский"
},
"Share": {
"title": "Teilen",
"description": "Teile die URL, damit andere Mitglieder die Gruppe sehen und Ausgaben hinzufügen können.",
"warning": "Achtung!",
"warningHelp": "Jede person mit der Gruppen-URL kann Ausgaben sehen und editieren. Teile den Link mit Bedacht!"
},
"SchemaErrors": {
"min1": "Gib mindestens ein Zeichen ein.",
"min2": "Gib mindestens zwei Zeichen ein.",
"max5": "Gib maximal fünf Zeichen ein.",
"max50": "Gib maximal 50 Zeichen ein.",
"duplicateParticipantName": "Der Name ist bereits an ein anderes Gruppenmitglied vergeben.",
"titleRequired": "Bitte gib einen Titel an.",
"invalidNumber": "Zahl nicht valide.",
"amountRequired": "Du musst einen Betrag angeben.",
"amountNotZero": "Der Betrag darf nicht 0 sein.",
"amountTenMillion": "Der Betrag muss kleiner als 10.000.000 sein",
"paidByRequired": "Du musst ein Mitglied auswählen.",
"paidForMin1": "Die Ausgabe muss mindestens für ein Mitglied bezahlt werden.",
"noZeroShares": "Alle Anteile müssen größer als 0 sein.",
"amountSum": "Die Summe der Beträge muss dem Betrag der Ausgabe entsprechen.",
"percentageSum": "Die Summe der prozentualen Anteile muss 100 ergeben."
},
"Categories": {
"search": "Nach Kategorie suchen...",
"noCategory": "Keine Kategorie gefunden.",
"Uncategorized": {
"heading": "Nicht kategorisiert",
"General": "Allgemein",
"Payment": "Zahlung"
},
"Entertainment": {
"heading": "Vergnügen",
"Entertainment": "Vergnügen",
"Games": "Spiele",
"Movies": "Filme",
"Music": "Musik",
"Sports": "Sport"
},
"Food and Drink": {
"heading": "Essen und Trinken",
"Food and Drink": "Essen und Trinken",
"Dining Out": "Essen gehen",
"Groceries": "Lebensmittel",
"Liquor": "Alkohol"
},
"Home": {
"heading": "Zuhause",
"Home": "Zuhause",
"Electronics": "Elektronik",
"Furniture": "Möbel",
"Household Supplies": "Haushaltsgegenstände",
"Maintenance": "Wartung",
"Mortgage": "Hypothek",
"Pets": "Haustiere",
"Rent": "Miete",
"Services": "Dienstleistungen"
},
"Life": {
"heading": "Leben",
"Childcare": "Kinderversorgung",
"Clothing": "Kleidung",
"Education": "Bildung",
"Gifts": "Geschenke",
"Insurance": "Versicherung",
"Medical Expenses": "Medizinische Ausgaben",
"Taxes": "Steuern"
},
"Transportation": {
"heading": "Transport",
"Transportation": "Transport",
"Bicycle": "Fahrrad",
"Bus/Train": "Bus/Bahn",
"Car": "Auto",
"Gas/Fuel": "Tanken",
"Hotel": "Hotel",
"Parking": "Parken",
"Plane": "Flugzeug",
"Taxi": "Taxi"
},
"Utilities": {
"heading": "Versorgung",
"Utilities": "Versorgung",
"Cleaning": "Reinigung/Putzen",
"Electricity": "Strom",
"Heat/Gas": "Heizung",
"Trash": "Müll",
"TV/Phone/Internet": "TV/Internet/Telefonie",
"Water": "Wasser"
}
}
}

View File

@@ -1,4 +1,12 @@
{
"Homepage": {
"title": "Share <strong>Expenses</strong> with <strong>Friends & Family</strong>",
"description": "Welcome to your new <strong>Spliit</strong> instance !",
"button": {
"groups": "Go to groups",
"github": "GitHub"
}
},
"Header": {
"groups": "Groups"
},
@@ -287,7 +295,12 @@
},
"Locale": {
"en-US": "English",
"fi": "Suomi"
"fi": "Suomi",
"fr-FR": "Français",
"es": "Español",
"de-DE": "Deutsch",
"zh-CN": "Chinese (Simplified)",
"ru-RU": "Русский"
},
"Share": {
"title": "Share",

396
messages/es.json Normal file
View File

@@ -0,0 +1,396 @@
{
"Homepage": {
"title": "Comparte <strong>Gastos</strong> con <strong>Amigos y Familia</strong>",
"description": "¡Bienvenido a tu nueva instancia de <strong>Spliit</strong>!",
"button": {
"groups": "Ir a grupos",
"github": "GitHub"
}
},
"Header": {
"groups": "Grupos"
},
"Footer": {
"madeIn": "Hecho en Montréal, Québec 🇨🇦",
"builtBy": "Construido por <author>Sebastien Castiel</author> y <source>colaboradores</source>"
},
"Expenses": {
"title": "Gastos",
"description": "Aqui encontraras los gastos que has creado para tu grupo.",
"create": "Crear gasto",
"createFirst": "Crea el primero",
"noExpenses": "Tu grupo aun no tiene gastos.",
"exportJson": "Exportar a JSON",
"searchPlaceholder": "Busca un gasto…",
"ActiveUserModal": {
"title": "¿Quién es usted?",
"description": "Dinos qué participante eres para que podamos personalizar cómo se muestra la información.",
"nobody": "No quiero seleccionar a nadie",
"save": "Guardar cambios",
"footer": "Esta configuración puede modificarse posteriormente en los ajustes del grupo."
},
"Groups": {
"upcoming": "Próximamente",
"thisWeek": "Esta semana",
"earlierThisMonth": "A principios de este mes",
"lastMonth": "El mes pasado",
"earlierThisYear": "A principios de este año",
"lastYera": "El año pasado",
"older": "Más antiguos"
}
},
"ExpenseCard": {
"paidBy": "Pagado por <strong>{paidBy}</strong> para <paidFor></paidFor>",
"receivedBy": "Recibido por <strong>{paidBy}</strong> para <paidFor></paidFor>",
"yourBalance": "Tu balance:"
},
"Groups": {
"myGroups": "Mis grupos",
"create": "Crear",
"loadingRecent": "Cargando grupos recientes…",
"NoRecent": {
"description": "No has visitado ningun grupo recientemente.",
"create": "Crea uno",
"orAsk": "o pídele a un amigo que te envíe el enlace a uno ya existente.."
},
"recent": "Grupos recientes",
"starred": "Grupos favoritos",
"archived": "Grupos archivados",
"archive": "Archivar grupo",
"unarchive": "Desarchivar groupo",
"removeRecent": "Remove from recent groups",
"RecentRemovedToast": {
"title": "El grupo fue eliminado",
"description": "El grupo ha sido eliminado de tu lista de grupos recientes.",
"undoAlt": "Deshacer la eliminación del grupo",
"undo": "Deshacer"
},
"AddByURL": {
"button": "Añadir mediante url",
"title": "Añadir grupo mediante url",
"description": "Si te han compartido un grupo, puedes pegar aquí su URL para añadirlo a tu lista.",
"error": "Oops, no somos capaces de encontrar el grupo desde la URL que has proporcionado..."
},
"NotFound": {
"text": "Este grupo no existe.",
"link": "Ir a los últimos grupos visitados"
}
},
"GroupForm": {
"title": "Información del grupo",
"NameField": {
"label": "Nombre del grupo",
"placeholder": "Vacaciones en Barcelona",
"description": "Inserta un nombre para tu nuevo grupo."
},
"InformationField": {
"label": "Información del grupo",
"placeholder": "Qué información es relevante para los participantes del grupo?"
},
"CurrencyField": {
"label": "Símbolo de divisa",
"placeholder": "$, €, £…",
"description": "Lo usaremos para mostrar balances."
},
"Participants": {
"title": "Participantes",
"description": "Ingresa el nombre de cada participante.",
"protectedParticipant": "Estos participantes forman parte de gastos y no pueden ser eliminados.",
"new": "Nuevo",
"add": "Añadir participante",
"John": "Juan",
"Jane": "Maria",
"Jack": "Sergio"
},
"Settings": {
"title": "Ajustes locales",
"description": "Estos ajustes se establecen por dispositivo y se utilizan para personalizar su experiencia.",
"ActiveUserField": {
"label": "Usuario activo",
"placeholder": "Selecciona un participante...",
"none": "Ninguno",
"description": "Usuario que paga los gastos por defecto."
},
"save": "Guardar",
"saving": "Guardando",
"create": "Crear",
"creating": "Creando",
"cancel": "Cancelar"
}
},
"ExpenseForm": {
"Income": {
"create": "Crear ingreso",
"edit": "Editar ingreso",
"TitleField": {
"label": "Título del ingreso",
"placeholder": "Comida Hamburgeseria",
"description": "Introduce una descripción para este ingreso."
},
"DateField": {
"label": "Fecha del ingreso",
"description": "Ingresa la fecha en que se recibio el ingreso."
},
"categoryFieldDescription": "Seleccione la categoría de ingresos.",
"paidByField": {
"label": "Recibido por",
"description": "Seleccione el participante que recibió los ingresos."
},
"paidFor": {
"title": "Recibido para for",
"description": "Seleccione para quién se recibió el ingreso."
},
"splitModeDescription": "Seleccione como quieres dividir el ingreso.",
"attachDescription": "Ver y adjuntar tickets para el ingreso."
},
"Expense": {
"create": "Crear gasto",
"edit": "Editar gasto",
"TitleField": {
"label": "Título del gasto",
"placeholder": "Monday evening restaurant",
"description": "Enter a description for the expense."
},
"DateField": {
"label": "Fecha del gasto",
"description": "Ingresa la fecha en que se recibio el gasto."
},
"categoryFieldDescription": "Select the expense category.",
"paidByField": {
"label": "Pagado por",
"description": "Seleccione el participante que pagó el gasto."
},
"paidFor": {
"title": "Pagado para",
"description": "Seleccione para quién se pagó el gasto."
},
"splitModeDescription": "Seleccione como quieres dividir el gasto.",
"attachDescription": "Ver y adjuntar tickets para el gasto."
},
"amountField": {
"label": "Cantidad"
},
"isReimbursementField": {
"label": "Esto es un reembolso"
},
"categoryField": {
"label": "Categoria"
},
"notesField": {
"label": "Notas"
},
"selectNone": "Seleccionar ninguno",
"selectAll": "Seleccionar todos",
"shares": "partes",
"advancedOptions": "Opciones avanzadas",
"SplitModeField": {
"label": "Modo de división",
"evenly": "Uniformemente",
"byShares": "Desigualmente Por partes",
"byPercentage": "Desigualmente por porcentaje",
"byAmount": "Desigualmente por cantidad",
"saveAsDefault": "Guardar como modo preferido"
},
"DeletePopup": {
"label": "Borrar",
"title": "Borrar gasto?",
"description": "Seguro que quieres borrar este gasto? Esta acción es irreversible.",
"yes": "Si",
"cancel": "Cancelar"
},
"attachDocuments": "Adjuntar documentos",
"create": "Crear",
"creating": "Creando",
"save": "Guardar",
"saving": "Guardando",
"cancel": "Cancelar"
},
"ExpenseDocumentsInput": {
"TooBigToast": {
"title": "El archivo es demasiado grande",
"description": "El tamaño máximo de archivo que puede cargar es {maxSize}. El tuyo pesa ${size}."
},
"ErrorToast": {
"title": "Error al cargar el documento",
"description": "Ha ocurrido un error al cargar el documento. Vuelva a intentarlo más tarde o seleccione otro archivo.",
"retry": "Reintentar"
}
},
"CreateFromReceipt": {
"Dialog": {
"triggerTitle": "Crear gasto desde ticket",
"title": "Crear desde ticket",
"description": "Extraer la información de gastos de una foto de recibo.",
"body": "Sube la foto de un recibo y lo escanearemos para extraer la información del gasto si podemos.",
"selectImage": "Seleccionar imagen…",
"titleLabel": "Titulo:",
"categoryLabel": "Categoria:",
"amountLabel": "Cantidad:",
"dateLabel": "Fecha:",
"editNext": "A continuación podrá editar la información de los gastos.",
"continue": "Continuar"
},
"unknown": "Desconocido",
"TooBigToast": {
"title": "El archivo es demasiado grande",
"description": "El tamaño máximo de archivo que puede cargar es {maxSize}. El tuyo pesa ${size}."
},
"ErrorToast": {
"title": "Error al cargar el documento",
"description": "Ha ocurrido un error al cargar el documento. Vuelva a intentarlo más tarde o seleccione otro archivo.",
"retry": "Reintentar"
}
},
"Balances": {
"title": "Balances",
"description": "Se trata del importe que ha pagado o ha recibido cada participante.",
"Reimbursements": {
"title": "Reembolsos propuestos",
"description": "He aquí algunas sugerencias para optimizar los reembolsos entre los participantes.",
"noImbursements": "Parece que tu grupo no necesita ningún reembolso 😁",
"owes": "<strong>{from}</strong> debe <strong>{to}</strong>",
"markAsPaid": "Marcar como pagado"
}
},
"Stats": {
"title": "Estadísticas",
"Totals": {
"title": "Totales",
"description": "Resumen de gastos de todo el grupo.",
"groupSpendings": "Gastos de todo el grupo",
"groupEarnings": "Ingresos de todo el grupo",
"yourSpendings": "Tus gastos totales",
"yourEarnings": "Tus ingresos totales",
"yourShare": "Tu parte final"
}
},
"Activity": {
"title": "Actividad",
"description": "Aquí encontrarás todas las actividades recientes en tu grupo.",
"noActivity": "No hay actividad reciente en este grupo.",
"someone": "Alguien",
"settingsModified": "Los ajustes del grupo fueron modificados por <strong>{participant}</strong>.",
"expenseCreated": "Gasto <em>{expense}</em> creado por <strong>{participant}</strong>.",
"expenseUpdated": "Gasto <em>{expense}</em> actualizado por <strong>{participant}</strong>.",
"expenseDeleted": "Gasto <em>{expense}</em> borrado por <strong>{participant}</strong>.",
"Groups": {
"today": "Hoy",
"yesterday": "Ayer",
"earlierThisWeek": "A principios de esta semana",
"lastWeek": "La semana pasada",
"earlierThisMonth": "A principios de este mes",
"lastMonth": "El mes pasado",
"earlierThisYear": "A principios de este año",
"lastYear": "El ultimo año",
"older": "Más antiguos"
}
},
"Information": {
"title": "Información",
"description": "Utilice este lugar para añadir cualquier información que pueda ser relevante para los participantes del grupo.",
"empty": "Aún no hay información sobre el grupo."
},
"Settings": {
"title": "Ajustes"
},
"Locale": {
"en-US": "English",
"fi": "Suomi",
"fr-FR": "Français",
"es": "Español",
"de-DE": "Deutsch",
"zh-CN": "Chinese (Simplified)",
"ru-RU": "Русский"
},
"Share": {
"title": "Compartir",
"description": "Para que otros participantes puedan ver el grupo y añadir gastos, compárteles su URL.",
"warning": "Cuidado!",
"warningHelp": "Todas las personas que tengan la URL del grupo podrán ver y editar los gastos. ¡Comparte con precaución!"
},
"SchemaErrors": {
"min1": "Introduzca al menos un carácter.",
"min2": "Introduzca al menos dos carácter.",
"max5": "Introduzca al menos cinco carácter.",
"max50": "Introduzca al menos treinta carácter.",
"duplicateParticipantName": "Otro participante ya tiene este nombre",
"titleRequired": "Por favor, introduzca un título",
"invalidNumber": "Número inválido",
"amountRequired": "Debe introducir un importe",
"amountNotZero": "El importe no debe ser cero.",
"amountTenMillion": "El importe debe ser inferior a 10.000.000.",
"paidByRequired": "Debe seleccionar un participante",
"paidForMin1": "El gasto debe ser pagado por al menos un participante",
"noZeroShares": "Todas las participaciones deben ser superiores a 0",
"amountSum": "La suma de los importes debe ser igual al importe del gasto",
"percentageSum": "Suma de porcentajes debe ser igual a 100"
},
"Categories": {
"search": "Buscar categoría...",
"noCategory": "Categoría no encontrada!",
"Uncategorized": {
"heading": "Sin categoría",
"General": "General",
"Payment": "Pago"
},
"Entertainment": {
"heading": "Ocio",
"Entertainment": "Ocio",
"Games": "Juegos",
"Movies": "Películas",
"Music": "Musica",
"Sports": "Deportes"
},
"Food and Drink": {
"heading": "Comida y bebida",
"Food and Drink": "Comida y bebida",
"Dining Out": "Comer fuera",
"Groceries": "Comestibles",
"Liquor": "Licores"
},
"Home": {
"heading": "Hogar",
"Home": "Hogar",
"Electronics": "Electrónica",
"Furniture": "Muebles",
"Household Supplies": "Suministros del hogar",
"Maintenance": "Mantenimiento",
"Mortgage": "Hipoteca",
"Pets": "Mascotas",
"Rent": "Alquiler",
"Services": "Servicios"
},
"Life": {
"heading": "Vida",
"Childcare": "Cuidado de niños",
"Clothing": "Ropa",
"Education": "Educación",
"Gifts": "Regalos",
"Insurance": "Seguro",
"Medical Expenses": "Gastos médicos",
"Taxes": "Impuestos"
},
"Transportation": {
"heading": "Transporte",
"Transportation": "Transporte",
"Bicycle": "Bicicleta",
"Bus/Train": "Autobús/Tren",
"Car": "Coche",
"Gas/Fuel": "Gasolina/Combustible",
"Hotel": "Hotel",
"Parking": "Parking",
"Plane": "Avión",
"Taxi": "Taxi"
},
"Utilities": {
"heading": "Utilidades",
"Utilities": "Utilidades",
"Cleaning": "Limpieza",
"Electricity": "Electricidad",
"Heat/Gas": "Calefacción/Gas",
"Trash": "Basura",
"TV/Phone/Internet": "TV/Teléfono/Internet",
"Water": "Agua"
}
}
}

View File

@@ -1,4 +1,12 @@
{
"Homepage": {
"title": "Jaa kulut ystävien ja perheen kanssa",
"description": "Tervetuloa uuteen Spliit-instanssiisi!",
"button": {
"groups": "Siirry ryhmiin",
"github": "GitHub"
}
},
"Header": {
"groups": "Ryhmät"
},
@@ -287,7 +295,12 @@
},
"Locale": {
"en-US": "English",
"fi": "Suomi"
"fi": "Suomi",
"fr-FR": "Français",
"es": "Español",
"de-DE": "Deutsch",
"zh-CN": "Chinese (Simplified)",
"ru-RU": "Русский"
},
"Share": {
"title": "Jaa",

396
messages/fr-FR.json Normal file
View File

@@ -0,0 +1,396 @@
{
"Homepage": {
"title": "Partagez <strong>vos dépenses</strong> avec <strong>vos amis</strong> & <strong>votre famille :)</strong>",
"description": "Bienvenue sur votre instance <strong>Spliit</strong> !",
"button": {
"groups": "Accéder aux groupes",
"github": "GitHub"
}
},
"Header": {
"groups": "Groupes"
},
"Footer": {
"madeIn": "Made in Montréal, Québec 🇨🇦",
"builtBy": "Développé par <author>Sebastien Castiel</author> et <source>contributeurs</source>"
},
"Expenses": {
"title": "Dépenses",
"description": "Voici les dépenses que vous avez créées pour votre groupe.",
"create": "Créer une dépense",
"createFirst": "Créer la première :)",
"noExpenses": "Votre groupe n'a effectué aucune dépense pour le moment.",
"exportJson": "Exporter en JSON",
"searchPlaceholder": "Rechercher une dépense…",
"ActiveUserModal": {
"title": "Qui êtes-vous ?",
"description": "Dites-nous quel participant vous êtes pour personnaliser l'affichage des informations.",
"nobody": "Je ne veux sélectionner personne",
"save": "Sauvegarder les modifications",
"footer": "Ce paramètre peut être modifié plus tard dans les paramètres du groupe."
},
"Groups": {
"upcoming": "À venir",
"thisWeek": "Cette semaine",
"earlierThisMonth": "Plus tôt ce mois-ci",
"lastMonth": "Le mois dernier",
"earlierThisYear": "Plus tôt cette année",
"lastYera": "L'année dernière",
"older": "Plus ancien"
}
},
"ExpenseCard": {
"paidBy": "Payé par <strong>{paidBy}</strong> pour <paidFor></paidFor>",
"receivedBy": "Reçu par <strong>{paidBy}</strong> pour <paidFor></paidFor>",
"yourBalance": "Votre solde :"
},
"Groups": {
"myGroups": "Mes groupes",
"create": "Créer",
"loadingRecent": "Chargement des groupes récents…",
"NoRecent": {
"description": "Vous n'avez visité aucun groupe récemment.",
"create": "Créer un groupe",
"orAsk": "ou demandez à un ami de vous envoyer le lien d'un groupe existant."
},
"recent": "Groupes récents",
"starred": "Groupes favoris",
"archived": "Groupes archivés",
"archive": "Archiver le groupe",
"unarchive": "Désarchiver le groupe",
"removeRecent": "Supprimer des groupes récents",
"RecentRemovedToast": {
"title": "Le groupe a été supprimé",
"description": "Le groupe a été supprimé de votre liste de groupes récents.",
"undoAlt": "Annuler la suppression du groupe",
"undo": "Annuler"
},
"AddByURL": {
"button": "Ajouter par URL",
"title": "Ajouter un groupe par URL",
"description": "Si un groupe a été partagé avec vous, vous pouvez coller son URL ici pour l'ajouter à votre liste.",
"error": "Oups, nous ne pouvons pas trouver le groupe à partir de l'URL que vous avez fournie…"
},
"NotFound": {
"text": "Ce groupe n'existe pas.",
"link": "Aller aux groupes récemment visités"
}
},
"GroupForm": {
"title": "Informations sur le groupe",
"NameField": {
"label": "Nom du groupe",
"placeholder": "Vacances d'été",
"description": "Entrez un nom pour votre groupe."
},
"InformationField": {
"label": "Informations sur le groupe",
"placeholder": "Quelles informations sont pertinentes pour les participants du groupe ?"
},
"CurrencyField": {
"label": "Symbole monétaire",
"placeholder": "$, €, £…",
"description": "Nous l'utiliserons pour afficher les montants."
},
"Participants": {
"title": "Participants",
"description": "Entrez le nom de chaque participant.",
"protectedParticipant": "Ce participant fait partie des dépenses et ne peut pas être supprimé.",
"new": "Nouveau",
"add": "Ajouter un participant",
"John": "John",
"Jane": "Jane",
"Jack": "Jack"
},
"Settings": {
"title": "Paramètres locaux",
"description": "Ces paramètres sont définis par appareil et sont utilisés pour personnaliser votre expérience.",
"ActiveUserField": {
"label": "Utilisateur actif",
"placeholder": "Sélectionner un participant",
"none": "Aucun",
"description": "Utilisateur utilisé comme défaut pour payer les dépenses."
},
"save": "Sauvegarder",
"saving": "Sauvegarde…",
"create": "Créer",
"creating": "Création…",
"cancel": "Annuler"
}
},
"ExpenseForm": {
"Income": {
"create": "Créer un revenu",
"edit": "Modifier le revenu",
"TitleField": {
"label": "Titre du revenu",
"placeholder": "Restaurant du lundi soir",
"description": "Entrez une description pour le revenu."
},
"DateField": {
"label": "Date du revenu",
"description": "Entrez la date à laquelle le revenu a été reçu."
},
"categoryFieldDescription": "Sélectionnez la catégorie de revenu.",
"paidByField": {
"label": "Reçu par",
"description": "Sélectionnez le participant qui a reçu le revenu."
},
"paidFor": {
"title": "Reçu pour",
"description": "Sélectionnez pour qui le revenu a été reçu."
},
"splitModeDescription": "Sélectionnez comment diviser le revenu.",
"attachDescription": "Voir et joindre des reçus au revenu."
},
"Expense": {
"create": "Créer une dépense",
"edit": "Modifier la dépense",
"TitleField": {
"label": "Titre de la dépense",
"placeholder": "Restaurant du lundi soir",
"description": "Entrez une description pour la dépense."
},
"DateField": {
"label": "Date de la dépense",
"description": "Entrez la date à laquelle la dépense a été payée."
},
"categoryFieldDescription": "Sélectionnez la catégorie de dépense.",
"paidByField": {
"label": "Payé par",
"description": "Sélectionnez le participant qui a réglé la dépense."
},
"paidFor": {
"title": "Payé pour",
"description": "Sélectionnez les participants concernés"
},
"splitModeDescription": "Sélectionnez comment diviser la dépense.",
"attachDescription": "Voir et joindre des reçus à la dépense."
},
"amountField": {
"label": "Montant"
},
"isReimbursementField": {
"label": "C'est un remboursement"
},
"categoryField": {
"label": "Catégorie"
},
"notesField": {
"label": "Notes"
},
"selectNone": "Tout désélectionner",
"selectAll": "Tout sélectionner",
"shares": "part(s)",
"advancedOptions": "Options de répartition avancées…",
"SplitModeField": {
"label": "Mode de répartition",
"evenly": "Également",
"byShares": "Inégalement Par parts",
"byPercentage": "Inégalement Par pourcentage",
"byAmount": "Inégalement Par montant",
"saveAsDefault": "Enregistrer comme options de répartition par défaut"
},
"DeletePopup": {
"label": "Supprimer",
"title": "Supprimer cette dépense ?",
"description": "Voulez-vous vraiment supprimer cette dépense ? Cette action est irréversible.",
"yes": "Oui",
"cancel": "Annuler"
},
"attachDocuments": "Joindre des documents",
"create": "Créer",
"creating": "Création…",
"save": "Sauvegarder",
"saving": "Sauvegarde…",
"cancel": "Annuler"
},
"ExpenseDocumentsInput": {
"TooBigToast": {
"title": "Le fichier est trop grand",
"description": "La taille maximale du fichier que vous pouvez télécharger est {maxSize}. La vôtre est ${size}."
},
"ErrorToast": {
"title": "Erreur lors du téléchargement du document",
"description": "Un problème est survenu lors du téléchargement du document. Veuillez réessayer plus tard ou sélectionner un fichier différent.",
"retry": "Réessayer"
}
},
"CreateFromReceipt": {
"Dialog": {
"triggerTitle": "Créer une dépense à partir du reçu",
"title": "Créer à partir du reçu",
"description": "Extraire les informations de la dépense à partir d'une photo de reçu.",
"body": "Téléchargez la photo d'un reçu, et nous l'analyserons pour extraire les informations de la dépense si possible.",
"selectImage": "Sélectionner une image…",
"titleLabel": "Titre :",
"categoryLabel": "Catégorie :",
"amountLabel": "Montant :",
"dateLabel": "Date :",
"editNext": "Vous pourrez modifier les informations de la dépense ensuite.",
"continue": "Continuer"
},
"unknown": "Inconnu",
"TooBigToast": {
"title": "Le fichier est trop grand",
"description": "La taille maximale du fichier que vous pouvez télécharger est {maxSize}. La vôtre est ${size}."
},
"ErrorToast": {
"title": "Erreur lors du téléchargement du document",
"description": "Un problème est survenu lors du téléchargement du document. Veuillez réessayer plus tard ou sélectionner un fichier différent.",
"retry": "Réessayer"
}
},
"Balances": {
"title": "Équilibres",
"description": "Voici le montant que chaque participant a payé ou doit rembourser.",
"Reimbursements": {
"title": "Remboursements suggérés",
"description": "Voici des suggestions pour des remboursements optimisés entre les participants.",
"noImbursements": "Les dépenses effectuées ne nécessitent pas d'équilibrage 😁",
"owes": "<strong>{from}</strong> doit à <strong>{to}</strong>",
"markAsPaid": "Marquer comme payé"
}
},
"Stats": {
"title": "Statistiques",
"Totals": {
"title": "Totaux",
"description": "Résumé des dépenses du groupe entier.",
"groupSpendings": "Total des dépenses du groupe",
"groupEarnings": "Total des revenus du groupe",
"yourSpendings": "Vos dépenses totales",
"yourEarnings": "Vos revenus totaux",
"yourShare": "Votre part totale"
}
},
"Activity": {
"title": "Activité",
"description": "Vue d'ensemble de toute l'activité dans ce groupe.",
"noActivity": "Il n'y a pas encore d'activité dans votre groupe.",
"someone": "Quelqu'un",
"settingsModified": "Les paramètres du groupe ont été modifiés par <strong>{participant}</strong>.",
"expenseCreated": "Dépense <em>{expense}</em> créée par <strong>{participant}</strong>.",
"expenseUpdated": "Dépense <em>{expense}</em> mise à jour par <strong>{participant}</strong>.",
"expenseDeleted": "Dépense <em>{expense}</em> supprimée par <strong>{participant}</strong>.",
"Groups": {
"today": "Aujourd'hui",
"yesterday": "Hier",
"earlierThisWeek": "Plus tôt cette semaine",
"lastWeek": "La semaine dernière",
"earlierThisMonth": "Plus tôt ce mois-ci",
"lastMonth": "Le mois dernier",
"earlierThisYear": "Plus tôt cette année",
"lastYear": "L'année dernière",
"older": "Plus ancien"
}
},
"Information": {
"title": "Information",
"description": "Utilisez cet espace pour ajouter toute information qui pourrait être pertinente pour les participants du groupe.",
"empty": "Aucune information pour le moment."
},
"Settings": {
"title": "Paramètres"
},
"Locale": {
"en-US": "English",
"fi": "Suomi",
"fr-FR": "Français",
"es": "Español",
"de-DE": "Deutsch",
"zh-CN": "Chinese (Simplified)",
"ru-RU": "Русский"
},
"Share": {
"title": "Partager",
"description": "Pour que d'autres participants puissent voir le groupe et ajouter des dépenses, partagez son URL avec eux.",
"warning": "Avertissement !",
"warningHelp": "Toute personne ayant l'URL du groupe pourra voir et modifier les dépenses. Partagez avec prudence !"
},
"SchemaErrors": {
"min1": "Entrez au moins un caractère.",
"min2": "Entrez au moins deux caractères.",
"max5": "Entrez au maximum cinq caractères.",
"max50": "Entrez au maximum 50 caractères.",
"duplicateParticipantName": "Un autre participant a déjà ce nom.",
"titleRequired": "Veuillez entrer un titre.",
"invalidNumber": "Nombre invalide.",
"amountRequired": "Vous devez entrer un montant.",
"amountNotZero": "Le montant ne doit pas être zéro.",
"amountTenMillion": "Le montant doit être inférieur à 10 000 000.",
"paidByRequired": "Vous devez sélectionner un participant.",
"paidForMin1": "La dépense doit concerner au moins un participant.",
"noZeroShares": "Toutes les parts doivent être supérieures à 0.",
"amountSum": "La somme des montants doit être égale au montant de la dépense.",
"percentageSum": "La somme des pourcentages doit être égale à 100."
},
"Categories": {
"search": "Rechercher une catégorie…",
"noCategory": "Aucune catégorie trouvée.",
"Uncategorized": {
"heading": "Non classé",
"General": "Général",
"Payment": "Paiement"
},
"Entertainment": {
"heading": "Divertissement",
"Entertainment": "Divertissement",
"Games": "Jeux",
"Movies": "Films",
"Music": "Musique",
"Sports": "Sports"
},
"Food and Drink": {
"heading": "Nourriture et boissons",
"Food and Drink": "Nourriture et boissons",
"Dining Out": "Repas au restaurant",
"Groceries": "Épicerie",
"Liquor": "Alcool"
},
"Home": {
"heading": "Maison",
"Home": "Maison",
"Electronics": "Électronique",
"Furniture": "Mobilier",
"Household Supplies": "Fournitures ménagères",
"Maintenance": "Entretien",
"Mortgage": "Hypothèque",
"Pets": "Animaux",
"Rent": "Loyer",
"Services": "Services"
},
"Life": {
"heading": "Vie",
"Childcare": "Garde d'enfants",
"Clothing": "Vêtements",
"Education": "Éducation",
"Gifts": "Cadeaux",
"Insurance": "Assurance",
"Medical Expenses": "Dépenses médicales",
"Taxes": "Impôts"
},
"Transportation": {
"heading": "Transport",
"Transportation": "Transport",
"Bicycle": "Bicyclette",
"Bus/Train": "Bus/Train",
"Car": "Voiture",
"Gas/Fuel": "Essence/Carburant",
"Hotel": "Hôtel",
"Parking": "Parking",
"Plane": "Avion",
"Taxi": "Taxi"
},
"Utilities": {
"heading": "Services publics",
"Utilities": "Services publics",
"Cleaning": "Nettoyage",
"Electricity": "Électricité",
"Heat/Gas": "Chauffage/Gaz",
"Trash": "Poubelle",
"TV/Phone/Internet": "TV/Téléphone/Internet",
"Water": "Eau"
}
}
}

396
messages/ru-RU.json Normal file
View File

@@ -0,0 +1,396 @@
{
"Homepage": {
"title": "Делитесь <strong>расходами</strong> с <strong>друзьями и семьей</strong>",
"description": "Добро пожаловать в вашу новую инстанцию <strong>Spliit</strong>!",
"button": {
"groups": "Перейти к группам",
"github": "GitHub"
}
},
"Header": {
"groups": "Группы"
},
"Footer": {
"madeIn": "Сделано в Монреале, Квебек 🇨🇦",
"builtBy": "Создано <author>Sebastien Castiel</author> и <source>соавторами</source>"
},
"Expenses": {
"title": "Расходы",
"description": "В этом разделе находятся расходы вашей группы.",
"create": "Создать расход",
"createFirst": "Создать первый расход",
"noExpenses": "У вашей группы пока что нет расходов.",
"exportJson": "Экспортировать в JSON",
"searchPlaceholder": "Поиск расходов…",
"ActiveUserModal": {
"title": "Кто вы?",
"description": "Скажите нам, кто вы из этого списка, чтобы мы могли подстроить интерфейс под вас.",
"nobody": "Не хочу выбирать",
"save": "Сохранить изменения",
"footer": "Вы сможете изменить это в настройках группы."
},
"Groups": {
"upcoming": "Будущее",
"thisWeek": "На этой неделе",
"earlierThisMonth": "Ранее в этом месяце",
"lastMonth": "В прошлом месяце",
"earlierThisYear": "Ранее в этом году",
"lastYera": "В прошлом году",
"older": "Очень давно"
}
},
"ExpenseCard": {
"paidBy": "Потратил <strong>{paidBy}</strong> за <paidFor></paidFor>",
"receivedBy": "Получил <strong>{paidBy}</strong> за <paidFor></paidFor>",
"yourBalance": "Изменение баланса участника:"
},
"Groups": {
"myGroups": "Мои группы",
"create": "Создать",
"loadingRecent": "Загрузка недавних групп…",
"NoRecent": {
"description": "У вас нет недавних групп.",
"create": "Вы можете создать группу",
"orAsk": "или попросить вашего друга отправить вам ссылку на существующую."
},
"recent": "Недавние группы",
"starred": "Избранные",
"archived": "Архивированные группы",
"archive": "Архивировать группу",
"unarchive": "Восстановить группу",
"removeRecent": "Убрать группу из недавних",
"RecentRemovedToast": {
"title": "Группа убрана",
"description": "Группа была убрана из вашего списка недавних групп.",
"undoAlt": "Отменить удаление группы из этого списка",
"undo": "Отмена"
},
"AddByURL": {
"button": "Добавить по URL",
"title": "Добавить группу по URL",
"description": "Если с вами поделились ссылкой на группу, вставьте ее сюда, чтобы добавить ее в ваш список.",
"error": "К сожалению, мы не смогли найти группу по этому URL."
},
"NotFound": {
"text": "Этой группы не существует",
"link": "Перейти к списку недавних групп"
}
},
"GroupForm": {
"title": "Сведения о группе",
"NameField": {
"label": "Название группы",
"placeholder": "Летние поездки",
"description": "Введите название вашей группы."
},
"InformationField": {
"label": "Информация о группе",
"placeholder": "Что важно знать участникам этой группы?"
},
"CurrencyField": {
"label": "Символ валюты",
"placeholder": "$, €, £…",
"description": "Этот символ будет использован для отображений денежных сумм."
},
"Participants": {
"title": "Участники",
"description": "Введите имя каждого участника.",
"protectedParticipant": "Этот участник — часть расходов, и поэтому не может быть удален.",
"new": "Новый участник",
"add": "Добавить участника",
"John": "Александр",
"Jane": "Михаил",
"Jack": "Иван"
},
"Settings": {
"title": "Локальные настройки",
"description": "Эти настройки хранятся на вашем устройстве и используются для подстройки интерфейса для вас.",
"ActiveUserField": {
"label": "Активный участник",
"placeholder": "Выберите участника",
"none": "Не выбран",
"description": "Этот участник будет автоматически выбран при создании нового расхода."
},
"save": "Сохранить",
"saving": "Сохранение…",
"create": "Создать",
"creating": "Создание…",
"cancel": "Отмена"
}
},
"ExpenseForm": {
"Income": {
"create": "Создать доход",
"edit": "Изменить доход",
"TitleField": {
"label": "Название доходв",
"placeholder": "Поход в ресторан",
"description": "Введите описание для этого дохода."
},
"DateField": {
"label": "Дата дохода",
"description": "Введите дату, когда этот доход был получен."
},
"categoryFieldDescription": "Выберите категорию дохода.",
"paidByField": {
"label": "Получивший",
"description": "Выберите участника, который получил этот доход."
},
"paidFor": {
"title": "Участники",
"description": "Выберите тех, между кем этот доход будет распределен."
},
"splitModeDescription": "Выберите, как доход необходимо распределить между людьми.",
"attachDescription": "Просмотр и прикрепление чеков к этому расходу."
},
"Expense": {
"create": "Создать расход",
"edit": "Изменить расход",
"TitleField": {
"label": "Название расхода",
"placeholder": "Поход в ресторан",
"description": "Введите описание для этого расхода."
},
"DateField": {
"label": "Дата расхода",
"description": "Введите дату, когда этот расход был совершен."
},
"categoryFieldDescription": "Выберите категорию расхода.",
"paidByField": {
"label": "Оплативший",
"description": "Выберите участника, который оплатил этот расход."
},
"paidFor": {
"title": "Участники",
"description": "Выберите тех, между кем этот расход будет распределен. Если этот расход — возмещение участнику (участникам), выберите только его (их)."
},
"splitModeDescription": "Выберите, как расход необходимо распределить между людьми.",
"attachDescription": "Просмотр и прикрепление чеков к этому доходу."
},
"amountField": {
"label": "Сумма"
},
"isReimbursementField": {
"label": "Этот расход является возмещением"
},
"categoryField": {
"label": "Категория"
},
"notesField": {
"label": "Заметки"
},
"selectNone": "Выбрать никого",
"selectAll": "Выбрать всех",
"shares": "доля(и)",
"advancedOptions": "Дополнительные настройки распределения…",
"SplitModeField": {
"label": "Режим разделения",
"evenly": "Равный",
"byShares": "Неравный По долям",
"byPercentage": "Неравный По процентам",
"byAmount": "Неравный По суммам",
"saveAsDefault": "Сделать режимом по умолчанию"
},
"DeletePopup": {
"label": "Удалить",
"title": "Удалить этот расход?",
"description": "Вы действительно хотите удалить этот расход? Это действие нельзя отменить.",
"yes": "Удалить",
"cancel": "Отмена"
},
"attachDocuments": "Прикрепить документы",
"create": "Создать",
"creating": "Создание…",
"save": "Сохранить",
"saving": "Сохранение…",
"cancel": "Отмена"
},
"ExpenseDocumentsInput": {
"TooBigToast": {
"title": "Файл слишком большой",
"description": "Максимальный размер файла, который можно загрузить — {maxSize}. Размер вашего файла — ${size}."
},
"ErrorToast": {
"title": "Ошибка при загрузке документа",
"description": "При загрузке документа что-то пошло не так. Пожалуйста, повторите позднее или попробуйте загрузить другой файл.",
"retry": "Повторить"
}
},
"CreateFromReceipt": {
"Dialog": {
"triggerTitle": "Создать расход из чека",
"title": "Создать из чека",
"description": "Извлечение информации о расходах из фотографии чека",
"body": "Загрузите фотографию чека, и мы попытаемся отсканировать его, чтобы извлечь информацию о расходах.",
"selectImage": "Выбрать изображение…",
"titleLabel": "Название:",
"categoryLabel": "Категория:",
"amountLabel": "Сумма:",
"dateLabel": "Дата:",
"editNext": "Вы сможете изменить эту информацию позднее.",
"continue": "Продолжить"
},
"unknown": "Неизвестно",
"TooBigToast": {
"title": "Файл слишком большой",
"description": "Максимальный размер файла, который можно загрузить — {maxSize}. Размер вашего файла — ${size}."
},
"ErrorToast": {
"title": "Ошибка при загрузке документа",
"description": "При загрузке документа что-то пошло не так. Пожалуйста, повторите позднее или попробуйте загрузить другой файл.",
"retry": "Повторить"
}
},
"Balances": {
"title": "Балансы",
"description": "Это список балансов всех участников группы. Баланс увеличивается у тех, кто оплачивает расход, и уменьшается у тех, между кем он был распределен.",
"Reimbursements": {
"title": "Предложенные возмещения",
"description": "Вот список задолженностей между участниками.",
"noImbursements": "Похоже, все в расчете 😁",
"owes": "<strong>{from}</strong> должен <strong>{to}</strong>",
"markAsPaid": "Пометить оплаченным"
}
},
"Stats": {
"title": "Статистика",
"Totals": {
"title": "Итоговые суммы",
"description": "Общая информация о расходах вашей группы.",
"groupSpendings": "Всего потрачено группой",
"groupEarnings": "Всего заработано группой",
"yourSpendings": "Всего потрачено вами",
"yourEarnings": "Всего заработано вами",
"yourShare": "Ваша суммарная доля"
}
},
"Activity": {
"title": "Активность",
"description": "Обзор действий, совершенных участниками этой группы.",
"noActivity": "История действий пуста.",
"someone": "Аноним",
"settingsModified": "Настройки группы изменены участником <strong>{participant}</strong>.",
"expenseCreated": "Расход <em>{expense}</em> создан участником <strong>{participant}</strong>.",
"expenseUpdated": "Расход <em>{expense}</em> изменен участником <strong>{participant}</strong>.",
"expenseDeleted": "Расход <em>{expense}</em> удален участником <strong>{participant}</strong>.",
"Groups": {
"today": "Сегодня",
"yesterday": "Вчера",
"earlierThisWeek": "Ранее на этой неделе",
"lastWeek": "На прошлой неделе",
"earlierThisMonth": "Ранее в этом месяце",
"lastMonth": "В прошлом месяце",
"earlierThisYear": "Ранее в этом году",
"lastYear": "В прошлом году",
"older": "Очень давно"
}
},
"Information": {
"title": "Информация",
"description": "В этом разделе вы можете добавить важную для участников информацию.",
"empty": "Информации нет."
},
"Settings": {
"title": "Настройки"
},
"Locale": {
"en-US": "English",
"fi": "Suomi",
"fr-FR": "Français",
"es": "Español",
"de-DE": "Deutsch",
"zh-CN": "Chinese (Simplified)",
"ru-RU": "Русский"
},
"Share": {
"title": "Поделиться",
"description": "Чтобы другие участники получили доступ к этой группе и смогли добавлять расходы, отправьте им этот URL.",
"warning": "Внимание!",
"warningHelp": "Любой человек с доступом к этой ссылке сможет просматривать и редактировать расходы. Будьте осторожны!"
},
"SchemaErrors": {
"min1": "Введите как минимум один символ.",
"min2": "Введите как минимум два символа.",
"max5": "Введите максимум 5 символов.",
"max50": "Введите максимум 50 символов.",
"duplicateParticipantName": "Участник с таким именем уже существует.",
"titleRequired": "Пожалуйста, введите название.",
"invalidNumber": "Неверное число.",
"amountRequired": "Пожалуйста, введите сумму.",
"amountNotZero": "Сумма не может быть нулевой.",
"amountTenMillion": "Сумма должна быть меньше 10 000 000.",
"paidByRequired": "Пожалуйста, выберите участника.",
"paidForMin1": "За этот расход должен заплатить как минимум один участник.",
"noZeroShares": "Все доли должны быть больше 0.",
"amountSum": "Сумма расхода должна быть равна сумме значений, распределенных между участниками.",
"percentageSum": "Сумма процентов должна быть равна 100."
},
"Categories": {
"search": "Поиск категорий...",
"noCategory": "Категорий не нашлось.",
"Uncategorized": {
"heading": "Без категории",
"General": "Общее",
"Payment": "Выплата"
},
"Entertainment": {
"heading": "Развлечения",
"Entertainment": "Развлечения",
"Games": "Игры",
"Movies": "Кино",
"Music": "Музыка",
"Sports": "Спорт"
},
"Food and Drink": {
"heading": "Еда и напитки",
"Food and Drink": "Еда и напитки",
"Dining Out": "Рестораны и кафе",
"Groceries": "Продукты",
"Liquor": "Напитки"
},
"Home": {
"heading": "Дом",
"Home": "Дом",
"Electronics": "Электроника",
"Furniture": "Мебель",
"Household Supplies": "Расходные материалы",
"Maintenance": "Уборка",
"Mortgage": "Ипотека",
"Pets": "Домашние животные",
"Rent": "Аренда",
"Services": "Коммунальные расходы"
},
"Life": {
"heading": "Жизнь",
"Childcare": "Дети",
"Clothing": "Одежда",
"Education": "Образование",
"Gifts": "Подарки",
"Insurance": "Страховки",
"Medical Expenses": "Медицина",
"Taxes": "Налоги"
},
"Transportation": {
"heading": "Транспорт",
"Transportation": "Транспорт",
"Bicycle": "Велосипед",
"Bus/Train": "Автобусы и поезда",
"Car": "Авто",
"Gas/Fuel": "Топливо",
"Hotel": "Отели",
"Parking": "Парковка",
"Plane": "Самолеты",
"Taxi": "Такси"
},
"Utilities": {
"heading": "Коммунальные расходы",
"Utilities": "Коммунальные расходы",
"Cleaning": "Клининг",
"Electricity": "Электричество",
"Heat/Gas": "Отопление/Газ",
"Trash": "Утилизация отходов",
"TV/Phone/Internet": "ТВ/Телефон/Интернет",
"Water": "Вода"
}
}
}

396
messages/zh-CN.json Normal file
View File

@@ -0,0 +1,396 @@
{
"Homepage": {
"title": "与<strong>朋友和家人</strong>共享<strong>开支</strong>",
"description": "欢迎使用你的全新<strong>Spliit</strong>实例!",
"button": {
"groups": "前往群组",
"github": "GitHub"
}
},
"Header": {
"groups": "群组"
},
"Footer": {
"madeIn": "Made in Montréal, Québec 🇨🇦",
"builtBy": "由 <author>Sebastien Castiel</author> 以及 <source>社区贡献者们</source> 共同构建"
},
"Expenses": {
"title": "消费",
"description": "这里有你为你的群组创建的消费。",
"create": "创建消费",
"createFirst": "创建首个消费",
"noExpenses": "你的群组内目前没有任何消费。",
"exportJson": "导出到JSON",
"searchPlaceholder": "查找消费……",
"ActiveUserModal": {
"title": "你是哪位?",
"description": "告诉我们你在群组中的身份,以便定制你的信息呈现方式。",
"nobody": "我不想选择任何人",
"save": "保存变更",
"footer": "此项设定之后可以在群组设定中修改。"
},
"Groups": {
"upcoming": "即将到来",
"thisWeek": "本周",
"earlierThisMonth": "本月早些时候",
"lastMonth": "上个月",
"earlierThisYear": "本年早些时候",
"lastYera": "去年",
"older": "更早"
}
},
"ExpenseCard": {
"paidBy": "<strong>{paidBy}</strong> 为 <paidFor></paidFor> 支付。",
"receivedBy": "<strong>{paidBy}</strong> 为 <paidFor></paidFor> 接收。",
"yourBalance": "你的余额:"
},
"Groups": {
"myGroups": "我的群组",
"create": "创建",
"loadingRecent": "加载最近的群组……",
"NoRecent": {
"description": "你最近没有访问任何群组。",
"create": "创建一个群组",
"orAsk": "或者让你的朋友发给你现有群组的链接。"
},
"recent": "最近的群组",
"starred": "已收藏的群组",
"archived": "已归档的群组",
"archive": "归档群组",
"unarchive": "取消归档群组",
"removeRecent": "从最近的群组中删除",
"RecentRemovedToast": {
"title": "群组已删除",
"description": "群组已从你的最近群组列表中删除。",
"undoAlt": "撤销群组删除",
"undo": "撤销"
},
"AddByURL": {
"button": "使用URL添加",
"title": "使用URL添加一个群组",
"description": "如果你被分享了一个群组你可以将群组的URL粘贴到这里以添加这个群组。",
"error": "哎呀我们没办法根据你的URL找到相应的群组……"
},
"NotFound": {
"text": "该群组不存在。",
"link": "跳转到最近访问过的群组"
}
},
"GroupForm": {
"title": "群组信息",
"NameField": {
"label": "群组名",
"placeholder": "暑假",
"description": "为你的群组输入一个名字。"
},
"InformationField": {
"label": "群组信息",
"placeholder": "哪些信息与群组成员有关?"
},
"CurrencyField": {
"label": "货币标志",
"placeholder": "$, €, £…",
"description": "我们根据这个显示相应的货币金额。"
},
"Participants": {
"title": "群组成员",
"description": "输入每位成员的名字。",
"protectedParticipant": "群组成员是消费的一部分,不可删除。",
"new": "新建",
"add": "添加群组成员",
"John": "John",
"Jane": "Jane",
"Jack": "Jack"
},
"Settings": {
"title": "本地设定",
"description": "这些设定是按设备设定的,用于定制你的使用体验。",
"ActiveUserField": {
"label": "当前用户",
"placeholder": "选择一个群组成员",
"none": "无",
"description": "用于支付消费的默认用户。"
},
"save": "保存",
"saving": "保存中……",
"create": "创建",
"creating": "创建中",
"cancel": "取消"
}
},
"ExpenseForm": {
"Income": {
"create": "创建收入",
"edit": "编辑收入",
"TitleField": {
"label": "收入标题",
"placeholder": "周一晚上的餐厅",
"description": "描述这个收入。"
},
"DateField": {
"label": "收入日期",
"description": "输入收到这笔收入的日期。"
},
"categoryFieldDescription": "选择收入类别。",
"paidByField": {
"label": "接收到",
"description": "选择接收到这笔收入的群组成员。"
},
"paidFor": {
"title": "接收给",
"description": "选择收入是为谁而收。"
},
"splitModeDescription": "选择如何划分这笔收入。",
"attachDescription": "查看并为这笔收入附加收据。"
},
"Expense": {
"create": "创建消费",
"edit": "编辑消费",
"TitleField": {
"label": "消费标题",
"placeholder": "周一晚上的餐厅",
"description": "描述这个消费。"
},
"DateField": {
"label": "消费日期",
"description": "输入支付这笔消费的日期。"
},
"categoryFieldDescription": "选择消费类别。",
"paidByField": {
"label": "支付自",
"description": "选择支付这笔消费的群组成员。"
},
"paidFor": {
"title": "支付给",
"description": "选择消费是为谁而支出。"
},
"splitModeDescription": "选择如何划分这笔消费。",
"attachDescription": "查看并为这笔消费附加收据。"
},
"amountField": {
"label": "金额"
},
"isReimbursementField": {
"label": "这是一笔报销款"
},
"categoryField": {
"label": "类别"
},
"notesField": {
"label": "附注"
},
"selectNone": "取消选中",
"selectAll": "全选",
"shares": "份额",
"advancedOptions": "高级分账选项……",
"SplitModeField": {
"label": "分账模式",
"evenly": "平均分配",
"byShares": "按份额分配",
"byPercentage": "按百分比分配",
"byAmount": "按金额分配",
"saveAsDefault": "保存为默认分账设置"
},
"DeletePopup": {
"label": "删除",
"title": "要删除这项消费吗?",
"description": "你真的确定要删除这项消费吗?此行动不可撤销。",
"yes": "确定",
"cancel": "取消"
},
"attachDocuments": "附加文档",
"create": "创建",
"creating": "创建中……",
"save": "保存",
"saving": "保存中……",
"cancel": "取消"
},
"ExpenseDocumentsInput": {
"TooBigToast": {
"title": "文件过大",
"description": "可上传的最大文件大小为{maxSize},你的文件为${size}。"
},
"ErrorToast": {
"title": "上传文档时发生错误",
"description": "上传文档时发生了一些错误。请稍后重试或更换文件。",
"retry": "重试"
}
},
"CreateFromReceipt": {
"Dialog": {
"triggerTitle": "从收据中创建消费",
"title": "从收据中创建",
"description": "从收据照片上提取消费信息。",
"body": "上传收据的图片,我们会尽可能地从中扫描出消费信息。",
"selectImage": "选择图片……",
"titleLabel": "标题:",
"categoryLabel": "类别:",
"amountLabel": "金额:",
"dateLabel": "日期:",
"editNext": "你之后可以修改消费的信息。",
"continue": "继续"
},
"unknown": "未知",
"TooBigToast": {
"title": "文件过大",
"description": "可上传的最大文件大小为{maxSize},你的文件为${size}。"
},
"ErrorToast": {
"title": "上传文档时发生错误",
"description": "上传文档时发生了一些错误。请稍后重试或更换文件。",
"retry": "重试"
}
},
"Balances": {
"title": "余额",
"description": "这是每位群组成员支付或被支付的金额。",
"Reimbursements": {
"title": "建议报销",
"description": "这里是优化群组成员之间报销的建议。",
"noImbursements": "看起来你的群组不需要任何报销😁",
"owes": "<strong>{from}</strong> 欠 <strong>{to}</strong>",
"markAsPaid": "标记为已支付"
}
},
"Stats": {
"title": "统计",
"Totals": {
"title": "总计",
"description": "整个群组的花费合计。",
"groupSpendings": "群组总计开销",
"groupEarnings": "群组总计收入",
"yourSpendings": "你的总计开销",
"yourEarnings": "你的总计收入",
"yourShare": "你的总计份额"
}
},
"Activity": {
"title": "活动",
"description": "该群组所有活动总览",
"noActivity": "你的群组目前没有任何活动。",
"someone": "某人",
"settingsModified": "群组设定已被<strong>{participant}</strong>更改。",
"expenseCreated": "消费 <em>{expense}</em> 由 <strong>{participant}</strong> 创建。",
"expenseUpdated": "消费 <em>{expense}</em> 由 <strong>{participant}</strong> 更新。",
"expenseDeleted": "消费 <em>{expense}</em> 由 <strong>{participant}</strong> 删除。",
"Groups": {
"today": "今天",
"yesterday": "昨天",
"earlierThisWeek": "这周早些时候",
"lastWeek": "上周",
"earlierThisMonth": "这月早些时候",
"lastMonth": "上月",
"earlierThisYear": "这年早些时候",
"lastYear": "上年",
"older": "更早"
}
},
"Information": {
"title": "信息",
"description": "使用此处以添加与群组成员相关的任何信息。",
"empty": "当前没有群组信息。"
},
"Settings": {
"title": "设定"
},
"Locale": {
"en-US": "English",
"fi": "Suomi",
"fr-FR": "Français",
"es": "Español",
"de-DE": "Deutsch",
"zh-CN": "Chinese (Simplified)",
"ru-RU": "Русский"
},
"Share": {
"title": "分享",
"description": "请将此URL分享给其他群组成员以使其可以查看群组并添加消费。",
"warning": "警告!",
"warningHelp": "任何持有群组URL的个体都有能够查看并编辑消费。请谨慎分享"
},
"SchemaErrors": {
"min1": "输入至少1个字符。",
"min2": "输入至少2个字符。",
"max5": "输入至少5个字符。",
"max50": "输入至少50个字符。",
"duplicateParticipantName": "此名字已被另一位群组成员占用。",
"titleRequired": "请输入标题。",
"invalidNumber": "无效数值。",
"amountRequired": "你必须输入一个金额。",
"amountNotZero": "金额不可以为0。",
"amountTenMillion": "金额必须小于10,000,000。",
"paidByRequired": "你必须选择一个群组成员。",
"paidForMin1": "这项消费必须支付给至少1名群组成员。",
"noZeroShares": "所有份额必须大于0。",
"amountSum": "金额之和必须等于消费的金额。",
"percentageSum": "百分比之和必须等于100。"
},
"Categories": {
"search": "搜寻类别……",
"noCategory": "未找到类别。",
"Uncategorized": {
"heading": "未分类",
"General": "一般",
"Payment": "支付"
},
"Entertainment": {
"heading": "娱乐",
"Entertainment": "娱乐",
"Games": "游戏",
"Movies": "电影",
"Music": "音乐",
"Sports": "运动"
},
"Food and Drink": {
"heading": "饮食",
"Food and Drink": "饮食",
"Dining Out": "下馆子",
"Groceries": "便利店",
"Liquor": "酒水"
},
"Home": {
"heading": "居家",
"Home": "居家",
"Electronics": "电费",
"Furniture": "家具",
"Household Supplies": "家庭日用品",
"Maintenance": "维护",
"Mortgage": "贷款",
"Pets": "宠物",
"Rent": "租金",
"Services": "服务"
},
"Life": {
"heading": "生活",
"Childcare": "儿童保育",
"Clothing": "衣物",
"Education": "教育",
"Gifts": "礼物",
"Insurance": "保险",
"Medical Expenses": "医疗支出",
"Taxes": "税"
},
"Transportation": {
"heading": "交通",
"Transportation": "交通",
"Bicycle": "自行车",
"Bus/Train": "巴士/列车",
"Car": "汽车",
"Gas/Fuel": "燃料",
"Hotel": "旅馆",
"Parking": "停车",
"Plane": "飞机",
"Taxi": "出租车"
},
"Utilities": {
"heading": "日常账单",
"Utilities": "日常账单",
"Cleaning": "清洁费",
"Electricity": "电费",
"Heat/Gas": "暖气/瓦斯",
"Trash": "垃圾",
"TV/Phone/Internet": "电视/手机/互联网",
"Water": "水"
}
}
}

4080
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,8 @@
"prettier": "prettier -w src",
"postinstall": "prisma migrate deploy && prisma generate",
"build-image": "./scripts/build-image.sh",
"start-container": "docker compose --env-file container.env up"
"start-container": "docker compose --env-file container.env up",
"test": "jest"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
@@ -62,8 +63,12 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@total-typescript/ts-reset": "^0.5.1",
"@types/content-disposition": "^0.5.8",
"@types/jest": "^29.5.12",
"@types/negotiator": "^0.6.3",
"@types/node": "^20",
"@types/pg": "^8.10.9",
@@ -74,10 +79,13 @@
"dotenv": "^16.3.1",
"eslint": "^8",
"eslint-config-next": "^14.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8",
"prettier": "^3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
"tailwindcss": "^3",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
}

View File

@@ -12,7 +12,7 @@ export async function extractExpenseInformationFromImage(imageUrl: string) {
const categories = await getCategories()
const body: ChatCompletionCreateParamsNonStreaming = {
model: 'gpt-4-vision-preview',
model: 'gpt-4-turbo',
messages: [
{
role: 'user',

View File

@@ -206,6 +206,7 @@ export function CreateFromReceiptButton({
groupCurrency,
receiptInfo.amount,
locale,
true,
)}
</>
) : (

View File

@@ -1,31 +1,35 @@
import { Button } from '@/components/ui/button'
import { Github } from 'lucide-react'
import { useTranslations } from 'next-intl'
import Link from 'next/link'
// FIX for https://github.com/vercel/next.js/issues/58615
// export const dynamic = 'force-dynamic'
export default function HomePage() {
const t = useTranslations()
return (
<main>
<section className="py-16 md:py-24 lg:py-32">
<div className="container flex max-w-screen-md flex-col items-center gap-4 text-center">
<h1 className="!leading-none font-bold text-3xl sm:text-5xl md:text-6xl lg:text-7xl landing-header py-2">
Share <strong>Expenses</strong> <br /> with <strong>Friends</strong>{' '}
& <strong>Family</strong>
<h1 className="!leading-none font-bold text-2xl sm:text-3xl md:text-4xl lg:text-5xl landing-header py-2">
{t.rich('Homepage.title', {
strong: (chunks) => <strong>{chunks}</strong>,
})}
</h1>
<p className="max-w-[42rem] leading-normal text-muted-foreground sm:text-xl sm:leading-8">
Welcome to your new <strong>Spliit</strong> instance! <br />
Customize this page by editing <em>src/app/page.tsx</em>.
{t.rich('Homepage.description', {
strong: (chunks) => <strong>{chunks}</strong>,
})}
</p>
<div className="flex gap-2">
<Button asChild>
<Link href="/groups">Go to groups</Link>
<Link href="/groups">{t('Homepage.button.groups')}</Link>
</Button>
<Button asChild variant="secondary">
<Link href="https://github.com/spliit-app/spliit">
<Github className="w-4 h-4 mr-2" />
GitHub
{t('Homepage.button.github')}
</Link>
</Button>
</div>

View File

@@ -253,28 +253,21 @@ export function ExpenseForm({
useEffect(() => {
setManuallyEditedParticipants(new Set())
const newPaidFor = defaultSplittingOptions.paidFor.map((participant) => ({
...participant,
shares: String(participant.shares) as unknown as number,
}))
form.setValue('paidFor', newPaidFor, { shouldValidate: true })
}, [form.watch('splitMode'), form.watch('amount')])
useEffect(() => {
const totalAmount = Number(form.getValues().amount) || 0
const paidFor = form.getValues().paidFor
const splitMode = form.getValues().splitMode
let newPaidFor = [...paidFor]
// Only auto-balance for split mode 'Unevenly - By amount'
if (
splitMode === 'EVENLY' ||
splitMode === 'BY_SHARES' ||
splitMode === 'BY_PERCENTAGE'
splitMode === 'BY_AMOUNT' &&
(form.getFieldState('paidFor').isDirty ||
form.getFieldState('amount').isDirty)
) {
return
} else {
// Only auto-balance for split mode 'Unevenly - By amount'
const totalAmount = Number(form.getValues().amount) || 0
const paidFor = form.getValues().paidFor
let newPaidFor = [...paidFor]
const editedParticipants = Array.from(manuallyEditedParticipants)
let remainingAmount = totalAmount
let remainingParticipants = newPaidFor.length - editedParticipants.length
@@ -569,18 +562,29 @@ export function ExpenseForm({
({ participant }) => participant === id,
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
{
participant: id,
shares: '1',
},
])
: field.onChange(
const options = {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
}
checked
? form.setValue(
'paidFor',
[
...field.value,
{
participant: id,
shares: '1' as unknown as number,
},
],
options,
)
: form.setValue(
'paidFor',
field.value?.filter(
(value) => value.participant !== id,
),
options,
)
}}
/>

View File

@@ -79,7 +79,7 @@ const FormItem = React.forwardRef<
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
<div ref={ref} className={cn("col-span-2 md:col-span-1 space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})

View File

@@ -1,7 +1,15 @@
import { getRequestConfig } from 'next-intl/server'
import { getUserLocale } from './lib/locale'
export const locales = ['en-US', 'fi'] as const
export const locales = [
'en-US',
'fi',
'fr-FR',
'es',
'de-DE',
'zh-CN',
'ru-RU',
] as const
export type Locale = (typeof locales)[number]
export type Locales = ReadonlyArray<Locale>
export const defaultLocale: Locale = 'en-US'

71
src/lib/utils.test.ts Normal file
View File

@@ -0,0 +1,71 @@
import { formatCurrency } from './utils'
describe('formatCurrency', () => {
const currency = 'CUR'
/** For testing decimals */
const partialAmount = 1.23
/** For testing small full amounts */
const smallAmount = 1
/** For testing large full amounts */
const largeAmount = 10000
/** Non-breaking space */
const nbsp = '\xa0'
interface variation {
amount: number
locale: string
result: string
}
/**
* Variations to be tested, chosen as follows
* - `en-US` is a very common i18n fallback
* - `de-DE` exhibited faulty behavior in previous versions
*/
const variations: variation[] = [
{
amount: partialAmount,
locale: `en-US`,
result: `${currency}1.23`,
},
{
amount: smallAmount,
locale: `en-US`,
result: `${currency}1.00`,
},
{
amount: largeAmount,
locale: `en-US`,
result: `${currency}10,000.00`,
},
{
amount: partialAmount,
locale: `de-DE`,
result: `1,23${nbsp}${currency}`,
},
{
amount: smallAmount,
locale: `de-DE`,
result: `1,00${nbsp}${currency}`,
},
{
amount: largeAmount,
locale: `de-DE`,
result: `10.000,00${nbsp}${currency}`,
},
]
for (const variation of variations) {
it(`formats ${variation.amount} in ${variation.locale} without fractions`, () => {
expect(
formatCurrency(currency, variation.amount * 100, variation.locale),
).toBe(variation.result)
})
it(`formats ${variation.amount} in ${variation.locale} with fractions`, () => {
expect(
formatCurrency(currency, variation.amount, variation.locale, true),
).toBe(variation.result)
})
}
})

View File

@@ -28,10 +28,16 @@ export function formatCategoryForAIPrompt(category: Category) {
return `"${category.grouping}/${category.name}" (ID: ${category.id})`
}
/**
* @param fractions Financial values in this app are generally processed in cents (or equivalent).
* They are are therefore integer representations of the amount (e.g. 100 for USD 1.00).
* Set this to `true` if you need to pass a value with decimal fractions instead (e.g. 1.00 for USD 1.00).
*/
export function formatCurrency(
currency: string,
amount: number,
locale: string,
fractions?: boolean,
) {
const format = new Intl.NumberFormat(locale, {
minimumFractionDigits: 2,
@@ -40,7 +46,7 @@ export function formatCurrency(
// '€' will be placed in correct position
currency: 'EUR',
})
const formattedAmount = format.format(amount / 100)
const formattedAmount = format.format(fractions ? amount : amount / 100)
return formattedAmount.replace('€', currency)
}