Second extraction pass: the strings the codemod could not see
`i18n:coverage` reported 100% while a hundred-odd strings rendered
English in every language. It was not wrong about what it measured: it
reads JSX text, and none of these were JSX text. They were toast
arguments, `confirmDialog({ title, confirmLabel })` props, `title=` and
`aria-label=` attributes, and template literals — every one built from an
expression the codemod cannot read.
176 source strings and 15 plural sets now go through t() and plural(),
translated into all nine languages. Where English put a word in a slot,
the sentence is spelled out per branch instead: `Filter ${verb}` became
"Filter saved" and "Filter created", because which word agrees with what,
and where it sits, is not a property English gets to decide for everyone.
Counts that were `${n} message${n === 1 ? "" : "s"}` are plural() calls,
so Russian and Ukrainian get three forms and Japanese and Chinese get the
one they actually have.
Two of the catalogue's own conventions were worth learning the hard way.
Plural entries are keyed on the English *other* form, not `one` — `one`
is a form English happens to have and Japanese does not. And a constant
table holding English that is translated at the render site is fine: the
literal is a key, not a leak.
Which is what the new check encodes. `scripts/i18n-literals.mjs` accepts
a string that is wrapped where it is written or is a catalogue key
somewhere, and refuses one that is neither — a string no catalogue can
translate, however many languages ship. It found twenty more than my own
sweep had, including the stale-folder toast seen in production. It runs
as part of `npm run i18n:check`.
Also fixed: the catalogue is now awaited before the first paint. The
tree is rebuilt when a catalogue lands, so components recover on their
own, but a string computed in an effect does not — a toast fired in that
gap is emitted in English and stays English. The wait costs nothing
visible, since the session bootstrap already shows a spinner and English
resolves immediately.
And the Japanese agenda title loses a space Japanese does not use:
"{date} からの予定" was written with the English habit of spacing around
a placeholder.
This commit is contained in:
@@ -853,6 +853,183 @@ export const catalog: Catalog = {
|
||||
"Saved {when}": "Збережено {when}",
|
||||
"Unsaved": "Не збережено",
|
||||
"Agenda from {date}": "Розклад від {date}",
|
||||
|
||||
// ── Toasts, dialogs and computed labels (second extraction pass) ───
|
||||
"(no subject)": "(без теми)",
|
||||
"(skipped: {actions})": "(пропущено: {actions})",
|
||||
"Activate": "Увімкнути",
|
||||
"Add a contact or import a vCard file.": "Додайте контакт або імпортуйте файл vCard.",
|
||||
"Added to your calendar": "Додано до вашого календаря",
|
||||
"All events in this calendar will be deleted.": "Усі події цього календаря буде видалено.",
|
||||
"All occurrences": "Усі повторення",
|
||||
"An occurrence cannot be moved to another calendar on its own": "Окреме повторення не можна перенести до іншого календаря",
|
||||
"Anything signed in with this password stops working immediately.": "Усе, що ввійшло з цим паролем, одразу перестане працювати.",
|
||||
"App password revoked": "Пароль застосунку відкликано",
|
||||
"Applies to every date in the series.": "Застосовується до всіх дат серії.",
|
||||
"Applies to this date only.": "Застосовується лише до цієї дати.",
|
||||
"Apply to series": "Застосувати до всієї серії",
|
||||
"Applying filter to existing messages…": "Фільтр застосовується до наявних листів…",
|
||||
"Attachments are still uploading": "Вкладення ще завантажуються",
|
||||
"Calendar saved": "Календар збережено",
|
||||
"Categorised as {name}": "Призначено категорію «{name}»",
|
||||
"Category cleared": "Категорію знято",
|
||||
"Change this event?": "Змінити цю подію?",
|
||||
"Choose a calendar": "Виберіть календар",
|
||||
"Choose an address book": "Виберіть адресну книгу",
|
||||
"Colour updated": "Колір змінено",
|
||||
"Contact created": "Контакт створено",
|
||||
"Contact deleted": "Контакт видалено",
|
||||
"Contact saved": "Контакт збережено",
|
||||
"Copied": "Скопійовано",
|
||||
"Copy of {title}": "Копія: {title}",
|
||||
"Could not cancel: {error}": "Не вдалося скасувати: {error}",
|
||||
"Could not copy": "Не вдалося скопіювати",
|
||||
"Could not copy the address": "Не вдалося скопіювати адресу",
|
||||
"Could not empty folder: {error}": "Не вдалося очистити теку: {error}",
|
||||
"Could not load source: {error}": "Не вдалося завантажити вихідний текст: {error}",
|
||||
"Could not mark as read: {error}": "Не вдалося позначити як прочитане: {error}",
|
||||
"Could not save draft: {error}": "Не вдалося зберегти чернетку: {error}",
|
||||
"Could not save filter: {error}": "Не вдалося зберегти фільтр: {error}",
|
||||
"Could not save filters: {error}": "Не вдалося зберегти фільтри: {error}",
|
||||
"Could not send the receipt: {error}": "Не вдалося надіслати сповіщення про прочитання: {error}",
|
||||
"Could not sign in.": "Не вдалося увійти.",
|
||||
"Could not store image: {error}": "Не вдалося зберегти зображення: {error}",
|
||||
"Could not update labels: {error}": "Не вдалося оновити мітки: {error}",
|
||||
"Could not update the Scheduled folder: {error}": "Не вдалося оновити теку «Заплановані»: {error}",
|
||||
"Could not update: {error}": "Не вдалося оновити: {error}",
|
||||
"Create": "Створити",
|
||||
"Create an address book first": "Спершу створіть адресну книгу",
|
||||
"Custom colour removed": "Власний колір знято",
|
||||
"Deactivate": "Вимкнути",
|
||||
"Delete failed: {error}": "Не вдалося видалити: {error}",
|
||||
"Delete forever": "Видалити назавжди",
|
||||
"Delete script “{name}”?": "Видалити сценарій «{name}»?",
|
||||
"Delete this event?": "Видалити цю подію?",
|
||||
"Delete this identity?": "Видалити цей профіль відправника?",
|
||||
"Delete {name}?": "Видалити {name}?",
|
||||
"Deleted": "Видалено",
|
||||
"Deletes the whole series. This cannot be undone.": "Видаляє всю серію. Це не можна буде скасувати.",
|
||||
"Did you forget the attachment?": "Не забули вкладення?",
|
||||
"Discard": "Не зберігати",
|
||||
"Discard this draft?": "Видалити цю чернетку?",
|
||||
"Edit calendar": "Змінити календар",
|
||||
"Edit event": "Змінити подію",
|
||||
"Edit identity": "Змінити профіль відправника",
|
||||
"Edit template": "Змінити шаблон",
|
||||
"End must be after start": "Завершення має бути пізніше за початок",
|
||||
"Event duplicated": "Подію продубльовано",
|
||||
"Everything inside it goes too.": "Усе, що всередині, теж буде видалено.",
|
||||
"Filter created": "Фільтр створено",
|
||||
"Filter created — it will run on new mail": "Фільтр створено — він працюватиме для нових листів",
|
||||
"Filter saved": "Фільтр збережено",
|
||||
"Filter saved — it will run on new mail": "Фільтр збережено — він працюватиме для нових листів",
|
||||
"Filter saved, but applying it failed: {error}": "Фільтр збережено, але застосувати його не вдалося: {error}",
|
||||
"Filters saved": "Фільтри збережено",
|
||||
"Folder changed, but its filter rules could not be updated: {error}": "Теку змінено, але оновити її правила фільтрування не вдалося: {error}",
|
||||
"Folder created": "Теку створено",
|
||||
"Folder deleted": "Теку видалено",
|
||||
"Folder name": "Назва теки",
|
||||
"Folder name (use / for a subfolder, e.g. Work/Invoices)": "Назва теки (/ створює вкладену теку, наприклад Робота/Рахунки)",
|
||||
"Folder name (use / for subfolders, e.g. Work/Invoices)": "Назва теки (/ створює вкладені теки, наприклад Робота/Рахунки)",
|
||||
"Folder “{name}” created": "Теку «{name}» створено",
|
||||
"Forwarded": "Переслано",
|
||||
"Hand to the server, held until {when} (Ctrl+Enter)": "Передати серверу й утримати до {when} (Ctrl+Enter)",
|
||||
"Hide": "Сховати",
|
||||
"Hide quoted text": "Сховати цитований текст",
|
||||
"Hide this from the compose picker": "Не пропонувати під час написання",
|
||||
"Identity saved": "Профіль відправника збережено",
|
||||
"Images in signatures need the Files feature, which this account doesn't have.": "Для зображень у підписі потрібні «Файли», яких цей обліковий запис не має.",
|
||||
"Invalid address: {address}": "Неправильна адреса: {address}",
|
||||
"Invalid username or password.": "Неправильне ім’я користувача або пароль.",
|
||||
"It belongs to a change that was applied to this and all later occurrences, which the server will only edit as a whole. Apply to the entire series instead?": "Вона належить до зміни, застосованої до цього й усіх наступних повторень, а сервер редагує таку зміну лише повністю. Застосувати до всієї серії?",
|
||||
"Label name": "Назва мітки",
|
||||
"Larger than {size} MB limit": "Більше за обмеження в {size} МБ",
|
||||
"Message sent": "Лист надіслано",
|
||||
"Move failed: {error}": "Не вдалося перемістити: {error}",
|
||||
"Move “{name}”": "Перемістити «{name}»",
|
||||
"Moved": "Переміщено",
|
||||
"Network error. Please check your connection.": "Помилка мережі. Перевірте з’єднання.",
|
||||
"New all-day event on {date}": "Нова подія на весь день {date}",
|
||||
"New event at {time}": "Нова подія о {time}",
|
||||
"New identity": "Новий профіль відправника",
|
||||
"No Archive folder found. Create one named “Archive” first.": "Теку архіву не знайдено. Спершу створіть теку з назвою «Archive».",
|
||||
"No contacts yet": "Контактів ще немає",
|
||||
"No longer shared": "Спільний доступ припинено",
|
||||
"No matches": "Збігів немає",
|
||||
"No new mail in your inbox.": "Нових листів у вхідних немає.",
|
||||
"No results": "Нічого не знайдено",
|
||||
"Nothing here": "Тут порожньо",
|
||||
"Only Deleted Items and Junk Mail can be emptied.": "Очистити можна лише кошик і спам.",
|
||||
"Please add at least one recipient": "Додайте принаймні одного одержувача",
|
||||
"Please use an image under 512 KB for signatures.": "Для підпису використайте зображення менше за 512 КБ.",
|
||||
"Preview": "Перегляд",
|
||||
"Read receipt sent": "Сповіщення про прочитання надіслано",
|
||||
"Remove attachments that failed to upload first": "Спершу приберіть вкладення, які не завантажилися",
|
||||
"Removed from calendar": "Видалено з календаря",
|
||||
"Removes this date and leaves the rest of the series.": "Видаляє цю дату й залишає решту серії.",
|
||||
"Rename address book": "Перейменувати адресну книгу",
|
||||
"Rename category": "Перейменувати категорію",
|
||||
"Replied": "Є відповідь",
|
||||
"Report spam (!)": "Поскаржитися на спам (!)",
|
||||
"Response sent": "Відповідь надіслано",
|
||||
"Revoke “{name}”?": "Відкликати «{name}»?",
|
||||
"Script has errors": "У сценарії є помилки",
|
||||
"Script is valid": "Сценарій коректний",
|
||||
"Script name is required": "Потрібно вказати назву сценарію",
|
||||
"Script saved": "Сценарій збережено",
|
||||
"Send (Ctrl+Enter)": "Надіслати (Ctrl+Enter)",
|
||||
"Send anyway": "Усе одно надіслати",
|
||||
"Send cancelled — the message is back in Drafts": "Надсилання скасовано — лист повернувся до чернеток",
|
||||
"Send failed: {error}": "Не вдалося надіслати: {error}",
|
||||
"Send invites": "Надіслати запрошення",
|
||||
"Send scheduled for {when}": "Надсилання заплановано на {when}",
|
||||
"Send without a subject?": "Надіслати без теми?",
|
||||
"Share “{name}”": "Надати доступ до «{name}»",
|
||||
"Sharing updated": "Спільний доступ оновлено",
|
||||
"Show": "Показати",
|
||||
"Show quoted text": "Показати цитований текст",
|
||||
"Show this in the compose picker": "Пропонувати під час написання",
|
||||
"Sign out other sessions?": "Завершити інші сеанси?",
|
||||
"Sign out others": "Завершити інші",
|
||||
"Stop sharing “{name}”?": "Припинити спільний доступ до «{name}»?",
|
||||
"Switch to {theme}": "Перемкнути на {theme}",
|
||||
"Template": "Шаблон",
|
||||
"Template name": "Назва шаблону",
|
||||
"The contacts in it go too.": "Контакти всередині теж буде видалено.",
|
||||
"The default identity is always offered when composing": "Профіль за умовчанням завжди пропонується під час написання",
|
||||
"The new passwords don't match": "Нові паролі не збігаються",
|
||||
"The server scheduled this for {when}, not the time requested.": "Сервер запланував це на {when}, а не на запитаний час.",
|
||||
"This date cannot be changed on its own": "Цю дату не можна змінити окремо",
|
||||
"This occurrence": "Лише це повторення",
|
||||
"Too many attempts. Please wait a few minutes and try again.": "Забагато спроб. Зачекайте кілька хвилин і спробуйте ще раз.",
|
||||
"Try another search.": "Спробуйте інший запит.",
|
||||
"Try different keywords or filters.": "Спробуйте інші слова або фільтри.",
|
||||
"Two-factor authentication is off": "Двофакторну автентифікацію вимкнено",
|
||||
"Undo window: {seconds}s": "Час на скасування: {seconds} с",
|
||||
"You're all caught up": "Усе прочитано",
|
||||
"Your browser refused the request: {error}": "Браузер відхилив запит: {error}",
|
||||
"Your browser will ask whether to open mail links in ihasmail": "Браузер запитає, чи відкривати поштові посилання в ihasmail",
|
||||
"Your message mentions an attachment, but nothing is attached.": "У листі згадано вкладення, але нічого не долучено.",
|
||||
"event": "подія",
|
||||
"Hide password": "Сховати пароль",
|
||||
"Show password": "Показати пароль",
|
||||
"Settings reset to defaults": "Налаштування скинуто до типових",
|
||||
"Removed. Mail links will open in whatever your browser falls back to.": "Вилучено. Поштові посилання відкриватимуться там, куди перемкнеться браузер.",
|
||||
"Edit rule": "Змінити правило",
|
||||
"Address copied": "Адресу скопійовано",
|
||||
"Search or create label": "Знайдіть або створіть мітку",
|
||||
"New label name": "Назва нової мітки",
|
||||
"That folder no longer exists. Showing your inbox instead.": "Цієї теки більше немає. Показано вхідні.",
|
||||
"Labels updated": "Мітки оновлено",
|
||||
"Unsubscribe message prepared — just hit Send": "Лист про відписку готовий — лишилося натиснути «Надіслати»",
|
||||
"Maximize": "Розгорнути",
|
||||
"Full screen": "На весь екран",
|
||||
"Resize panes": "Змінити розмір панелей",
|
||||
"QR code": "QR-код",
|
||||
"Draft discarded": "Чернетку видалено",
|
||||
"Draft saved": "Чернетку збережено",
|
||||
"Emptying folder…": "Тека очищується…",
|
||||
"Nothing unread here": "Тут немає непрочитаного",
|
||||
},
|
||||
plurals: {
|
||||
/*
|
||||
@@ -864,5 +1041,20 @@ export const catalog: Catalog = {
|
||||
"{n} messages": { one: "{n} лист", few: "{n} листи", many: "{n} листів", other: "{n} листа" },
|
||||
"{n} conversations": { one: "{n} листування", few: "{n} листування", many: "{n} листувань", other: "{n} листування" },
|
||||
"{n} selected": { one: "Вибрано: {n}", few: "Вибрано: {n}", many: "Вибрано: {n}", other: "Вибрано: {n}" },
|
||||
"{n} messages will be permanently deleted.": { one: "{n} лист буде видалено назавжди.", few: "{n} листи буде видалено назавжди.", many: "{n} листів буде видалено назавжди.", other: "{n} листа буде видалено назавжди." },
|
||||
"Signed out {n} other sessions": { one: "Завершено {n} інший сеанс", few: "Завершено {n} інші сеанси", many: "Завершено {n} інших сеансів", other: "Завершено {n} інших сеансів" },
|
||||
"applied to {matched} of {n} messages": { one: "застосовано до {matched} з {n} листа", few: "застосовано до {matched} з {n} листів", many: "застосовано до {matched} з {n} листів", other: "застосовано до {matched} з {n} листів" },
|
||||
"Move {n} messages to…": { one: "Перемістити {n} лист до…", few: "Перемістити {n} листи до…", many: "Перемістити {n} листів до…", other: "Перемістити {n} листа до…" },
|
||||
"This permanently deletes the folder and its {n} messages.": { one: "Теку та {n} лист у ній буде видалено назавжди.", few: "Теку та {n} листи в ній буде видалено назавжди.", many: "Теку та {n} листів у ній буде видалено назавжди.", other: "Теку та {n} листа в ній буде видалено назавжди." },
|
||||
"Added {n} contacts": { one: "Додано {n} контакт", few: "Додано {n} контакти", many: "Додано {n} контактів", other: "Додано {n} контакта" },
|
||||
"{n} people will lose access. The contacts in it are not affected.": { one: "{n} особа втратить доступ. Контакти всередині не змінюються.", few: "{n} особи втратять доступ. Контакти всередині не змінюються.", many: "{n} осіб втратять доступ. Контакти всередині не змінюються.", other: "{n} особи втратять доступ. Контакти всередині не змінюються." },
|
||||
"Imported {n} contacts": { one: "Імпортовано {n} контакт", few: "Імпортовано {n} контакти", many: "Імпортовано {n} контактів", other: "Імпортовано {n} контакта" },
|
||||
"Saved for this date. {names} apply to the whole series and were left unchanged.": { one: "Збережено для цієї дати. {names} стосується всієї серії й залишилося без змін.", few: "Збережено для цієї дати. {names} стосуються всієї серії й залишилися без змін.", many: "Збережено для цієї дати. {names} стосуються всієї серії й залишилися без змін.", other: "Збережено для цієї дати. {names} стосуються всієї серії й залишилися без змін." },
|
||||
"{n} people will lose access. Events in it are not affected.": { one: "{n} особа втратить доступ. Події всередині не змінюються.", few: "{n} особи втратять доступ. Події всередині не змінюються.", many: "{n} осіб втратять доступ. Події всередині не змінюються.", other: "{n} особи втратять доступ. Події всередині не змінюються." },
|
||||
"{n} messages could not be deleted": { one: "Не вдалося видалити {n} лист", few: "Не вдалося видалити {n} листи", many: "Не вдалося видалити {n} листів", other: "Не вдалося видалити {n} листа" },
|
||||
"({n} deleted first)": { one: "(перед цим видалено: {n})", few: "(перед цим видалено: {n})", many: "(перед цим видалено: {n})", other: "(перед цим видалено: {n})" },
|
||||
"Marked {n} messages as read": { one: "{n} лист позначено як прочитаний", few: "{n} листи позначено як прочитані", many: "{n} листів позначено як прочитані", other: "{n} листа позначено як прочитані" },
|
||||
"in {n} folders": { one: "у {n} теці", few: "у {n} теках", many: "у {n} теках", other: "у {n} теках" },
|
||||
"Deleted {n} messages": { one: "Видалено {n} лист", few: "Видалено {n} листи", many: "Видалено {n} листів", other: "Видалено {n} листа" },
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user