Translate UI and user-facing messages to Ukrainian (#5)

- All frontend pages, labels, notices and errors; html lang=uk, uk-UA money format
- Brand "Assistant System" in the top bar and page title
- Backend error details returned to the UI (auth, orders, receipts,
  cash registers, Checkbox/CRM/NP errors) and CLI output
- Tests updated for the new messages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 15:27:39 +03:00
co-authored by Claude Opus 5.5
parent 47df3a78dc
commit b8f2fe6b6e
38 changed files with 232 additions and 230 deletions
@@ -96,18 +96,18 @@ function orderDiscountOf(draft: Draft): number {
/** Проверяет черновик и собирает тело PATCH; строка — текст ошибки для кассира. */
function toPayload(draft: Draft): OrderUpdate | string {
if (draft.goods.length === 0) return 'Добавьте хотя бы один товар'
if (draft.goods.length === 0) return 'Додайте хоча б один товар'
const goods = []
for (const good of draft.goods) {
const label = good.name.trim() || 'без названия'
if (!good.name.trim()) return 'У товара не указано наименование'
const label = good.name.trim() || 'без назви'
if (!good.name.trim()) return 'У товару не вказано найменування'
const price = toKopecks(good.price)
const quantity = toThousandths(good.quantity)
const discount = toKopecks(good.discount)
if (price === null) return `Товар «${label}»: некорректная цена`
if (quantity === null || quantity <= 0) return `Товар «${label}»: некорректное количество`
if (discount === null) return `Товар «${label}»: некорректная скидка`
if ((lineAmount(good) ?? 0) < 0) return `Товар «${label}»: скидка больше суммы строки`
if (price === null) return `Товар «${label}»: некоректна ціна`
if (quantity === null || quantity <= 0) return `Товар «${label}»: некоректна кількість`
if (discount === null) return `Товар «${label}»: некоректна знижка`
if ((lineAmount(good) ?? 0) < 0) return `Товар «${label}»: знижка більша за суму рядка`
goods.push({
id: good.id,
sku: good.sku.trim(),
@@ -118,9 +118,9 @@ function toPayload(draft: Draft): OrderUpdate | string {
})
}
const total = toKopecks(draft.total)
if (total === null) return 'Некорректная сумма заказа'
if (total === null) return 'Некоректна сума замовлення'
const sum = goodsSum(draft.goods) ?? 0
if (total > sum) return `Сумма заказа больше суммы товаров (${formatKopecks(sum)} ₴)`
if (total > sum) return `Сума замовлення більша за суму товарів (${formatKopecks(sum)} ₴)`
return {
recipient_name: draft.recipient_name,
@@ -134,7 +134,7 @@ function toPayload(draft: Draft): OrderUpdate | string {
}
const TEXT_FIELDS: { field: TextField; label: string; type?: string }[] = [
{ field: 'recipient_name', label: 'Клиент' },
{ field: 'recipient_name', label: 'Клієнт' },
{ field: 'recipient_phone', label: 'Телефон', type: 'tel' },
{ field: 'recipient_email', label: 'Email', type: 'email' },
{ field: 'waybill_number', label: 'Номер ТТН' },
@@ -153,7 +153,7 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
const editable = canEdit && !order.has_receipt
function requestClose() {
if (dirty && !window.confirm('Есть несохранённые изменения. Закрыть без сохранения?')) return
if (dirty && !window.confirm('Є незбережені зміни. Закрити без збереження?')) return
onClose()
}
@@ -203,10 +203,10 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
setDraft(next)
setSaved(next)
setOrderDiscount(orderDiscountOf(next))
setMessage({ tone: 'ok', text: 'Изменения сохранены' })
setMessage({ tone: 'ok', text: 'Зміни збережено' })
await queryClient.invalidateQueries({ queryKey: ['orders'] })
} catch (err) {
setMessage({ tone: 'error', text: err instanceof Error ? err.message : 'Не удалось сохранить заказ' })
setMessage({ tone: 'error', text: err instanceof Error ? err.message : 'Не вдалося зберегти замовлення' })
} finally {
setSaving(false)
}
@@ -217,10 +217,10 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
<div className="order-modal">
<div className="order-modal-header">
<h3>
Заказ {order.id}
{order.edited && <span className="order-modal-edited">изменён вручную</span>}
Замовлення {order.id}
{order.edited && <span className="order-modal-edited">змінено вручну</span>}
</h3>
<button type="button" className="order-modal-close" onClick={requestClose} aria-label="Закрыть">
<button type="button" className="order-modal-close" onClick={requestClose} aria-label="Закрити">
×
</button>
</div>
@@ -232,19 +232,19 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
</div>
<div>
<span className="order-modal-label">Статус ТТН</span>
<span>{order.np_status || 'Нет данных'}</span>
<span>{order.np_status || 'Немає даних'}</span>
</div>
<div>
<span className="order-modal-label">Наложенный платёж</span>
<span className="order-modal-label">Накладений платіж</span>
<span>{order.np_cod_amount ? `${order.np_cod_amount} ₴` : '—'}</span>
</div>
<div>
<span className="order-modal-label">Оплачено</span>
<span>
{order.np_payment_status === 'Payed'
? 'Да'
? 'Так'
: order.np_payment_status === 'NeedPayment'
? 'Нет'
? 'Ні'
: '—'}
</span>
</div>
@@ -272,7 +272,7 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
{editable ? (
<label className="order-modal-notes">
<span className="order-modal-label">Заметки</span>
<span className="order-modal-label">Нотатки</span>
<textarea
className="order-modal-input"
rows={2}
@@ -283,7 +283,7 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
) : (
draft.notes && (
<div className="order-modal-notes">
<span className="order-modal-label">Заметки</span>
<span className="order-modal-label">Нотатки</span>
<p>{draft.notes}</p>
</div>
)
@@ -292,13 +292,13 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
<table className="order-modal-goods">
<thead>
<tr>
<th>Наименование</th>
<th>Найменування</th>
<th>SKU</th>
<th>Цена</th>
<th>Кол-во</th>
<th>Скидка</th>
<th>Сумма</th>
{editable && <th aria-label="Удалить" />}
<th>Ціна</th>
<th>К-сть</th>
<th>Знижка</th>
<th>Сума</th>
{editable && <th aria-label="Видалити" />}
</tr>
</thead>
<tbody>
@@ -350,7 +350,7 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
<button
type="button"
className="order-modal-remove"
aria-label={`Удалить товар ${good.name}`}
aria-label={`Видалити товар ${good.name}`}
onClick={() => setGoods((goods) => goods.filter((g) => g.key !== good.key))}
>
×
@@ -363,7 +363,7 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
{draft.goods.length === 0 && (
<tr>
<td colSpan={editable ? 7 : 6} className="order-modal-goods-empty">
Товары не указаны
Товари не вказані
</td>
</tr>
)}
@@ -372,19 +372,19 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
{editable && (
<button type="button" className="order-modal-add" onClick={addGood}>
+ Добавить товар
+ Додати товар
</button>
)}
<div className="order-modal-total">
<span>Итого</span>
<span>Разом</span>
{editable ? (
<span>
<input
className="order-modal-input order-modal-input--num"
inputMode="decimal"
value={draft.total}
title="Сумма заказа после всех скидок"
title="Сума замовлення після всіх знижок"
onChange={(e) => setTotal(e.target.value)}
/>{' '}
₴
@@ -403,12 +403,12 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
</span>
</div>
<div>
<span className="order-modal-label">Предоплата в чеке</span>
<span className="order-modal-label">Передоплата в чеку</span>
<span>{order.receipt_prepayment ? `${order.receipt_prepayment} ₴` : '—'}</span>
</div>
{order.receipt_error && (
<div>
<span className="order-modal-label">Ошибка</span>
<span className="order-modal-label">Помилка</span>
<span>{order.receipt_error}</span>
</div>
)}
@@ -425,11 +425,11 @@ export function OrderDetailModal({ order, canEdit, onClose }: OrderDetailModalPr
disabled={!dirty || saving}
onClick={() => void handleSave()}
>
{saving ? 'Сохранение…' : 'Сохранить'}
{saving ? 'Збереження…' : 'Зберегти'}
</button>
)}
<button type="button" className="order-modal-close-btn" onClick={requestClose}>
Закрыть
Закрити
</button>
</div>
</div>