From 54b316ae36645efd9a1671be1c0be3285b7b5b5d Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 10 Sep 2026 16:00:00 -0700 Subject: [PATCH] Report the stale keys that are stale, and remove them The check reported 41 stale keys per catalogue. Ten of them were. The other 31 were strings held in constants and translated where they render -- t(b.description), t(group), t(c.label) -- so they reach t() as a variable and there is no literal at the call site to find. The script already chased two of those shapes, `label:` and objects named *_LABELS, with a comment about crying wolf 33 times. The shapes kept coming: `description:` and `group:` on the keyboard bindings, the calendar's view names, the read-receipt refusals, the palette names. Chasing them one at a time is the wrong shape of fix. Stale detection now asks only "is this key still written down anywhere in the source" -- any string literal counts. That under-reports, and that is the right way round: a missed stale key costs a line of dead translation, a false one costs the credibility of the check and every real finding after it. Which is what happened here -- these sat unread long enough to need a commit of their own. Coverage keeps the strict set. The two questions need different nets, and widening the one that measures what a catalogue *owes* would count every CSS class and JMAP method name as an untranslated string -- it read 29% while I had them sharing a set. `wanted` is the obligation, `seen` is the evidence. What was actually dead, removed from all nine: "Availability on {date}", "Import vCard", "PDF", two settings hints replaced by rewordings that are still live, the Catppuccin palette description, and Tuesday through Friday -- left behind when the week-start dropdown narrowed to the three days a week actually starts on, and appearing since only in comments. Coverage is unchanged at 1269/1285: none of the ten was ever owed. --- scripts/i18n-catalog-check.mjs | 50 ++++++++++++++++++++++++++++------ web/src/locales/de.ts | 10 ------- web/src/locales/es.ts | 10 ------- web/src/locales/fr.ts | 10 ------- web/src/locales/ja.ts | 10 ------- web/src/locales/nl.ts | 10 ------- web/src/locales/pt-BR.ts | 10 ------- web/src/locales/ru.ts | 10 ------- web/src/locales/uk.ts | 10 ------- web/src/locales/zh-Hans.ts | 10 ------- 10 files changed, 41 insertions(+), 99 deletions(-) diff --git a/scripts/i18n-catalog-check.mjs b/scripts/i18n-catalog-check.mjs index 7281af1..9f5cfdc 100644 --- a/scripts/i18n-catalog-check.mjs +++ b/scripts/i18n-catalog-check.mjs @@ -29,17 +29,49 @@ import ts from "typescript-ast"; import { readFileSync, globSync } from "node:fs"; +/* + * Two sets, because there are two questions and they need different nets. + * + * `wanted` is what a catalogue *owes*: the strings that actually reach t(), + * tc() or plural(). Coverage is measured against it, so it has to stay strict + * -- widening it would count every CSS class and JMAP method name as an + * untranslated string. + * + * `seen` is every string literal in the source, and answers only "is this + * catalogue key still written down anywhere". Stale detection needs the wide + * net: a key reaches t() as a variable often enough that a strict set reports + * mostly false alarms. + */ const wanted = new Set(); +const seen = new Set(); for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("__tests__") && !f.includes("/locales/"))) { const src = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); const visit = (n) => { /* - * Labels held in a constant and translated where they render -- t(s.label) - * -- reach t() as a variable, so there is no literal for this to find and - * every one of them looked "stale". They are collected from the constants - * instead: a `label:` property, or a value in an object of them. Without - * this the stale check cried wolf 33 times and would have been switched - * off, which is the only outcome worse than not having it. + * Anything held in a constant and translated where it renders -- t(s.label), + * t(b.description), t(group) -- reaches t() as a variable, so there is no + * literal at the call site and every one of them looked "stale". + * + * This used to chase the shapes one at a time: a `label:` property, then an + * object named *_LABELS. It still cried wolf, because the shapes kept + * coming -- `description:` and `group:` on keyboard bindings, the calendar's + * view names, the read-receipt refusals, the palette names. 41 reported, + * 10 of them real. A report that is three-quarters false is one nobody acts + * on, which is how these sat unread long enough to be worth a commit of + * their own. + * + * So: any string literal anywhere in the source counts as a use. That + * under-reports -- a literal that exists but is never passed to t() will not + * be flagged -- and that is the right way round. A missed stale key costs a + * line of dead translation; a false one costs the credibility of the whole + * check, and then every real finding with it. + */ + if (ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n)) seen.add(n.text); + if (ts.isJsxText(n)) { const text = n.text.trim(); if (text) seen.add(text); } + /* + * A `label:` in a constant is still a string somebody has to translate -- + * it reaches t() one render later -- so it stays part of what a catalogue + * owes, and out of coverage it would flatter the number. */ if (ts.isPropertyAssignment(n) && n.name.getText(src) === "label" && ts.isStringLiteral(n.initializer)) wanted.add(n.initializer.text); if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && /_LABELS?$/.test(n.name.text)) { @@ -58,6 +90,7 @@ for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes(" // fallback, not a second obligation -- asking for both would report // work that does not exist. wanted.add(`${a0.text}\u0004${n.arguments[1].text}`); + seen.add(`${a0.text}\u0004${n.arguments[1].text}`); } if (fn === "plural" && n.arguments[1] && ts.isObjectLiteralExpression(n.arguments[1])) { for (const p of n.arguments[1].properties) { @@ -105,15 +138,14 @@ for (const file of globSync("web/src/locales/*.ts")) { ts.forEachChild(n, visit); }; visit(src); - const stale = [...have].filter((k) => !wanted.has(k) && !["one", "other", "few", "many", "zero", "two"].includes(k)); + const stale = [...have].filter((k) => !seen.has(k) && !["one", "other", "few", "many", "zero", "two"].includes(k)); const missing = [...wanted].filter((k) => !have.has(k)); const pct = Math.round(((wanted.size - missing.length) / wanted.size) * 100); console.log(`${tag}: ${wanted.size - missing.length}/${wanted.size} translated (${pct}%), ${missing.length} falling back to English`); if (stale.length) { failed = true; console.log(`\n ${stale.length} STALE key(s) — translated but never looked up, so they do nothing:`); - for (const k of stale.slice(0, 25)) console.log(` ${JSON.stringify(k)}`); - if (stale.length > 25) console.log(` …and ${stale.length - 25} more`); + for (const k of stale) console.log(` ${JSON.stringify(k)}`); } if (process.argv.includes("--missing")) { console.log(`\n missing:`); diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index 61ca2c2..5f88716 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -327,7 +327,6 @@ export const catalog: Catalog = { "Busy": "Gebucht", "Free/busy": "Frei/Gebucht", "Show as": "Anzeigen als", - "Availability on {date}": "Verfügbarkeit am {date}", "Count all events as busy": "Alle Termine als gebucht zählen", "Only events I'm attending": "Nur Termine, an denen ich teilnehme", "Don't include in availability": "Nicht in die Verfügbarkeit einbeziehen", @@ -366,7 +365,6 @@ export const catalog: Catalog = { "New address book": "Neues Adressbuch", "No address books yet.": "Noch keine Adressbücher.", "Choose from address books": "Aus Adressbüchern wählen", - "Import vCard": "vCard importieren", "Export all contacts": "Alle Kontakte exportieren", "Export address book": "Dieses Adressbuch exportieren", "Import contacts…": "Kontakte importieren…", @@ -437,7 +435,6 @@ export const catalog: Catalog = { "Make ihasmail yours.": "Machen Sie ihasmail zu Ihrem.", "Reading": "Lesen", "Reading pane": "Lesebereich", - "Reading, sending and list behaviour. Settings are stored in this browser.": "Verhalten beim Lesen, Senden und in der Liste. Die Einstellungen werden in diesem Browser gespeichert.", "Right of the list": "Rechts von der Liste", "Below the list": "Unter der Liste", "Hidden (open full width)": "Ausgeblendet (in voller Breite öffnen)", @@ -714,8 +711,6 @@ export const catalog: Catalog = { "Manage labels": "Labels verwalten", "Create “{name}”": "„{name}“ erstellen", "Type a name to create your first label.": "Geben Sie einen Namen ein, um Ihr erstes Label zu erstellen.", - "Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "Labels sind IMAP-Schlüsselwörter, die in Ihren Nachrichten gespeichert werden und daher mit anderen Clients synchronisiert werden. Namen und Farben bleiben in diesem Browser.", - "PDF": "PDF", "Large attachments may be rejected by some servers": "Große Anhänge werden von manchen Servern abgelehnt", "Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "Bilder werden in Ihren Dateien (Ordner „ihasmail“) gespeichert und beim Senden eingebettet.", "Thanks for your message. I'm away until … and will reply when I'm back.": "Vielen Dank für Ihre Nachricht. Ich bin bis … abwesend und melde mich nach meiner Rückkehr.", @@ -839,16 +834,11 @@ export const catalog: Catalog = { "Drop here for the top level": "Hierher ziehen für die oberste Ebene", // ── Remaining prose ──────────────────────────────────────────────── - "{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} ist die Farbpalette von {site} und das, womit ein neues Konto startet. Es ist ein dunkles Design und zählt daher überall dort als dunkel, wo das eine Rolle spielt; die Akzentfarbe unten wirkt weiterhin darauf.", "ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "Die Version von ihasmail ist das Datum des Commits, aus dem es gebaut wurde, gefolgt davon, woher dieser Commit stammt: {example} wurde aus einem Commit vom 30. August 2026 gebaut, der über Pull Request 129 kam. Ein Commit, der nicht über einen solchen kam, trägt stattdessen seinen kurzen SHA — {sha}. Die Version sagt bewusst nichts über Stalwart aus; was dieser Build vom Server benötigt, steht in der Zeile darüber.", // ── Weekdays, schedule presets, rule operators ───────────────────── // Header names (List-Id, X-Spam-Status) stay English: they are the actual // field names in the message, not words. - "Tuesday": "Dienstag", - "Wednesday": "Mittwoch", - "Thursday": "Donnerstag", - "Friday": "Freitag", "Later today": "Später heute", "Tomorrow morning": "Morgen früh", "Tomorrow afternoon": "Morgen Nachmittag", diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts index df1f181..16819e6 100644 --- a/web/src/locales/es.ts +++ b/web/src/locales/es.ts @@ -319,7 +319,6 @@ export const catalog: Catalog = { "Busy": "Ocupado", "Free/busy": "Disponibilidad", "Show as": "Mostrar como", - "Availability on {date}": "Disponibilidad el {date}", "Count all events as busy": "Contar todos los eventos como ocupado", "Only events I'm attending": "Solo los eventos a los que asisto", "Don't include in availability": "No incluir en la disponibilidad", @@ -358,7 +357,6 @@ export const catalog: Catalog = { "New address book": "Libreta de direcciones nueva", "No address books yet.": "Aún no hay libretas de direcciones.", "Choose from address books": "Elegir de las libretas de direcciones", - "Import vCard": "Importar una vCard", "Export all contacts": "Exportar todos los contactos", "Export address book": "Exportar esta libreta de direcciones", "Import contacts…": "Importar contactos…", @@ -432,7 +430,6 @@ export const catalog: Catalog = { "Make ihasmail yours.": "Haga suyo ihasmail.", "Reading": "Lectura", "Reading pane": "Panel de lectura", - "Reading, sending and list behaviour. Settings are stored in this browser.": "Comportamiento de lectura, envío y lista. La configuración se guarda en este navegador.", "Right of the list": "A la derecha de la lista", "Below the list": "Debajo de la lista", "Hidden (open full width)": "Oculto (abrir a todo el ancho)", @@ -475,10 +472,6 @@ export const catalog: Catalog = { "Time zone": "Zona horaria", "Week starts on": "La semana empieza el", "Monday": "Lunes", - "Tuesday": "Martes", - "Wednesday": "Miércoles", - "Thursday": "Jueves", - "Friday": "Viernes", "Saturday": "Sábado", "Sunday": "Domingo", "12-hour clock (6:23 PM)": "Formato de 12 horas (6:23 PM)", @@ -725,10 +718,8 @@ export const catalog: Catalog = { "Manage labels": "Gestionar las etiquetas", "Create “{name}”": "Crear «{name}»", "Type a name to create your first label.": "Escriba un nombre para crear su primera etiqueta.", - "Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "Las etiquetas son palabras clave IMAP guardadas en sus mensajes, así que se sincronizan con otros clientes. Los nombres y colores se guardan en este navegador.", "New label": "Etiqueta nueva", "Delete label": "Eliminar la etiqueta", - "PDF": "PDF", "Large attachments may be rejected by some servers": "Algunos servidores rechazan los adjuntos grandes", "Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "Las imágenes se guardan en sus Archivos (carpeta «ihasmail») y se incrustan al enviar.", "Thanks for your message. I'm away until … and will reply when I'm back.": "Gracias por su mensaje. Estaré ausente hasta el … y le responderé a mi regreso.", @@ -862,7 +853,6 @@ export const catalog: Catalog = { "Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them — a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Aquí solo aparecen los idiomas a los que se ha traducido ihasmail, así que la lista crece a medida que llegan las traducciones y no antes: un idioma ofrecido sin textos detrás haría que la página afirmara estar en un idioma que no es el suyo.", "tell us about it": "cuéntenoslo", "This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Esta traducción la ha generado una IA y no la ha revisado ninguna persona de habla nativa, así que está marcada como Beta hasta que alguien la dé por buena. Todo lo que suene mal merece un aviso: {report}.", - "{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} es la paleta de {site}, y con la que empieza una cuenta nueva. Es un tema oscuro, así que cuenta como oscuro allí donde importa, y el color de acento de abajo se sigue aplicando encima.", "ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "La versión de ihasmail es la fecha del commit a partir del cual se compiló, seguida de su procedencia: {example} se compiló a partir de un commit del 30 de agosto de 2026 que llegó mediante la pull request 129. Un commit que no llegó por esa vía lleva en su lugar su SHA corto: {sha}. La versión no dice nada sobre Stalwart a propósito; lo que esta compilación necesita del servidor está en la línea de arriba.", // ── Composer status, calendar title ──────────────────────────────── diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts index 48d523c..b8911c5 100644 --- a/web/src/locales/fr.ts +++ b/web/src/locales/fr.ts @@ -324,7 +324,6 @@ export const catalog: Catalog = { "Busy": "Occupé", "Free/busy": "Disponibilité", "Show as": "Afficher comme", - "Availability on {date}": "Disponibilité le {date}", "Count all events as busy": "Compter tous les événements comme occupé", "Only events I'm attending": "Uniquement les événements auxquels je participe", "Don't include in availability": "Ne pas inclure dans la disponibilité", @@ -363,7 +362,6 @@ export const catalog: Catalog = { "New address book": "Nouveau carnet d'adresses", "No address books yet.": "Aucun carnet d'adresses pour le moment.", "Choose from address books": "Choisir dans les carnets d'adresses", - "Import vCard": "Importer une vCard", "Export all contacts": "Exporter tous les contacts", "Export address book": "Exporter ce carnet d’adresses", "Import contacts…": "Importer des contacts…", @@ -438,7 +436,6 @@ export const catalog: Catalog = { "Make ihasmail yours.": "Faites de ihasmail le vôtre.", "Reading": "Lecture", "Reading pane": "Volet de lecture", - "Reading, sending and list behaviour. Settings are stored in this browser.": "Comportement de lecture, d'envoi et de liste. Les paramètres sont enregistrés dans ce navigateur.", "Right of the list": "À droite de la liste", "Below the list": "Sous la liste", "Hidden (open full width)": "Masqué (ouvrir en pleine largeur)", @@ -481,10 +478,6 @@ export const catalog: Catalog = { "Time zone": "Fuseau horaire", "Week starts on": "La semaine commence le", "Monday": "Lundi", - "Tuesday": "Mardi", - "Wednesday": "Mercredi", - "Thursday": "Jeudi", - "Friday": "Vendredi", "Saturday": "Samedi", "Sunday": "Dimanche", "12-hour clock (6:23 PM)": "Format 12 heures (6:23 PM)", @@ -730,10 +723,8 @@ export const catalog: Catalog = { "Manage labels": "Gérer les libellés", "Create “{name}”": "Créer « {name} »", "Type a name to create your first label.": "Saisissez un nom pour créer votre premier libellé.", - "Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "Les libellés sont des mots-clés IMAP enregistrés dans vos messages : ils se synchronisent donc avec les autres clients. Les noms et couleurs restent dans ce navigateur.", "New label": "Nouveau libellé", "Delete label": "Supprimer le libellé", - "PDF": "PDF", "Large attachments may be rejected by some servers": "Les pièces jointes volumineuses peuvent être refusées par certains serveurs", "Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "Les images sont enregistrées dans vos Fichiers (dossier « ihasmail ») et intégrées à l'envoi.", "Thanks for your message. I'm away until … and will reply when I'm back.": "Merci pour votre message. Je suis absent jusqu'au … et vous répondrai à mon retour.", @@ -867,7 +858,6 @@ export const catalog: Catalog = { "Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them — a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Seules les langues dans lesquelles ihasmail a été traduit apparaissent ici : la liste s'allonge donc à mesure que les traductions arrivent, et non avant — une langue proposée sans textes derrière elle ferait prétendre à la page qu'elle est dans une langue qui n'est pas la sienne.", "tell us about it": "signalez-le-nous", "This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Cette traduction a été générée par une IA et n'a pas été relue par une personne de langue maternelle française ; elle est donc marquée Beta jusqu'à validation. Tout ce qui sonne faux mérite d'être signalé — {report}.", - "{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} est la palette de {site}, et celle d'un nouveau compte. C'est un thème sombre : il compte donc comme sombre partout où cela importe, et la couleur d'accentuation ci-dessous s'y applique toujours.", "ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "La version de ihasmail est la date du commit à partir duquel elle a été construite, suivie de l'origine de ce commit : {example} provient d'un commit daté du 30 août 2026 arrivé via la pull request 129. Un commit qui n'est pas passé par là porte à la place son SHA court — {sha}. La version ne dit délibérément rien de Stalwart ; ce dont cette build a besoin du serveur figure à la ligne ci-dessus.", // ── Composer status, calendar title ──────────────────────────────── diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index 6aed729..8573299 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -318,7 +318,6 @@ export const catalog: Catalog = { "Busy": "予定あり", "Free/busy": "空き時間", "Show as": "表示方法", - "Availability on {date}": "{date} の空き状況", "Count all events as busy": "すべての予定を「予定あり」とする", "Only events I'm attending": "参加する予定のみ", "Don't include in availability": "空き状況に含めない", @@ -357,7 +356,6 @@ export const catalog: Catalog = { "New address book": "新しいアドレス帳", "No address books yet.": "アドレス帳がまだありません。", "Choose from address books": "アドレス帳から選択", - "Import vCard": "vCard をインポート", "Export all contacts": "すべての連絡先をエクスポート", "Export address book": "このアドレス帳をエクスポート", "Import contacts…": "連絡先をインポート…", @@ -432,7 +430,6 @@ export const catalog: Catalog = { "Make ihasmail yours.": "ihasmail を自分好みに整えましょう。", "Reading": "閲覧", "Reading pane": "プレビューウィンドウ", - "Reading, sending and list behaviour. Settings are stored in this browser.": "閲覧・送信・一覧の動作。設定はこのブラウザーに保存されます。", "Right of the list": "一覧の右", "Below the list": "一覧の下", "Hidden (open full width)": "表示しない(全幅で開く)", @@ -475,10 +472,6 @@ export const catalog: Catalog = { "Time zone": "タイムゾーン", "Week starts on": "週の始まり", "Monday": "月曜日", - "Tuesday": "火曜日", - "Wednesday": "水曜日", - "Thursday": "木曜日", - "Friday": "金曜日", "Saturday": "土曜日", "Sunday": "日曜日", "12-hour clock (6:23 PM)": "12 時間制 (6:23 PM)", @@ -729,12 +722,10 @@ export const catalog: Catalog = { "Manage labels": "ラベルを管理", "Create “{name}”": "「{name}」を作成", "Type a name to create your first label.": "名前を入力すると、最初のラベルを作成できます。", - "Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "ラベルはメールに保存される IMAP キーワードなので、他のクライアントにも同期されます。名前と色はこのブラウザーに保存されます。", "New label": "新しいラベル", "Delete label": "ラベルを削除", // ── Attachments, dates, search prose ─────────────────────────────── - "PDF": "PDF", "Large attachments may be rejected by some servers": "大きな添付ファイルは、サーバーによっては拒否されることがあります", "Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "画像は「ファイル」内(フォルダー「ihasmail」)に保存され、送信時にメールへ埋め込まれます。", "After": "以降", @@ -809,7 +800,6 @@ export const catalog: Catalog = { "Copy it into {name} now — it isn't shown again.": "いま {name} にコピーしてください。二度と表示されません。", "No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "ディレクトリに他のユーザーが見つからないため、新しく追加することはできません。すでに設定されている共有は下に表示され、解除はできます。", "Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart はメールクライアントにバージョン番号を公開しないため、ihasmail はサーバーが示すエディションだけを表示します。ihasmail には 0.16 以降が必要で、それより古いサーバーへのサインインは拒否されます。", - "{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} は {site} の配色で、新しいアカウントの初期テーマです。ダークテーマなので、明暗が問われる場面ではダークとして扱われます。下のアクセントカラーはその上に重ねて適用されます。", "ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "ihasmail 自身のバージョンは、ビルド元となったコミットの日付と、そのコミットの出どころを並べたものです。{example} は 2026 年 8 月 30 日付のコミットから作られ、そのコミットはプルリクエスト 129 を通って届きました。プルリクエストを経ていないコミットは、代わりに短い SHA が付きます — {sha}。バージョンには Stalwart に関する情報をあえて含めていません。このビルドがサーバーに求めるものは、上の行に示されています。", // ── Constant labels ──────────────────────────────────────────────── diff --git a/web/src/locales/nl.ts b/web/src/locales/nl.ts index ea75229..6f0b108 100644 --- a/web/src/locales/nl.ts +++ b/web/src/locales/nl.ts @@ -315,7 +315,6 @@ export const catalog: Catalog = { "Busy": "Bezet", "Free/busy": "Vrij/bezet", "Show as": "Weergeven als", - "Availability on {date}": "Beschikbaarheid op {date}", "Count all events as busy": "Alle afspraken als bezet tellen", "Only events I'm attending": "Alleen afspraken waaraan ik deelneem", "Don't include in availability": "Niet meetellen voor beschikbaarheid", @@ -354,7 +353,6 @@ export const catalog: Catalog = { "New address book": "Nieuw adresboek", "No address books yet.": "Nog geen adresboeken.", "Choose from address books": "Kiezen uit adresboeken", - "Import vCard": "vCard importeren", "Export all contacts": "Alle contacten exporteren", "Export address book": "Dit adresboek exporteren", "Import contacts…": "Contacten importeren…", @@ -429,7 +427,6 @@ export const catalog: Catalog = { "Make ihasmail yours.": "Maak ihasmail van uzelf.", "Reading": "Lezen", "Reading pane": "Leesvenster", - "Reading, sending and list behaviour. Settings are stored in this browser.": "Gedrag bij lezen, verzenden en in de lijst. De instellingen worden in deze browser bewaard.", "Right of the list": "Rechts van de lijst", "Below the list": "Onder de lijst", "Hidden (open full width)": "Verborgen (op volle breedte openen)", @@ -472,10 +469,6 @@ export const catalog: Catalog = { "Time zone": "Tijdzone", "Week starts on": "Week begint op", "Monday": "Maandag", - "Tuesday": "Dinsdag", - "Wednesday": "Woensdag", - "Thursday": "Donderdag", - "Friday": "Vrijdag", "Saturday": "Zaterdag", "Sunday": "Zondag", "12-hour clock (6:23 PM)": "12-uursnotatie (6:23 PM)", @@ -721,10 +714,8 @@ export const catalog: Catalog = { "Manage labels": "Labels beheren", "Create “{name}”": "“{name}” maken", "Type a name to create your first label.": "Typ een naam om uw eerste label te maken.", - "Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "Labels zijn IMAP-trefwoorden die in uw berichten worden opgeslagen en dus met andere clients synchroniseren. Namen en kleuren blijven in deze browser.", "New label": "Nieuw label", "Delete label": "Label verwijderen", - "PDF": "PDF", "Large attachments may be rejected by some servers": "Grote bijlagen worden door sommige servers geweigerd", "Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "Afbeeldingen worden opgeslagen in uw Bestanden (map “ihasmail”) en bij verzending ingesloten.", "Thanks for your message. I'm away until … and will reply when I'm back.": "Bedankt voor uw bericht. Ik ben afwezig tot … en reageer zodra ik terug ben.", @@ -858,7 +849,6 @@ export const catalog: Catalog = { "Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them — a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Hier verschijnen alleen talen waarin ihasmail is vertaald; de lijst groeit dus mee met de vertalingen en niet erop vooruit — een taal die wordt aangeboden zonder teksten erachter zou de pagina laten beweren dat ze in een taal is die ze niet is.", "tell us about it": "laat het ons weten", "This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Deze vertaling is door AI gemaakt en niet gecontroleerd door iemand met Nederlands als moedertaal; ze is daarom als Beta gemarkeerd tot iemand haar goedkeurt. Alles wat verkeerd klinkt, is een melding waard — {report}.", - "{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} is het kleurenpalet van {site}, en waarmee een nieuw account begint. Het is een donker thema en telt dus overal als donker waar dat uitmaakt; de accentkleur hieronder werkt er nog steeds bovenop.", "ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "De eigen versie van ihasmail is de datum van de commit waaruit het is gebouwd, gevolgd door waar die commit vandaan kwam: {example} is gebouwd uit een commit van 30 augustus 2026 die via pull request 129 binnenkwam. Een commit die niet via zo'n verzoek kwam, draagt in plaats daarvan zijn korte SHA — {sha}. De versie zegt bewust niets over Stalwart; wat deze build van de server nodig heeft, staat op de regel hierboven.", // ── Composer status, calendar title ──────────────────────────────── diff --git a/web/src/locales/pt-BR.ts b/web/src/locales/pt-BR.ts index 4e0bb65..c743517 100644 --- a/web/src/locales/pt-BR.ts +++ b/web/src/locales/pt-BR.ts @@ -322,7 +322,6 @@ export const catalog: Catalog = { "Busy": "Ocupado", "Free/busy": "Disponibilidade", "Show as": "Mostrar como", - "Availability on {date}": "Disponibilidade em {date}", "Count all events as busy": "Contar todos os eventos como ocupado", "Only events I'm attending": "Somente os eventos de que participo", "Don't include in availability": "Não incluir na disponibilidade", @@ -361,7 +360,6 @@ export const catalog: Catalog = { "New address book": "Novo catálogo de endereços", "No address books yet.": "Ainda não há catálogos de endereços.", "Choose from address books": "Escolher nos catálogos de endereços", - "Import vCard": "Importar um vCard", "Export all contacts": "Exportar todos os contatos", "Export address book": "Exportar este catálogo de endereços", "Import contacts…": "Importar contatos…", @@ -435,7 +433,6 @@ export const catalog: Catalog = { "Make ihasmail yours.": "Deixe o ihasmail do seu jeito.", "Reading": "Leitura", "Reading pane": "Painel de leitura", - "Reading, sending and list behaviour. Settings are stored in this browser.": "Comportamento de leitura, envio e lista. As configurações ficam guardadas neste navegador.", "Right of the list": "À direita da lista", "Below the list": "Abaixo da lista", "Hidden (open full width)": "Oculto (abrir em largura total)", @@ -478,10 +475,6 @@ export const catalog: Catalog = { "Time zone": "Fuso horário", "Week starts on": "A semana começa em", "Monday": "Segunda-feira", - "Tuesday": "Terça-feira", - "Wednesday": "Quarta-feira", - "Thursday": "Quinta-feira", - "Friday": "Sexta-feira", "Saturday": "Sábado", "Sunday": "Domingo", "12-hour clock (6:23 PM)": "Formato de 12 horas (6:23 PM)", @@ -728,10 +721,8 @@ export const catalog: Catalog = { "Manage labels": "Gerenciar os marcadores", "Create “{name}”": "Criar “{name}”", "Type a name to create your first label.": "Digite um nome para criar seu primeiro marcador.", - "Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "Os marcadores são palavras-chave IMAP guardadas nas suas mensagens, então eles sincronizam com outros clientes. Os nomes e as cores ficam neste navegador.", "New label": "Novo marcador", "Delete label": "Excluir o marcador", - "PDF": "PDF", "Large attachments may be rejected by some servers": "Anexos grandes podem ser recusados por alguns servidores", "Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "As imagens são guardadas nos seus Arquivos (pasta “ihasmail”) e incorporadas no envio.", "Thanks for your message. I'm away until … and will reply when I'm back.": "Obrigado pela sua mensagem. Estarei ausente até … e responderei quando voltar.", @@ -865,7 +856,6 @@ export const catalog: Catalog = { "Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them — a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Aqui aparecem só os idiomas para os quais o ihasmail foi traduzido, então a lista cresce conforme as traduções chegam, e não antes — um idioma oferecido sem textos por trás faria a página afirmar estar em um idioma que não é o dela.", "tell us about it": "conte para nós", "This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Esta tradução foi gerada por IA e não foi revisada por uma pessoa nativa, então está marcada como Beta até que alguém a aprove. Tudo o que soar errado vale um aviso — {report}.", - "{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} é a paleta de {site}, e com a qual uma conta nova começa. É um tema escuro, então conta como escuro onde isso importa, e a cor de destaque abaixo continua valendo por cima dele.", "ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "A versão do próprio ihasmail é a data do commit a partir do qual ele foi compilado, seguida da origem desse commit: {example} foi compilado a partir de um commit de 30 de agosto de 2026 que veio pela pull request 129. Um commit que não veio por uma delas carrega no lugar o SHA curto — {sha}. A versão não diz nada sobre o Stalwart de propósito; o que esta compilação precisa do servidor está na linha acima.", // ── Composer status, calendar title ──────────────────────────────── diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index ae619b4..a18e80e 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -321,7 +321,6 @@ export const catalog: Catalog = { "Busy": "Занят", "Free/busy": "Занятость", "Show as": "Показывать как", - "Availability on {date}": "Занятость на {date}", "Count all events as busy": "Считать все события занятостью", "Only events I'm attending": "Только события, где я участвую", "Don't include in availability": "Не учитывать в занятости", @@ -360,7 +359,6 @@ export const catalog: Catalog = { "New address book": "Новая адресная книга", "No address books yet.": "Адресных книг пока нет.", "Choose from address books": "Выбрать из адресных книг", - "Import vCard": "Импорт vCard", "Export all contacts": "Экспортировать все контакты", "Export address book": "Экспортировать эту адресную книгу", "Import contacts…": "Импортировать контакты…", @@ -435,7 +433,6 @@ export const catalog: Catalog = { "Make ihasmail yours.": "Настройте ihasmail под себя.", "Reading": "Чтение", "Reading pane": "Область чтения", - "Reading, sending and list behaviour. Settings are stored in this browser.": "Поведение при чтении, отправке и в списке. Настройки хранятся в этом браузере.", "Right of the list": "Справа от списка", "Below the list": "Под списком", "Hidden (open full width)": "Скрыта (открывать во всю ширину)", @@ -478,10 +475,6 @@ export const catalog: Catalog = { "Time zone": "Часовой пояс", "Week starts on": "Неделя начинается с", "Monday": "Понедельник", - "Tuesday": "Вторник", - "Wednesday": "Среда", - "Thursday": "Четверг", - "Friday": "Пятница", "Saturday": "Суббота", "Sunday": "Воскресенье", "12-hour clock (6:23 PM)": "12-часовой формат (6:23 PM)", @@ -727,10 +720,8 @@ export const catalog: Catalog = { "Manage labels": "Управление ярлыками", "Create “{name}”": "Создать «{name}»", "Type a name to create your first label.": "Введите название, чтобы создать первый ярлык.", - "Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "Ярлыки — это ключевые слова IMAP, которые хранятся в самих письмах и синхронизируются с другими клиентами. Названия и цвета остаются в этом браузере.", "New label": "Новый ярлык", "Delete label": "Удалить ярлык", - "PDF": "PDF", "Large attachments may be rejected by some servers": "Некоторые серверы отклоняют большие вложения", "Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "Изображения хранятся в ваших Файлах (папка «ihasmail») и вставляются при отправке.", "Thanks for your message. I'm away until … and will reply when I'm back.": "Спасибо за письмо. Я отсутствую до … и отвечу после возвращения.", @@ -864,7 +855,6 @@ export const catalog: Catalog = { "Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them — a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Здесь показаны только языки, на которые ihasmail переведён, поэтому список растёт вместе с переводами, а не опережает их: язык без текстов заставил бы страницу утверждать, что она написана на языке, которым не является.", "tell us about it": "сообщите нам", "This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Этот перевод сделан ИИ и не проверен носителем языка, поэтому помечен как Beta до тех пор, пока кто-нибудь его не подтвердит. Обо всём, что звучит неправильно, стоит сообщить — {report}.", - "{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} — это палитра с {site}, с которой начинает новая учётная запись. Тема тёмная, поэтому везде, где это важно, считается тёмной, а акцентный цвет ниже применяется поверх неё.", "ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "Собственная версия ihasmail — это дата коммита, из которого он собран, и указание, откуда этот коммит взялся: {example} собран из коммита от 30 августа 2026 года, пришедшего через pull request 129. Коммит, пришедший иначе, несёт вместо этого короткий SHA — {sha}. Версия намеренно ничего не сообщает о Stalwart; то, что этой сборке нужно от сервера, указано строкой выше.", // ── Composer status, calendar title ──────────────────────────────── diff --git a/web/src/locales/uk.ts b/web/src/locales/uk.ts index 669a082..f9d67d7 100644 --- a/web/src/locales/uk.ts +++ b/web/src/locales/uk.ts @@ -315,7 +315,6 @@ export const catalog: Catalog = { "Busy": "Зайнятий", "Free/busy": "Зайнятість", "Show as": "Показувати як", - "Availability on {date}": "Зайнятість на {date}", "Count all events as busy": "Вважати всі події зайнятістю", "Only events I'm attending": "Лише події, де я беру участь", "Don't include in availability": "Не враховувати в зайнятості", @@ -354,7 +353,6 @@ export const catalog: Catalog = { "New address book": "Нова адресна книга", "No address books yet.": "Адресних книг поки немає.", "Choose from address books": "Вибрати з адресних книг", - "Import vCard": "Імпорт vCard", "Export all contacts": "Експортувати всі контакти", "Export address book": "Експортувати цю адресну книгу", "Import contacts…": "Імпортувати контакти…", @@ -429,7 +427,6 @@ export const catalog: Catalog = { "Make ihasmail yours.": "Налаштуйте ihasmail під себе.", "Reading": "Читання", "Reading pane": "Область читання", - "Reading, sending and list behaviour. Settings are stored in this browser.": "Поведінка під час читання, надсилання та в списку. Налаштування зберігаються в цьому браузері.", "Right of the list": "Праворуч від списку", "Below the list": "Під списком", "Hidden (open full width)": "Прихована (відкривати на всю ширину)", @@ -472,10 +469,6 @@ export const catalog: Catalog = { "Time zone": "Часовий пояс", "Week starts on": "Тиждень починається з", "Monday": "Понеділок", - "Tuesday": "Вівторок", - "Wednesday": "Середа", - "Thursday": "Четвер", - "Friday": "П'ятниця", "Saturday": "Субота", "Sunday": "Неділя", "12-hour clock (6:23 PM)": "12-годинний формат (6:23 PM)", @@ -721,10 +714,8 @@ export const catalog: Catalog = { "Manage labels": "Керування мітками", "Create “{name}”": "Створити «{name}»", "Type a name to create your first label.": "Введіть назву, щоб створити першу мітку.", - "Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "Мітки — це ключові слова IMAP, які зберігаються в самих листах і синхронізуються з іншими клієнтами. Назви та кольори залишаються в цьому браузері.", "New label": "Нова мітка", "Delete label": "Видалити мітку", - "PDF": "PDF", "Large attachments may be rejected by some servers": "Деякі сервери відхиляють великі вкладення", "Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "Зображення зберігаються у ваших Файлах (тека «ihasmail») і вставляються під час надсилання.", "Thanks for your message. I'm away until … and will reply when I'm back.": "Дякую за лист. Мене немає до … і я відповім після повернення.", @@ -858,7 +849,6 @@ export const catalog: Catalog = { "Only languages ihasmail has been translated into appear here, so this list grows as translations land rather than ahead of them — a language offered without strings behind it would leave the page claiming to be in a language it is not.": "Тут показано лише мови, якими перекладено ihasmail, тому список зростає разом із перекладами, а не випереджає їх: мова без текстів змусила б сторінку стверджувати, що вона написана мовою, якою не є.", "tell us about it": "повідомте нам", "This translation was generated by AI and has not been checked by a native speaker, so it is marked Beta until somebody who speaks it signs it off. Anything that reads wrongly is worth reporting — {report}.": "Цей переклад зроблено ШІ й не перевірено носієм мови, тому його позначено як Beta, доки хтось його не підтвердить. Про все, що звучить неправильно, варто повідомити — {report}.", - "{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} — це палітра з {site}, з якою починає новий обліковий запис. Тема темна, тому скрізь, де це важливо, вважається темною, а акцентний колір нижче застосовується поверх неї.", "ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "Власна версія ihasmail — це дата коміту, з якого його зібрано, і вказівка, звідки цей коміт узявся: {example} зібрано з коміту від 30 серпня 2026 року, що надійшов через pull request 129. Коміт, який надійшов інакше, несе замість цього короткий SHA — {sha}. Версія навмисно нічого не повідомляє про Stalwart; те, що цій збірці потрібно від сервера, вказано рядком вище.", // ── Composer status, calendar title ──────────────────────────────── diff --git a/web/src/locales/zh-Hans.ts b/web/src/locales/zh-Hans.ts index 19569b9..fc0951e 100644 --- a/web/src/locales/zh-Hans.ts +++ b/web/src/locales/zh-Hans.ts @@ -317,7 +317,6 @@ export const catalog: Catalog = { "Busy": "忙碌", "Free/busy": "忙闲状态", "Show as": "显示为", - "Availability on {date}": "{date} 的忙闲状态", "Count all events as busy": "所有日程都计为忙碌", "Only events I'm attending": "仅我参加的日程", "Don't include in availability": "不计入忙闲状态", @@ -356,7 +355,6 @@ export const catalog: Catalog = { "New address book": "新建通讯录", "No address books yet.": "还没有通讯录。", "Choose from address books": "从通讯录中选择", - "Import vCard": "导入 vCard", "Export all contacts": "导出所有联系人", "Export address book": "导出此通讯录", "Import contacts…": "导入联系人…", @@ -431,7 +429,6 @@ export const catalog: Catalog = { "Make ihasmail yours.": "把 ihasmail 调成您喜欢的样子。", "Reading": "阅读", "Reading pane": "阅读窗格", - "Reading, sending and list behaviour. Settings are stored in this browser.": "阅读、发送和列表行为。设置保存在此浏览器中。", "Right of the list": "列表右侧", "Below the list": "列表下方", "Hidden (open full width)": "隐藏(全宽打开)", @@ -474,10 +471,6 @@ export const catalog: Catalog = { "Time zone": "时区", "Week starts on": "每周开始于", "Monday": "星期一", - "Tuesday": "星期二", - "Wednesday": "星期三", - "Thursday": "星期四", - "Friday": "星期五", "Saturday": "星期六", "Sunday": "星期日", "12-hour clock (6:23 PM)": "12 小时制 (6:23 PM)", @@ -728,12 +721,10 @@ export const catalog: Catalog = { "Manage labels": "管理标签", "Create “{name}”": "创建「{name}」", "Type a name to create your first label.": "输入名称以创建您的第一个标签。", - "Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.": "标签是保存在邮件上的 IMAP 关键词,因此会同步到其他客户端。名称和颜色则保存在此浏览器中。", "New label": "新建标签", "Delete label": "删除标签", // ── Attachments, dates, search prose ─────────────────────────────── - "PDF": "PDF", "Large attachments may be rejected by some servers": "部分服务器可能拒收过大的附件", "Images are stored in your Files (folder “ihasmail”) and embedded when you send.": "图片保存在您的「文件」中(文件夹「ihasmail」),并在发送时嵌入邮件。", "After": "晚于", @@ -808,7 +799,6 @@ export const catalog: Catalog = { "Copy it into {name} now — it isn't shown again.": "请立即把它复制到 {name}——它不会再次显示。", "No other users found in the directory, so nobody new can be added. Sharing already in place is listed below and can still be removed.": "目录中没有找到其他用户,因此无法添加新的共享对象。已有的共享列在下方,仍可移除。", "Stalwart does not publish its version number to mail clients, so ihasmail reports the edition where the server gives one. ihasmail requires 0.16 or newer, and sign-in refuses anything older.": "Stalwart 不会向邮件客户端公布版本号,因此只有在服务器给出版本类型时,ihasmail 才会报告它。ihasmail 需要 0.16 或更高版本,更旧的版本一律无法登录。", - "{name} is the palette from {site}, and what a new account starts on. It is a dark theme, so it counts as dark wherever that matters, and the accent colour below still applies on top of it.": "{name} 是 {site} 的配色,也是新账户的初始主题。它属于深色主题,因此在需要区分明暗的地方都算作深色,下方的强调色仍会叠加在它之上。", "ihasmail's own version is the date of the commit it was built from, followed by where that commit came from: {example} was built from a commit dated the 30th of August 2026 that arrived through pull request 129. A commit that did not come through one carries its short SHA instead — {sha}. The version deliberately says nothing about Stalwart; what this build needs from the server is the line above.": "ihasmail 自身的版本号是其构建所用提交的日期,后面跟着该提交的来源:{example} 表示由 2026 年 8 月 30 日的一个提交构建而成,而该提交来自第 129 号拉取请求。未经拉取请求的提交则改用简短 SHA 表示——{sha}。版本号刻意不包含任何关于 Stalwart 的信息;此版本对服务器的要求见上一行。", // ── Constant labels ────────────────────────────────────────────────