From cdfbde663de12f10598be5dc074b29b7e00159d5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 09:20:11 -0800 Subject: [PATCH 01/28] i18n: translate the composite font errors in the complete-message languages The errors of type.compositeFonts.set, export and import, the composite font checks behind them and the Composite Fonts dialog showed in English in every language. Each complete-message catalog gets a row for each of them. The translations keep the font name, the parameter keys and the class keys (kanji, kana, punctuation, symbols, roman, numerals) as written, and NAMED_KEYS checks that they do. --- crates/ui-egui/src/i18n/ca.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/cs.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/de.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/el.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/es.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/fi.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/fr.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/hi.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/hu.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/it.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/ja.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/ka.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/ko.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/nl.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/pl.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/pt-br.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/ru.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/sv.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/tests.rs | 95 +++++++++++++++++++++++++++++ crates/ui-egui/src/i18n/tr.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/uk.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/vi.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/zh-hans.tsv | 36 +++++++++++ crates/ui-egui/src/i18n/zh-hant.tsv | 36 +++++++++++ 24 files changed, 923 insertions(+) diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 5516c9662f..f293bb4202 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -4589,6 +4589,42 @@ digits Malayalam Malaiàlam Composite Fonts Tipus de lletra compostos Export Composite Fonts Exporta tipus de lletra compostos Import Composite Fonts Importa tipus de lletra compostos +@msg each font must be {name, parts} cada tipus de lletra ha de ser {name, parts} +@msg unknown key `{k}`; a font takes name and parts clau desconeguda `{k}`; un tipus de lletra admet name i parts +@msg each font needs a name cada tipus de lletra necessita un nom +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` no pot anomenar un tipus de lletra compost (d’1 a {COMPOSITE_NAME_MAX} caràcters, sense caràcters de control) +@msg `{name}`: parts must be an object `{name}`: parts ha de ser un objecte +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: classe desconeguda `{k}`; les classes són kanji, kana, punctuation, symbols, roman i numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} ha de ser un objecte +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: clau desconeguda `{k}` a {_1} +@msg `{name}`: give the kanji part `{name}`: indiqueu la part kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un document conté com a màxim {COMPOSITE_FONTS_MAX} tipus de lletra compostos +@msg two composite fonts are named `{_1}` dos tipus de lletra compostos es diuen `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` és el nom d’un tipus de lletra instal·lat +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, un tipus de lletra compost +@msg give fonts: [{name, parts}…] indiqueu fonts: [{name, parts}…] +@msg path is empty path és buit +@msg fonts must be a list of composite fonts fonts ha de ser una llista de tipus de lletra compostos +@msg names must be strings names ha de contenir cadenes +@msg no composite font named `{n}` cap tipus de lletra compost anomenat `{n}` +@msg names must be a list of names names ha de ser una llista de noms +@msg no composite fonts to export cap tipus de lletra compost per exportar +@msg not a composite fonts file: {e} no és un fitxer de tipus de lletra compostos: {e} +@msg not a composite fonts file: no compositeFonts list no és un fitxer de tipus de lletra compostos: no hi ha cap llista compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts el fitxer conté més de {COMPOSITE_FONTS_MAX} tipus de lletra compostos +@msg the file is larger than 4 MB el fitxer ocupa més de 4 MB +@msg dataBase64 is not base64 dataBase64 no és base64 +@msg give one of path, data or dataBase64 indiqueu path, data o dataBase64 +@msg the file holds no composite fonts el fitxer no conté cap tipus de lletra compost +@msg the file holds two composite fonts named `{_1}` el fitxer conté dos tipus de lletra compostos anomenats `{_1}` +@msg `{_1}` can't name a composite font `{_1}` no pot anomenar un tipus de lletra compost +@msg {k}: give a font {k}: indiqueu un tipus de lletra +@msg {k}: size must be {_1} to {_2}% {k}: size ha d’estar entre {_1} i {_2} % +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline ha d’estar entre {_1} i {_2} % +@msg {k}: scale must be {_1} to {_2}% {k}: l’escala ha d’estar entre {_1} i {_2} % +@msg {k}: only kana, roman and numerals can be scaled {k}: només kana, roman i numerals es poden escalar +@msg {k}: only kana can be scaled at the center {k}: només kana es pot escalar des del centre +@msg no composite font cap tipus de lletra compost Delete Font Suprimeix el tipus de lletra Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index d03d192a3a..5e850005d1 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1638,6 +1638,42 @@ digits Malayalam Malajálamské Composite Fonts Složená písma Export Composite Fonts Exportovat složená písma Import Composite Fonts Importovat složená písma +@msg each font must be {name, parts} každé písmo musí být {name, parts} +@msg unknown key `{k}`; a font takes name and parts neznámý klíč `{k}`; písmo přijímá name a parts +@msg each font needs a name každé písmo potřebuje název +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` nemůže být názvem složeného písma (1 až {COMPOSITE_NAME_MAX} znaků, bez řídicích znaků) +@msg `{name}`: parts must be an object `{name}`: parts musí být objekt +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: neznámá třída `{k}`; třídy jsou kanji, kana, punctuation, symbols, roman a numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} musí být objekt +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: neznámý klíč `{k}` v {_1} +@msg `{name}`: give the kanji part `{name}`: zadejte část kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts dokument obsahuje nejvýše {COMPOSITE_FONTS_MAX} složených písem +@msg two composite fonts are named `{_1}` dvě složená písma se jmenují `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` je název nainstalovaného písma +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} používá `{font}`, což je složené písmo +@msg give fonts: [{name, parts}…] zadejte fonts: [{name, parts}…] +@msg path is empty path je prázdné +@msg fonts must be a list of composite fonts fonts musí být seznam složených písem +@msg names must be strings names musí být řetězce +@msg no composite font named `{n}` žádné složené písmo s názvem `{n}` +@msg names must be a list of names names musí být seznam názvů +@msg no composite fonts to export žádná složená písma k exportu +@msg not a composite fonts file: {e} nejde o soubor složených písem: {e} +@msg not a composite fonts file: no compositeFonts list nejde o soubor složených písem: chybí seznam compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts soubor obsahuje více než {COMPOSITE_FONTS_MAX} složených písem +@msg the file is larger than 4 MB soubor je větší než 4 MB +@msg dataBase64 is not base64 dataBase64 není base64 +@msg give one of path, data or dataBase64 zadejte path, data nebo dataBase64 +@msg the file holds no composite fonts soubor neobsahuje žádná složená písma +@msg the file holds two composite fonts named `{_1}` soubor obsahuje dvě složená písma s názvem `{_1}` +@msg `{_1}` can't name a composite font `{_1}` nemůže být názvem složeného písma +@msg {k}: give a font {k}: zadejte písmo +@msg {k}: size must be {_1} to {_2}% {k}: size musí být {_1} až {_2} % +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline musí být {_1} až {_2} % +@msg {k}: scale must be {_1} to {_2}% {k}: měřítko musí být {_1} až {_2} % +@msg {k}: only kana, roman and numerals can be scaled {k}: měnit měřítko lze jen u kana, roman a numerals +@msg {k}: only kana can be scaled at the center {k}: měřítko od středu lze měnit jen u kana +@msg no composite font žádné složené písmo Delete Font Odstranit písmo Kanji Kandži Kana Kana diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 47337855aa..0fd46c7d0b 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3590,6 +3590,42 @@ effect Points Punkte Composite Fonts Zusammengesetzte Schriften Export Composite Fonts Zusammengesetzte Schriften exportieren Import Composite Fonts Zusammengesetzte Schriften importieren +@msg each font must be {name, parts} jede Schrift muss {name, parts} sein +@msg unknown key `{k}`; a font takes name and parts unbekannter Schlüssel `{k}`; eine Schrift nimmt name und parts +@msg each font needs a name jede Schrift braucht einen Namen +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kann keine zusammengesetzte Schrift benennen (1 bis {COMPOSITE_NAME_MAX} Zeichen, keine Steuerzeichen) +@msg `{name}`: parts must be an object `{name}`: parts muss ein Objekt sein +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: unbekannte Klasse `{k}`; die Klassen sind kanji, kana, punctuation, symbols, roman und numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} muss ein Objekt sein +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: unbekannter Schlüssel `{k}` in {_1} +@msg `{name}`: give the kanji part `{name}`: geben Sie den Teil kanji an +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ein Dokument enthält höchstens {COMPOSITE_FONTS_MAX} zusammengesetzte Schriften +@msg two composite fonts are named `{_1}` zwei zusammengesetzte Schriften heißen `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` ist der Name einer installierten Schrift +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} ist in `{font}` gesetzt, einer zusammengesetzten Schrift +@msg give fonts: [{name, parts}…] geben Sie fonts an: [{name, parts}…] +@msg path is empty path ist leer +@msg fonts must be a list of composite fonts fonts muss eine Liste zusammengesetzter Schriften sein +@msg names must be strings names müssen Zeichenketten sein +@msg no composite font named `{n}` keine zusammengesetzte Schrift namens `{n}` +@msg names must be a list of names names muss eine Liste von Namen sein +@msg no composite fonts to export keine zusammengesetzten Schriften zum Exportieren +@msg not a composite fonts file: {e} keine Datei mit zusammengesetzten Schriften: {e} +@msg not a composite fonts file: no compositeFonts list keine Datei mit zusammengesetzten Schriften: keine Liste compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts die Datei enthält mehr als {COMPOSITE_FONTS_MAX} zusammengesetzte Schriften +@msg the file is larger than 4 MB die Datei ist größer als 4 MB +@msg dataBase64 is not base64 dataBase64 ist kein Base64 +@msg give one of path, data or dataBase64 geben Sie path, data oder dataBase64 an +@msg the file holds no composite fonts die Datei enthält keine zusammengesetzten Schriften +@msg the file holds two composite fonts named `{_1}` die Datei enthält zwei zusammengesetzte Schriften namens `{_1}` +@msg `{_1}` can't name a composite font `{_1}` kann keine zusammengesetzte Schrift benennen +@msg {k}: give a font {k}: geben Sie eine Schrift an +@msg {k}: size must be {_1} to {_2}% {k}: size muss {_1} bis {_2} % sein +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline muss {_1} bis {_2} % sein +@msg {k}: scale must be {_1} to {_2}% {k}: die Skalierung muss {_1} bis {_2} % sein +@msg {k}: only kana, roman and numerals can be scaled {k}: nur kana, roman und numerals lassen sich skalieren +@msg {k}: only kana can be scaled at the center {k}: nur kana lässt sich am Mittelpunkt skalieren +@msg no composite font keine zusammengesetzte Schrift Delete Font Schrift löschen Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index c319729a71..98f743c7fc 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4109,6 +4109,42 @@ digits Malayalam Μαλαγιάλαμ Composite Fonts Σύνθετες γραμματοσειρές Export Composite Fonts Εξαγωγή σύνθετων γραμματοσειρών Import Composite Fonts Εισαγωγή σύνθετων γραμματοσειρών +@msg each font must be {name, parts} κάθε γραμματοσειρά πρέπει να είναι {name, parts} +@msg unknown key `{k}`; a font takes name and parts άγνωστο κλειδί `{k}`· μια γραμματοσειρά δέχεται name και parts +@msg each font needs a name κάθε γραμματοσειρά χρειάζεται όνομα +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) το `{raw}` δεν μπορεί να είναι όνομα σύνθετης γραμματοσειράς (1 έως {COMPOSITE_NAME_MAX} χαρακτήρες, χωρίς χαρακτήρες ελέγχου) +@msg `{name}`: parts must be an object `{name}`: το parts πρέπει να είναι αντικείμενο +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: άγνωστη κλάση `{k}`· οι κλάσεις είναι kanji, kana, punctuation, symbols, roman και numerals +@msg `{name}`: {_1} must be an object `{name}`: το {_1} πρέπει να είναι αντικείμενο +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: άγνωστο κλειδί `{k}` στο {_1} +@msg `{name}`: give the kanji part `{name}`: δώστε το τμήμα kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ένα έγγραφο περιέχει το πολύ {COMPOSITE_FONTS_MAX} σύνθετες γραμματοσειρές +@msg two composite fonts are named `{_1}` δύο σύνθετες γραμματοσειρές ονομάζονται `{_1}` +@msg `{_1}` is the name of an installed font το `{_1}` είναι όνομα εγκατεστημένης γραμματοσειράς +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: το {_2} χρησιμοποιεί την `{font}`, που είναι σύνθετη γραμματοσειρά +@msg give fonts: [{name, parts}…] δώστε fonts: [{name, parts}…] +@msg path is empty το path είναι κενό +@msg fonts must be a list of composite fonts το fonts πρέπει να είναι λίστα σύνθετων γραμματοσειρών +@msg names must be strings τα names πρέπει να είναι συμβολοσειρές +@msg no composite font named `{n}` δεν υπάρχει σύνθετη γραμματοσειρά με όνομα `{n}` +@msg names must be a list of names το names πρέπει να είναι λίστα ονομάτων +@msg no composite fonts to export δεν υπάρχουν σύνθετες γραμματοσειρές για εξαγωγή +@msg not a composite fonts file: {e} δεν είναι αρχείο σύνθετων γραμματοσειρών: {e} +@msg not a composite fonts file: no compositeFonts list δεν είναι αρχείο σύνθετων γραμματοσειρών: δεν υπάρχει λίστα compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts το αρχείο περιέχει περισσότερες από {COMPOSITE_FONTS_MAX} σύνθετες γραμματοσειρές +@msg the file is larger than 4 MB το αρχείο είναι μεγαλύτερο από 4 MB +@msg dataBase64 is not base64 το dataBase64 δεν είναι base64 +@msg give one of path, data or dataBase64 δώστε path, data ή dataBase64 +@msg the file holds no composite fonts το αρχείο δεν περιέχει σύνθετες γραμματοσειρές +@msg the file holds two composite fonts named `{_1}` το αρχείο περιέχει δύο σύνθετες γραμματοσειρές με όνομα `{_1}` +@msg `{_1}` can't name a composite font το `{_1}` δεν μπορεί να είναι όνομα σύνθετης γραμματοσειράς +@msg {k}: give a font {k}: δώστε γραμματοσειρά +@msg {k}: size must be {_1} to {_2}% {k}: το size πρέπει να είναι από {_1} έως {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}: το baseline πρέπει να είναι από {_1} έως {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}: η κλίμακα πρέπει να είναι από {_1} έως {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}: μόνο τα kana, roman και numerals αλλάζουν κλίμακα +@msg {k}: only kana can be scaled at the center {k}: μόνο το kana αλλάζει κλίμακα από το κέντρο +@msg no composite font καμία σύνθετη γραμματοσειρά Delete Font Διαγραφή γραμματοσειράς Kanji Κάντζι Kana Κάνα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 5b203292a4..af64c7e2de 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3638,6 +3638,42 @@ digits Malayalam Malayálam Composite Fonts Fuentes compuestas Export Composite Fonts Exportar fuentes compuestas Import Composite Fonts Importar fuentes compuestas +@msg each font must be {name, parts} cada fuente debe ser {name, parts} +@msg unknown key `{k}`; a font takes name and parts clave desconocida `{k}`; una fuente admite name y parts +@msg each font needs a name cada fuente necesita un nombre +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` no puede nombrar una fuente compuesta (de 1 a {COMPOSITE_NAME_MAX} caracteres, sin caracteres de control) +@msg `{name}`: parts must be an object `{name}`: parts debe ser un objeto +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: clase desconocida `{k}`; las clases son kanji, kana, punctuation, symbols, roman y numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} debe ser un objeto +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: clave desconocida `{k}` en {_1} +@msg `{name}`: give the kanji part `{name}`: indique la parte kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un documento admite como máximo {COMPOSITE_FONTS_MAX} fuentes compuestas +@msg two composite fonts are named `{_1}` dos fuentes compuestas se llaman `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` es el nombre de una fuente instalada +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, una fuente compuesta +@msg give fonts: [{name, parts}…] indique fonts: [{name, parts}…] +@msg path is empty path está vacío +@msg fonts must be a list of composite fonts fonts debe ser una lista de fuentes compuestas +@msg names must be strings names debe contener cadenas +@msg no composite font named `{n}` no hay ninguna fuente compuesta llamada `{n}` +@msg names must be a list of names names debe ser una lista de nombres +@msg no composite fonts to export no hay fuentes compuestas para exportar +@msg not a composite fonts file: {e} no es un archivo de fuentes compuestas: {e} +@msg not a composite fonts file: no compositeFonts list no es un archivo de fuentes compuestas: no hay lista compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts el archivo contiene más de {COMPOSITE_FONTS_MAX} fuentes compuestas +@msg the file is larger than 4 MB el archivo ocupa más de 4 MB +@msg dataBase64 is not base64 dataBase64 no es base64 +@msg give one of path, data or dataBase64 indique path, data o dataBase64 +@msg the file holds no composite fonts el archivo no contiene fuentes compuestas +@msg the file holds two composite fonts named `{_1}` el archivo contiene dos fuentes compuestas llamadas `{_1}` +@msg `{_1}` can't name a composite font `{_1}` no puede nombrar una fuente compuesta +@msg {k}: give a font {k}: indique una fuente +@msg {k}: size must be {_1} to {_2}% {k}: size debe estar entre {_1} y {_2} % +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline debe estar entre {_1} y {_2} % +@msg {k}: scale must be {_1} to {_2}% {k}: la escala debe estar entre {_1} y {_2} % +@msg {k}: only kana, roman and numerals can be scaled {k}: solo kana, roman y numerals se pueden escalar +@msg {k}: only kana can be scaled at the center {k}: solo kana se puede escalar desde el centro +@msg no composite font ninguna fuente compuesta Delete Font Eliminar fuente Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index b2c6f48d54..ccff1a1a06 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1611,6 +1611,42 @@ digits Malayalam Malajalam Composite Fonts Yhdistelmäfontit Export Composite Fonts Vie yhdistelmäfontit Import Composite Fonts Tuo yhdistelmäfontit +@msg each font must be {name, parts} jokaisen fontin on oltava {name, parts} +@msg unknown key `{k}`; a font takes name and parts tuntematon avain `{k}`; fontti ottaa name ja parts +@msg each font needs a name jokainen fontti tarvitsee nimen +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` ei kelpaa yhdistelmäfontin nimeksi (1–{COMPOSITE_NAME_MAX} merkkiä, ei ohjausmerkkejä) +@msg `{name}`: parts must be an object `{name}`: parts on oltava objekti +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: tuntematon luokka `{k}`; luokat ovat kanji, kana, punctuation, symbols, roman ja numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} on oltava objekti +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: tuntematon avain `{k}` osassa {_1} +@msg `{name}`: give the kanji part `{name}`: anna osa kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts asiakirjassa voi olla enintään {COMPOSITE_FONTS_MAX} yhdistelmäfonttia +@msg two composite fonts are named `{_1}` kahden yhdistelmäfontin nimi on `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` on asennetun fontin nimi +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} käyttää fonttia `{font}`, joka on yhdistelmäfontti +@msg give fonts: [{name, parts}…] anna fonts: [{name, parts}…] +@msg path is empty path on tyhjä +@msg fonts must be a list of composite fonts fonts on oltava luettelo yhdistelmäfonteista +@msg names must be strings names on oltava merkkijonoja +@msg no composite font named `{n}` yhdistelmäfonttia `{n}` ei ole +@msg names must be a list of names names on oltava luettelo nimistä +@msg no composite fonts to export ei vietäviä yhdistelmäfontteja +@msg not a composite fonts file: {e} ei yhdistelmäfonttitiedosto: {e} +@msg not a composite fonts file: no compositeFonts list ei yhdistelmäfonttitiedosto: luettelo compositeFonts puuttuu +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts tiedostossa on yli {COMPOSITE_FONTS_MAX} yhdistelmäfonttia +@msg the file is larger than 4 MB tiedosto on suurempi kuin 4 Mt +@msg dataBase64 is not base64 dataBase64 ei ole base64-muotoa +@msg give one of path, data or dataBase64 anna path, data tai dataBase64 +@msg the file holds no composite fonts tiedostossa ei ole yhdistelmäfontteja +@msg the file holds two composite fonts named `{_1}` tiedostossa on kaksi yhdistelmäfonttia nimeltä `{_1}` +@msg `{_1}` can't name a composite font `{_1}` ei kelpaa yhdistelmäfontin nimeksi +@msg {k}: give a font {k}: anna fontti +@msg {k}: size must be {_1} to {_2}% {k}: size on oltava {_1}–{_2} % +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline on oltava {_1}–{_2} % +@msg {k}: scale must be {_1} to {_2}% {k}: skaalauksen on oltava {_1}–{_2} % +@msg {k}: only kana, roman and numerals can be scaled {k}: vain kana, roman ja numerals voidaan skaalata +@msg {k}: only kana can be scaled at the center {k}: vain kana voidaan skaalata keskeltä +@msg no composite font ei yhdistelmäfonttia Delete Font Poista fontti Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index e84f344060..2468d74626 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3630,6 +3630,42 @@ digits Malayalam Malayalam Composite Fonts Polices composites Export Composite Fonts Exporter des polices composites Import Composite Fonts Importer des polices composites +@msg each font must be {name, parts} chaque police doit être {name, parts} +@msg unknown key `{k}`; a font takes name and parts clé inconnue `{k}` ; une police prend name et parts +@msg each font needs a name chaque police doit avoir un nom +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` ne peut pas nommer une police composite (1 à {COMPOSITE_NAME_MAX} caractères, sans caractères de contrôle) +@msg `{name}`: parts must be an object `{name}` : parts doit être un objet +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}` : classe inconnue `{k}` ; les classes sont kanji, kana, punctuation, symbols, roman et numerals +@msg `{name}`: {_1} must be an object `{name}` : {_1} doit être un objet +@msg `{name}`: unknown key `{k}` in {_1} `{name}` : clé inconnue `{k}` dans {_1} +@msg `{name}`: give the kanji part `{name}` : indiquez la partie kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un document contient au plus {COMPOSITE_FONTS_MAX} polices composites +@msg two composite fonts are named `{_1}` deux polices composites s’appellent `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` est le nom d’une police installée +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}` : {_2} utilise `{font}`, une police composite +@msg give fonts: [{name, parts}…] indiquez fonts : [{name, parts}…] +@msg path is empty path est vide +@msg fonts must be a list of composite fonts fonts doit être une liste de polices composites +@msg names must be strings names doit contenir des chaînes +@msg no composite font named `{n}` aucune police composite nommée `{n}` +@msg names must be a list of names names doit être une liste de noms +@msg no composite fonts to export aucune police composite à exporter +@msg not a composite fonts file: {e} ce n’est pas un fichier de polices composites : {e} +@msg not a composite fonts file: no compositeFonts list ce n’est pas un fichier de polices composites : pas de liste compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts le fichier contient plus de {COMPOSITE_FONTS_MAX} polices composites +@msg the file is larger than 4 MB le fichier dépasse 4 Mo +@msg dataBase64 is not base64 dataBase64 n’est pas du base64 +@msg give one of path, data or dataBase64 indiquez path, data ou dataBase64 +@msg the file holds no composite fonts le fichier ne contient aucune police composite +@msg the file holds two composite fonts named `{_1}` le fichier contient deux polices composites nommées `{_1}` +@msg `{_1}` can't name a composite font `{_1}` ne peut pas nommer une police composite +@msg {k}: give a font {k} : indiquez une police +@msg {k}: size must be {_1} to {_2}% {k} : size doit être compris entre {_1} et {_2} % +@msg {k}: baseline must be {_1} to {_2}% {k} : baseline doit être compris entre {_1} et {_2} % +@msg {k}: scale must be {_1} to {_2}% {k} : l’échelle doit être comprise entre {_1} et {_2} % +@msg {k}: only kana, roman and numerals can be scaled {k} : seuls kana, roman et numerals peuvent être mis à l’échelle +@msg {k}: only kana can be scaled at the center {k} : seul kana peut être mis à l’échelle depuis le centre +@msg no composite font aucune police composite Delete Font Supprimer la police Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 017bc03750..4246a55ac3 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -4972,6 +4972,42 @@ antiAlias Sharp शार्प Composite Fonts कंपोज़िट फ़ॉन्ट Export Composite Fonts कंपोज़िट फ़ॉन्ट एक्सपोर्ट करें Import Composite Fonts कंपोज़िट फ़ॉन्ट इम्पोर्ट करें +@msg each font must be {name, parts} हर फ़ॉन्ट {name, parts} होना चाहिए +@msg unknown key `{k}`; a font takes name and parts अज्ञात कुंजी `{k}`; फ़ॉन्ट name और parts लेता है +@msg each font needs a name हर फ़ॉन्ट का एक नाम होना चाहिए +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` कंपोज़िट फ़ॉन्ट का नाम नहीं हो सकता (1 से {COMPOSITE_NAME_MAX} अक्षर, कोई नियंत्रण अक्षर नहीं) +@msg `{name}`: parts must be an object `{name}`: parts एक ऑब्जेक्ट होना चाहिए +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: अज्ञात वर्ग `{k}`; वर्ग kanji, kana, punctuation, symbols, roman और numerals हैं +@msg `{name}`: {_1} must be an object `{name}`: {_1} एक ऑब्जेक्ट होना चाहिए +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} में अज्ञात कुंजी `{k}` +@msg `{name}`: give the kanji part `{name}`: kanji भाग दें +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts एक दस्तावेज़ में अधिकतम {COMPOSITE_FONTS_MAX} कंपोज़िट फ़ॉन्ट रहते हैं +@msg two composite fonts are named `{_1}` दो कंपोज़िट फ़ॉन्ट का नाम `{_1}` है +@msg `{_1}` is the name of an installed font `{_1}` एक इंस्टॉल किए गए फ़ॉन्ट का नाम है +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} `{font}` में सेट है, जो एक कंपोज़िट फ़ॉन्ट है +@msg give fonts: [{name, parts}…] fonts दें: [{name, parts}…] +@msg path is empty path खाली है +@msg fonts must be a list of composite fonts fonts कंपोज़िट फ़ॉन्ट की सूची होनी चाहिए +@msg names must be strings names स्ट्रिंग होने चाहिए +@msg no composite font named `{n}` `{n}` नाम का कोई कंपोज़िट फ़ॉन्ट नहीं है +@msg names must be a list of names names नामों की सूची होनी चाहिए +@msg no composite fonts to export एक्सपोर्ट करने के लिए कोई कंपोज़िट फ़ॉन्ट नहीं है +@msg not a composite fonts file: {e} यह कंपोज़िट फ़ॉन्ट फ़ाइल नहीं है: {e} +@msg not a composite fonts file: no compositeFonts list यह कंपोज़िट फ़ॉन्ट फ़ाइल नहीं है: compositeFonts सूची नहीं है +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts फ़ाइल में {COMPOSITE_FONTS_MAX} से अधिक कंपोज़िट फ़ॉन्ट हैं +@msg the file is larger than 4 MB फ़ाइल 4 MB से बड़ी है +@msg dataBase64 is not base64 dataBase64 base64 नहीं है +@msg give one of path, data or dataBase64 path, data या dataBase64 में से एक दें +@msg the file holds no composite fonts फ़ाइल में कोई कंपोज़िट फ़ॉन्ट नहीं है +@msg the file holds two composite fonts named `{_1}` फ़ाइल में `{_1}` नाम के दो कंपोज़िट फ़ॉन्ट हैं +@msg `{_1}` can't name a composite font `{_1}` कंपोज़िट फ़ॉन्ट का नाम नहीं हो सकता +@msg {k}: give a font {k}: एक फ़ॉन्ट दें +@msg {k}: size must be {_1} to {_2}% {k}: size {_1} से {_2}% तक होना चाहिए +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline {_1} से {_2}% तक होना चाहिए +@msg {k}: scale must be {_1} to {_2}% {k}: स्केल {_1} से {_2}% तक होना चाहिए +@msg {k}: only kana, roman and numerals can be scaled {k}: केवल kana, roman और numerals को स्केल किया जा सकता है +@msg {k}: only kana can be scaled at the center {k}: केंद्र से केवल kana को स्केल किया जा सकता है +@msg no composite font कोई कंपोज़िट फ़ॉन्ट नहीं Delete Font फ़ॉन्ट हटाएँ Kanji कांजी Kana काना diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index effb31e5d8..5c8f222c62 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4303,6 +4303,42 @@ mark-style Japanese Japán Composite Fonts Összetett betűtípusok Export Composite Fonts Összetett betűtípusok exportálása Import Composite Fonts Összetett betűtípusok importálása +@msg each font must be {name, parts} minden betűtípusnak {name, parts} alakúnak kell lennie +@msg unknown key `{k}`; a font takes name and parts ismeretlen kulcs: `{k}`; egy betűtípus name és parts kulcsot fogad +@msg each font needs a name minden betűtípusnak kell egy név +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` nem lehet összetett betűtípus neve (1–{COMPOSITE_NAME_MAX} karakter, vezérlőkarakterek nélkül) +@msg `{name}`: parts must be an object `{name}`: a parts értékének objektumnak kell lennie +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: ismeretlen osztály: `{k}`; az osztályok: kanji, kana, punctuation, symbols, roman és numerals +@msg `{name}`: {_1} must be an object `{name}`: a(z) {_1} értékének objektumnak kell lennie +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: ismeretlen kulcs: `{k}` itt: {_1} +@msg `{name}`: give the kanji part `{name}`: adja meg a kanji részt +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts egy dokumentum legfeljebb {COMPOSITE_FONTS_MAX} összetett betűtípust tartalmazhat +@msg two composite fonts are named `{_1}` két összetett betűtípus neve is `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` egy telepített betűtípus neve +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: a(z) {_2} a(z) `{font}` összetett betűtípust használja +@msg give fonts: [{name, parts}…] adja meg: fonts: [{name, parts}…] +@msg path is empty a path üres +@msg fonts must be a list of composite fonts a fonts értékének összetett betűtípusok listájának kell lennie +@msg names must be strings a names elemeinek karakterláncoknak kell lenniük +@msg no composite font named `{n}` nincs `{n}` nevű összetett betűtípus +@msg names must be a list of names a names értékének nevek listájának kell lennie +@msg no composite fonts to export nincs exportálható összetett betűtípus +@msg not a composite fonts file: {e} nem összetettbetűtípus-fájl: {e} +@msg not a composite fonts file: no compositeFonts list nem összetettbetűtípus-fájl: nincs compositeFonts lista +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts a fájl több mint {COMPOSITE_FONTS_MAX} összetett betűtípust tartalmaz +@msg the file is larger than 4 MB a fájl nagyobb 4 MB-nál +@msg dataBase64 is not base64 a dataBase64 nem base64 +@msg give one of path, data or dataBase64 adja meg a path, data vagy dataBase64 egyikét +@msg the file holds no composite fonts a fájl nem tartalmaz összetett betűtípust +@msg the file holds two composite fonts named `{_1}` a fájl két `{_1}` nevű összetett betűtípust tartalmaz +@msg `{_1}` can't name a composite font `{_1}` nem lehet összetett betűtípus neve +@msg {k}: give a font {k}: adjon meg egy betűtípust +@msg {k}: size must be {_1} to {_2}% {k}: a size értéke {_1}–{_2}% lehet +@msg {k}: baseline must be {_1} to {_2}% {k}: a baseline értéke {_1}–{_2}% lehet +@msg {k}: scale must be {_1} to {_2}% {k}: a méretezés {_1}–{_2}% lehet +@msg {k}: only kana, roman and numerals can be scaled {k}: csak a kana, roman és numerals méretezhető +@msg {k}: only kana can be scaled at the center {k}: csak a kana méretezhető a középpontból +@msg no composite font nincs összetett betűtípus Delete Font Betűtípus törlése Kanji Kandzsi Kana Kana diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 2d6c7e2186..98e597c75e 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3684,6 +3684,42 @@ digits Malayalam Malayalam Composite Fonts Font compositi Export Composite Fonts Esporta font compositi Import Composite Fonts Importa font compositi +@msg each font must be {name, parts} ogni font deve essere {name, parts} +@msg unknown key `{k}`; a font takes name and parts chiave sconosciuta `{k}`; un font accetta name e parts +@msg each font needs a name ogni font deve avere un nome +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` non può essere il nome di un font composito (da 1 a {COMPOSITE_NAME_MAX} caratteri, senza caratteri di controllo) +@msg `{name}`: parts must be an object `{name}`: parts deve essere un oggetto +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: classe sconosciuta `{k}`; le classi sono kanji, kana, punctuation, symbols, roman e numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} deve essere un oggetto +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: chiave sconosciuta `{k}` in {_1} +@msg `{name}`: give the kanji part `{name}`: indica la parte kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts un documento contiene al massimo {COMPOSITE_FONTS_MAX} font compositi +@msg two composite fonts are named `{_1}` due font compositi si chiamano `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` è il nome di un font installato +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, un font composito +@msg give fonts: [{name, parts}…] indica fonts: [{name, parts}…] +@msg path is empty path è vuoto +@msg fonts must be a list of composite fonts fonts deve essere un elenco di font compositi +@msg names must be strings names deve contenere stringhe +@msg no composite font named `{n}` nessun font composito di nome `{n}` +@msg names must be a list of names names deve essere un elenco di nomi +@msg no composite fonts to export nessun font composito da esportare +@msg not a composite fonts file: {e} non è un file di font compositi: {e} +@msg not a composite fonts file: no compositeFonts list non è un file di font compositi: manca l’elenco compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts il file contiene più di {COMPOSITE_FONTS_MAX} font compositi +@msg the file is larger than 4 MB il file supera 4 MB +@msg dataBase64 is not base64 dataBase64 non è base64 +@msg give one of path, data or dataBase64 indica path, data o dataBase64 +@msg the file holds no composite fonts il file non contiene font compositi +@msg the file holds two composite fonts named `{_1}` il file contiene due font compositi di nome `{_1}` +@msg `{_1}` can't name a composite font `{_1}` non può essere il nome di un font composito +@msg {k}: give a font {k}: indica un font +@msg {k}: size must be {_1} to {_2}% {k}: size deve essere tra {_1} e {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline deve essere tra {_1} e {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}: la scala deve essere tra {_1} e {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}: solo kana, roman e numerals si possono ridimensionare +@msg {k}: only kana can be scaled at the center {k}: solo kana si può ridimensionare dal centro +@msg no composite font nessun font composito Delete Font Elimina font Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 17444d894e..89f481878d 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4013,6 +4013,42 @@ file-filter Templates テンプレート Composite Fonts 合成フォント Export Composite Fonts 合成フォントを書き出し Import Composite Fonts 合成フォントを読み込み +@msg each font must be {name, parts} 各フォントは {name, parts} の形にしてください +@msg unknown key `{k}`; a font takes name and parts `{k}` は不明なキーです。フォントに使えるのは name と parts です +@msg each font needs a name 各フォントには名前が必要です +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` は合成フォントの名前にできません (1~{COMPOSITE_NAME_MAX} 文字、制御文字なし) +@msg `{name}`: parts must be an object `{name}`: parts はオブジェクトにしてください +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: `{k}` は不明なクラスです。クラスは kanji、kana、punctuation、symbols、roman、numerals です +@msg `{name}`: {_1} must be an object `{name}`: {_1} はオブジェクトにしてください +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} の `{k}` は不明なキーです +@msg `{name}`: give the kanji part `{name}`: kanji の部分を指定してください +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 1 つのドキュメントに置ける合成フォントは {COMPOSITE_FONTS_MAX} 個までです +@msg two composite fonts are named `{_1}` `{_1}` という名前の合成フォントが 2 つあります +@msg `{_1}` is the name of an installed font `{_1}` はインストール済みのフォントの名前です +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} に合成フォントの `{font}` が設定されています +@msg give fonts: [{name, parts}…] fonts を指定してください: [{name, parts}…] +@msg path is empty path が空です +@msg fonts must be a list of composite fonts fonts は合成フォントのリストにしてください +@msg names must be strings names は文字列にしてください +@msg no composite font named `{n}` `{n}` という合成フォントはありません +@msg names must be a list of names names は名前のリストにしてください +@msg no composite fonts to export 書き出す合成フォントがありません +@msg not a composite fonts file: {e} 合成フォントのファイルではありません: {e} +@msg not a composite fonts file: no compositeFonts list 合成フォントのファイルではありません: compositeFonts のリストがありません +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts ファイルに {COMPOSITE_FONTS_MAX} 個を超える合成フォントがあります +@msg the file is larger than 4 MB ファイルが 4 MB を超えています +@msg dataBase64 is not base64 dataBase64 が base64 ではありません +@msg give one of path, data or dataBase64 path、data、dataBase64 のいずれかを指定してください +@msg the file holds no composite fonts ファイルに合成フォントがありません +@msg the file holds two composite fonts named `{_1}` ファイルに `{_1}` という名前の合成フォントが 2 つあります +@msg `{_1}` can't name a composite font `{_1}` は合成フォントの名前にできません +@msg {k}: give a font {k}: フォントを指定してください +@msg {k}: size must be {_1} to {_2}% {k}: size は {_1}~{_2}% にしてください +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline は {_1}~{_2}% にしてください +@msg {k}: scale must be {_1} to {_2}% {k}: 比率は {_1}~{_2}% にしてください +@msg {k}: only kana, roman and numerals can be scaled {k}: 比率を変更できるのは kana、roman、numerals だけです +@msg {k}: only kana can be scaled at the center {k}: 中央を基準に比率を変更できるのは kana だけです +@msg no composite font 合成フォントがありません Delete Font フォントを削除 Kanji 漢字 Kana かな diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index be8a847cb2..07ae7d42b2 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1482,6 +1482,42 @@ digits Malayalam მალაიალამური Composite Fonts კომპოზიტური შრიფტები Export Composite Fonts კომპოზიტური შრიფტების ექსპორტი Import Composite Fonts კომპოზიტური შრიფტების იმპორტი +@msg each font must be {name, parts} თითოეული შრიფტი უნდა იყოს {name, parts} +@msg unknown key `{k}`; a font takes name and parts უცნობი გასაღები `{k}`; შრიფტი იღებს name-სა და parts-ს +@msg each font needs a name თითოეულ შრიფტს სახელი სჭირდება +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` ვერ იქნება კომპოზიტური შრიფტის სახელი (1-დან {COMPOSITE_NAME_MAX} სიმბოლომდე, მმართველი სიმბოლოების გარეშე) +@msg `{name}`: parts must be an object `{name}`: parts უნდა იყოს ობიექტი +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: უცნობი კლასი `{k}`; კლასებია: kanji, kana, punctuation, symbols, roman და numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} უნდა იყოს ობიექტი +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: უცნობი გასაღები `{k}` ნაწილში {_1} +@msg `{name}`: give the kanji part `{name}`: მიუთითეთ ნაწილი kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts დოკუმენტი შეიცავს არაუმეტეს {COMPOSITE_FONTS_MAX} კომპოზიტურ შრიფტს +@msg two composite fonts are named `{_1}` ორ კომპოზიტურ შრიფტს ჰქვია `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` დაინსტალირებული შრიფტის სახელია +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} იყენებს `{font}`-ს, რომელიც კომპოზიტური შრიფტია +@msg give fonts: [{name, parts}…] მიუთითეთ fonts: [{name, parts}…] +@msg path is empty path ცარიელია +@msg fonts must be a list of composite fonts fonts უნდა იყოს კომპოზიტური შრიფტების სია +@msg names must be strings names უნდა იყოს სტრიქონები +@msg no composite font named `{n}` კომპოზიტური შრიფტი სახელით `{n}` არ არსებობს +@msg names must be a list of names names უნდა იყოს სახელების სია +@msg no composite fonts to export საექსპორტო კომპოზიტური შრიფტები არ არის +@msg not a composite fonts file: {e} ეს არ არის კომპოზიტური შრიფტების ფაილი: {e} +@msg not a composite fonts file: no compositeFonts list ეს არ არის კომპოზიტური შრიფტების ფაილი: სია compositeFonts არ არის +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts ფაილი შეიცავს {COMPOSITE_FONTS_MAX}-ზე მეტ კომპოზიტურ შრიფტს +@msg the file is larger than 4 MB ფაილი 4 MB-ზე დიდია +@msg dataBase64 is not base64 dataBase64 არ არის base64 +@msg give one of path, data or dataBase64 მიუთითეთ path, data ან dataBase64 +@msg the file holds no composite fonts ფაილი კომპოზიტურ შრიფტებს არ შეიცავს +@msg the file holds two composite fonts named `{_1}` ფაილი შეიცავს ორ კომპოზიტურ შრიფტს სახელით `{_1}` +@msg `{_1}` can't name a composite font `{_1}` ვერ იქნება კომპოზიტური შრიფტის სახელი +@msg {k}: give a font {k}: მიუთითეთ შრიფტი +@msg {k}: size must be {_1} to {_2}% {k}: size უნდა იყოს {_1}-დან {_2}%-მდე +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline უნდა იყოს {_1}-დან {_2}%-მდე +@msg {k}: scale must be {_1} to {_2}% {k}: მასშტაბი უნდა იყოს {_1}-დან {_2}%-მდე +@msg {k}: only kana, roman and numerals can be scaled {k}: მასშტაბირება შეიძლება მხოლოდ kana, roman და numerals ნაწილებში +@msg {k}: only kana can be scaled at the center {k}: ცენტრიდან მასშტაბირება შეიძლება მხოლოდ kana ნაწილში +@msg no composite font კომპოზიტური შრიფტი არ არის Delete Font შრიფტის წაშლა Kanji კანძი Kana კანა diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 991daba6d0..fbb22fd589 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4001,6 +4001,42 @@ file-filter Templates 템플릿 Composite Fonts 합성 글꼴 Export Composite Fonts 합성 글꼴 내보내기 Import Composite Fonts 합성 글꼴 가져오기 +@msg each font must be {name, parts} 각 글꼴은 {name, parts} 형식이어야 합니다 +@msg unknown key `{k}`; a font takes name and parts 알 수 없는 키 `{k}`: 글꼴에는 name과 parts만 쓸 수 있습니다 +@msg each font needs a name 각 글꼴에는 이름이 필요합니다 +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}`은(는) 합성 글꼴 이름으로 쓸 수 없습니다(1~{COMPOSITE_NAME_MAX}자, 제어 문자 없음) +@msg `{name}`: parts must be an object `{name}`: parts는 객체여야 합니다 +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: 알 수 없는 클래스 `{k}`: 클래스는 kanji, kana, punctuation, symbols, roman, numerals입니다 +@msg `{name}`: {_1} must be an object `{name}`: {_1}은(는) 객체여야 합니다 +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1}에 알 수 없는 키 `{k}`이(가) 있습니다 +@msg `{name}`: give the kanji part `{name}`: kanji 부분을 지정하세요 +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 문서에는 합성 글꼴을 최대 {COMPOSITE_FONTS_MAX}개까지 둘 수 있습니다 +@msg two composite fonts are named `{_1}` 이름이 `{_1}`인 합성 글꼴이 두 개 있습니다 +@msg `{_1}` is the name of an installed font `{_1}`은(는) 설치된 글꼴의 이름입니다 +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2}이(가) 합성 글꼴인 `{font}`(으)로 설정되어 있습니다 +@msg give fonts: [{name, parts}…] fonts를 지정하세요: [{name, parts}…] +@msg path is empty path가 비어 있습니다 +@msg fonts must be a list of composite fonts fonts는 합성 글꼴 목록이어야 합니다 +@msg names must be strings names는 문자열이어야 합니다 +@msg no composite font named `{n}` 이름이 `{n}`인 합성 글꼴이 없습니다 +@msg names must be a list of names names는 이름 목록이어야 합니다 +@msg no composite fonts to export 내보낼 합성 글꼴이 없습니다 +@msg not a composite fonts file: {e} 합성 글꼴 파일이 아닙니다: {e} +@msg not a composite fonts file: no compositeFonts list 합성 글꼴 파일이 아닙니다: compositeFonts 목록이 없습니다 +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts 파일에 합성 글꼴이 {COMPOSITE_FONTS_MAX}개보다 많습니다 +@msg the file is larger than 4 MB 파일이 4MB보다 큽니다 +@msg dataBase64 is not base64 dataBase64가 base64가 아닙니다 +@msg give one of path, data or dataBase64 path, data, dataBase64 중 하나를 지정하세요 +@msg the file holds no composite fonts 파일에 합성 글꼴이 없습니다 +@msg the file holds two composite fonts named `{_1}` 파일에 이름이 `{_1}`인 합성 글꼴이 두 개 있습니다 +@msg `{_1}` can't name a composite font `{_1}`은(는) 합성 글꼴 이름으로 쓸 수 없습니다 +@msg {k}: give a font {k}: 글꼴을 지정하세요 +@msg {k}: size must be {_1} to {_2}% {k}: size는 {_1}~{_2}%여야 합니다 +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline은 {_1}~{_2}%여야 합니다 +@msg {k}: scale must be {_1} to {_2}% {k}: 비율은 {_1}~{_2}%여야 합니다 +@msg {k}: only kana, roman and numerals can be scaled {k}: 비율을 바꿀 수 있는 것은 kana, roman, numerals뿐입니다 +@msg {k}: only kana can be scaled at the center {k}: 가운데를 기준으로 비율을 바꿀 수 있는 것은 kana뿐입니다 +@msg no composite font 합성 글꼴이 없습니다 Delete Font 글꼴 삭제 Kanji 한자 Kana 가나 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index b8ca33858f..3335e94b74 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2059,6 +2059,42 @@ digits Malayalam Malayalam Composite Fonts Samengestelde lettertypen Export Composite Fonts Samengestelde lettertypen exporteren Import Composite Fonts Samengestelde lettertypen importeren +@msg each font must be {name, parts} elk lettertype moet {name, parts} zijn +@msg unknown key `{k}`; a font takes name and parts onbekende sleutel `{k}`; een lettertype neemt name en parts +@msg each font needs a name elk lettertype heeft een naam nodig +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kan geen naam van een samengesteld lettertype zijn (1 tot {COMPOSITE_NAME_MAX} tekens, geen besturingstekens) +@msg `{name}`: parts must be an object `{name}`: parts moet een object zijn +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: onbekende klasse `{k}`; de klassen zijn kanji, kana, punctuation, symbols, roman en numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} moet een object zijn +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: onbekende sleutel `{k}` in {_1} +@msg `{name}`: give the kanji part `{name}`: geef het deel kanji op +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts een document bevat hoogstens {COMPOSITE_FONTS_MAX} samengestelde lettertypen +@msg two composite fonts are named `{_1}` twee samengestelde lettertypen heten `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` is de naam van een geïnstalleerd lettertype +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} gebruikt `{font}`, een samengesteld lettertype +@msg give fonts: [{name, parts}…] geef fonts op: [{name, parts}…] +@msg path is empty path is leeg +@msg fonts must be a list of composite fonts fonts moet een lijst van samengestelde lettertypen zijn +@msg names must be strings names moeten tekenreeksen zijn +@msg no composite font named `{n}` geen samengesteld lettertype met de naam `{n}` +@msg names must be a list of names names moet een lijst van namen zijn +@msg no composite fonts to export geen samengestelde lettertypen om te exporteren +@msg not a composite fonts file: {e} geen bestand met samengestelde lettertypen: {e} +@msg not a composite fonts file: no compositeFonts list geen bestand met samengestelde lettertypen: geen lijst compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts het bestand bevat meer dan {COMPOSITE_FONTS_MAX} samengestelde lettertypen +@msg the file is larger than 4 MB het bestand is groter dan 4 MB +@msg dataBase64 is not base64 dataBase64 is geen base64 +@msg give one of path, data or dataBase64 geef path, data of dataBase64 op +@msg the file holds no composite fonts het bestand bevat geen samengestelde lettertypen +@msg the file holds two composite fonts named `{_1}` het bestand bevat twee samengestelde lettertypen met de naam `{_1}` +@msg `{_1}` can't name a composite font `{_1}` kan geen naam van een samengesteld lettertype zijn +@msg {k}: give a font {k}: geef een lettertype op +@msg {k}: size must be {_1} to {_2}% {k}: size moet tussen {_1} en {_2}% liggen +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline moet tussen {_1} en {_2}% liggen +@msg {k}: scale must be {_1} to {_2}% {k}: de schaal moet tussen {_1} en {_2}% liggen +@msg {k}: only kana, roman and numerals can be scaled {k}: alleen kana, roman en numerals kunnen worden geschaald +@msg {k}: only kana can be scaled at the center {k}: alleen kana kan vanuit het midden worden geschaald +@msg no composite font geen samengesteld lettertype Delete Font Lettertype verwijderen Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index eb5afe87cc..cb236dfcaf 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1550,6 +1550,42 @@ digits Malayalam Malajalam Composite Fonts Fonty złożone Export Composite Fonts Eksportuj fonty złożone Import Composite Fonts Importuj fonty złożone +@msg each font must be {name, parts} każdy font musi mieć postać {name, parts} +@msg unknown key `{k}`; a font takes name and parts nieznany klucz `{k}`; font przyjmuje name i parts +@msg each font needs a name każdy font musi mieć nazwę +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` nie może być nazwą fontu złożonego (od 1 do {COMPOSITE_NAME_MAX} znaków, bez znaków sterujących) +@msg `{name}`: parts must be an object `{name}`: parts musi być obiektem +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: nieznana klasa `{k}`; klasy to kanji, kana, punctuation, symbols, roman i numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} musi być obiektem +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: nieznany klucz `{k}` w {_1} +@msg `{name}`: give the kanji part `{name}`: podaj część kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts dokument może zawierać najwyżej {COMPOSITE_FONTS_MAX} fontów złożonych +@msg two composite fonts are named `{_1}` dwa fonty złożone mają nazwę `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` to nazwa zainstalowanego fontu +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} używa `{font}`, który jest fontem złożonym +@msg give fonts: [{name, parts}…] podaj fonts: [{name, parts}…] +@msg path is empty path jest puste +@msg fonts must be a list of composite fonts fonts musi być listą fontów złożonych +@msg names must be strings names muszą być ciągami znaków +@msg no composite font named `{n}` brak fontu złożonego o nazwie `{n}` +@msg names must be a list of names names musi być listą nazw +@msg no composite fonts to export brak fontów złożonych do eksportu +@msg not a composite fonts file: {e} to nie jest plik fontów złożonych: {e} +@msg not a composite fonts file: no compositeFonts list to nie jest plik fontów złożonych: brak listy compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts plik zawiera więcej niż {COMPOSITE_FONTS_MAX} fontów złożonych +@msg the file is larger than 4 MB plik jest większy niż 4 MB +@msg dataBase64 is not base64 dataBase64 nie jest w formacie base64 +@msg give one of path, data or dataBase64 podaj path, data lub dataBase64 +@msg the file holds no composite fonts plik nie zawiera fontów złożonych +@msg the file holds two composite fonts named `{_1}` plik zawiera dwa fonty złożone o nazwie `{_1}` +@msg `{_1}` can't name a composite font `{_1}` nie może być nazwą fontu złożonego +@msg {k}: give a font {k}: podaj font +@msg {k}: size must be {_1} to {_2}% {k}: size musi wynosić od {_1} do {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline musi wynosić od {_1} do {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}: skala musi wynosić od {_1} do {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}: skalować można tylko kana, roman i numerals +@msg {k}: only kana can be scaled at the center {k}: od środka można skalować tylko kana +@msg no composite font brak fontu złożonego Delete Font Usuń font Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 80615d6370..8f9a2ef218 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5181,6 +5181,42 @@ axis Both Ambos Composite Fonts Fontes compostas Export Composite Fonts Exportar fontes compostas Import Composite Fonts Importar fontes compostas +@msg each font must be {name, parts} cada fonte deve ser {name, parts} +@msg unknown key `{k}`; a font takes name and parts chave desconhecida `{k}`; uma fonte aceita name e parts +@msg each font needs a name cada fonte precisa de um nome +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` não pode nomear uma fonte composta (de 1 a {COMPOSITE_NAME_MAX} caracteres, sem caracteres de controle) +@msg `{name}`: parts must be an object `{name}`: parts deve ser um objeto +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: classe desconhecida `{k}`; as classes são kanji, kana, punctuation, symbols, roman e numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} deve ser um objeto +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: chave desconhecida `{k}` em {_1} +@msg `{name}`: give the kanji part `{name}`: informe a parte kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts um documento contém no máximo {COMPOSITE_FONTS_MAX} fontes compostas +@msg two composite fonts are named `{_1}` duas fontes compostas se chamam `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` é o nome de uma fonte instalada +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} usa `{font}`, uma fonte composta +@msg give fonts: [{name, parts}…] informe fonts: [{name, parts}…] +@msg path is empty path está vazio +@msg fonts must be a list of composite fonts fonts deve ser uma lista de fontes compostas +@msg names must be strings names deve conter strings +@msg no composite font named `{n}` nenhuma fonte composta chamada `{n}` +@msg names must be a list of names names deve ser uma lista de nomes +@msg no composite fonts to export nenhuma fonte composta para exportar +@msg not a composite fonts file: {e} não é um arquivo de fontes compostas: {e} +@msg not a composite fonts file: no compositeFonts list não é um arquivo de fontes compostas: não há lista compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts o arquivo contém mais de {COMPOSITE_FONTS_MAX} fontes compostas +@msg the file is larger than 4 MB o arquivo tem mais de 4 MB +@msg dataBase64 is not base64 dataBase64 não é base64 +@msg give one of path, data or dataBase64 informe path, data ou dataBase64 +@msg the file holds no composite fonts o arquivo não contém fontes compostas +@msg the file holds two composite fonts named `{_1}` o arquivo contém duas fontes compostas chamadas `{_1}` +@msg `{_1}` can't name a composite font `{_1}` não pode nomear uma fonte composta +@msg {k}: give a font {k}: informe uma fonte +@msg {k}: size must be {_1} to {_2}% {k}: size deve estar entre {_1} e {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline deve estar entre {_1} e {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}: a escala deve estar entre {_1} e {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}: só kana, roman e numerals podem ser escalados +@msg {k}: only kana can be scaled at the center {k}: só kana pode ser escalado a partir do centro +@msg no composite font nenhuma fonte composta Delete Font Excluir fonte Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index c830e69b37..84da770f7a 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3670,6 +3670,42 @@ digits Malayalam Малаялам Composite Fonts Составные шрифты Export Composite Fonts Экспорт составных шрифтов Import Composite Fonts Импорт составных шрифтов +@msg each font must be {name, parts} каждый шрифт должен иметь вид {name, parts} +@msg unknown key `{k}`; a font takes name and parts неизвестный ключ `{k}`; шрифт принимает name и parts +@msg each font needs a name у каждого шрифта должно быть имя +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` не может быть именем составного шрифта (от 1 до {COMPOSITE_NAME_MAX} символов, без управляющих символов) +@msg `{name}`: parts must be an object `{name}`: parts должен быть объектом +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: неизвестный класс `{k}`; классы: kanji, kana, punctuation, symbols, roman и numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} должен быть объектом +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: неизвестный ключ `{k}` в {_1} +@msg `{name}`: give the kanji part `{name}`: укажите часть kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts документ содержит не более {COMPOSITE_FONTS_MAX} составных шрифтов +@msg two composite fonts are named `{_1}` два составных шрифта называются `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` является именем установленного шрифта +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} использует `{font}`, а это составной шрифт +@msg give fonts: [{name, parts}…] укажите fonts: [{name, parts}…] +@msg path is empty path пуст +@msg fonts must be a list of composite fonts fonts должен быть списком составных шрифтов +@msg names must be strings names должны быть строками +@msg no composite font named `{n}` нет составного шрифта с именем `{n}` +@msg names must be a list of names names должен быть списком имён +@msg no composite fonts to export нет составных шрифтов для экспорта +@msg not a composite fonts file: {e} это не файл составных шрифтов: {e} +@msg not a composite fonts file: no compositeFonts list это не файл составных шрифтов: нет списка compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts в файле больше {COMPOSITE_FONTS_MAX} составных шрифтов +@msg the file is larger than 4 MB файл больше 4 МБ +@msg dataBase64 is not base64 dataBase64 не в формате base64 +@msg give one of path, data or dataBase64 укажите path, data или dataBase64 +@msg the file holds no composite fonts в файле нет составных шрифтов +@msg the file holds two composite fonts named `{_1}` в файле два составных шрифта с именем `{_1}` +@msg `{_1}` can't name a composite font `{_1}` не может быть именем составного шрифта +@msg {k}: give a font {k}: укажите шрифт +@msg {k}: size must be {_1} to {_2}% {k}: size должен быть от {_1} до {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline должен быть от {_1} до {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}: масштаб должен быть от {_1} до {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}: масштабировать можно только kana, roman и numerals +@msg {k}: only kana can be scaled at the center {k}: масштабировать от центра можно только kana +@msg no composite font нет составного шрифта Delete Font Удалить шрифт Kanji Кандзи Kana Кана diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index d4304b97bd..3994415e3a 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3560,6 +3560,42 @@ digits Malayalam Malayalam Composite Fonts Sammansatta typsnitt Export Composite Fonts Exportera sammansatta typsnitt Import Composite Fonts Importera sammansatta typsnitt +@msg each font must be {name, parts} varje typsnitt måste vara {name, parts} +@msg unknown key `{k}`; a font takes name and parts okänd nyckel `{k}`; ett typsnitt tar name och parts +@msg each font needs a name varje typsnitt behöver ett namn +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kan inte vara namnet på ett sammansatt typsnitt (1 till {COMPOSITE_NAME_MAX} tecken, inga styrtecken) +@msg `{name}`: parts must be an object `{name}`: parts måste vara ett objekt +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: okänd klass `{k}`; klasserna är kanji, kana, punctuation, symbols, roman och numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} måste vara ett objekt +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: okänd nyckel `{k}` i {_1} +@msg `{name}`: give the kanji part `{name}`: ange delen kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts ett dokument har högst {COMPOSITE_FONTS_MAX} sammansatta typsnitt +@msg two composite fonts are named `{_1}` två sammansatta typsnitt heter `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` är namnet på ett installerat typsnitt +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} använder `{font}`, ett sammansatt typsnitt +@msg give fonts: [{name, parts}…] ange fonts: [{name, parts}…] +@msg path is empty path är tom +@msg fonts must be a list of composite fonts fonts måste vara en lista med sammansatta typsnitt +@msg names must be strings names måste vara strängar +@msg no composite font named `{n}` inget sammansatt typsnitt heter `{n}` +@msg names must be a list of names names måste vara en lista med namn +@msg no composite fonts to export inga sammansatta typsnitt att exportera +@msg not a composite fonts file: {e} inte en fil med sammansatta typsnitt: {e} +@msg not a composite fonts file: no compositeFonts list inte en fil med sammansatta typsnitt: ingen lista compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts filen innehåller fler än {COMPOSITE_FONTS_MAX} sammansatta typsnitt +@msg the file is larger than 4 MB filen är större än 4 MB +@msg dataBase64 is not base64 dataBase64 är inte base64 +@msg give one of path, data or dataBase64 ange path, data eller dataBase64 +@msg the file holds no composite fonts filen innehåller inga sammansatta typsnitt +@msg the file holds two composite fonts named `{_1}` filen innehåller två sammansatta typsnitt som heter `{_1}` +@msg `{_1}` can't name a composite font `{_1}` kan inte vara namnet på ett sammansatt typsnitt +@msg {k}: give a font {k}: ange ett typsnitt +@msg {k}: size must be {_1} to {_2}% {k}: size måste vara {_1} till {_2} % +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline måste vara {_1} till {_2} % +@msg {k}: scale must be {_1} to {_2}% {k}: skalan måste vara {_1} till {_2} % +@msg {k}: only kana, roman and numerals can be scaled {k}: bara kana, roman och numerals kan skalas +@msg {k}: only kana can be scaled at the center {k}: bara kana kan skalas från mitten +@msg no composite font inget sammansatt typsnitt Delete Font Ta bort typsnitt Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index d7f2c8a2ef..72f9a4db10 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -1801,6 +1801,81 @@ fn recolor_notes_on_objects_left_unchanged_translate_with_their_counts() { } } +/// Every complete-message catalog has a row for each error that `type.compositeFonts.set`, +/// `.export` and `.import`, the composite font checks behind them and the Composite Fonts dialog +/// give, and translates them with the font name, the key and the class key kept, and the reason +/// after a font's name translated in turn. +#[test] +fn composite_font_errors_translate_in_complete_languages() { + const SOURCES: &[&str] = &[ + "each font must be {name, parts}", + "unknown key `{k}`; a font takes name and parts", + "each font needs a name", + "`{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters)", + "`{name}`: parts must be an object", + "`{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals", + "`{name}`: {_1} must be an object", + "`{name}`: unknown key `{k}` in {_1}", + "`{name}`: give the kanji part", + "a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts", + "two composite fonts are named `{_1}`", + "`{_1}` is the name of an installed font", + "`{_1}`: {_2} is set in `{font}`, a composite font", + "give fonts: [{name, parts}…]", + "path is empty", + "fonts must be a list of composite fonts", + "names must be strings", + "no composite font named `{n}`", + "names must be a list of names", + "no composite fonts to export", + "not a composite fonts file: {e}", + "not a composite fonts file: no compositeFonts list", + "the file holds more than {COMPOSITE_FONTS_MAX} composite fonts", + "the file is larger than 4 MB", + "dataBase64 is not base64", + "give one of path, data or dataBase64", + "the file holds no composite fonts", + "the file holds two composite fonts named `{_1}`", + "`{_1}` can't name a composite font", + "{k}: give a font", + "{k}: size must be {_1} to {_2}%", + "{k}: baseline must be {_1} to {_2}%", + "{k}: scale must be {_1} to {_2}%", + "{k}: only kana, roman and numerals can be scaled", + "{k}: only kana can be scaled at the center", + "no composite font", + ]; + // An error, English words its translation replaces, and the text it keeps. + let cases: &[(&str, &str, &[&str])] = &[ + ( + "`Mincho Mix`: unknown class `kanjj`; the classes are kanji, kana, punctuation, symbols, roman and numerals", + "the classes are", + &["`Mincho Mix`", "`kanjj`", "kanji", "kana", "punctuation", "symbols", "roman", "numerals"], + ), + ("`Mincho Mix`: give the kanji part", "give the", &["`Mincho Mix`", "kanji"]), + ("`Mincho Mix`: parts must be an object", "must be an object", &["`Mincho Mix`", "parts"]), + ("`Mincho Mix`: kana must be an object", "must be an object", &["`Mincho Mix`", "kana"]), + ("`Mincho Mix`: unknown key `weight` in roman", "unknown key", &["`Mincho Mix`", "`weight`", "roman"]), + ("two composite fonts are named `Mincho Mix`", "are named", &["`Mincho Mix`"]), + ("`Mincho Mix`: kana: give a font", "give a font", &["`Mincho Mix`", "kana"]), + ("not a composite fonts file: the file is larger than 4 MB", "larger than", &["4"]), + ]; + for code in COMPLETE_MESSAGES { + let lang = Lang::from_code(code).expect("registered"); + let (entries, _) = parse_entries(lang.0.source); + let rows: Vec<&str> = entries.iter().filter(|(ctx, _, _)| ctx == "@msg").map(|(_, src, _)| src.as_str()).collect(); + let missing: Vec<&&str> = SOURCES.iter().filter(|s| !rows.contains(s)).collect(); + assert!(missing.is_empty(), "{code} has no row for {missing:?}"); + for (error, english, kept) in cases { + let shown = message(lang, error); + assert!(!shown.contains(english), "{code}: {shown}"); + for k in *kept { + assert!(shown.contains(k), "{code}: {k} in {shown}"); + } + } + } +} + #[test] fn package_status_message_templates_parse() { let c = Catalog::parse(concat!( @@ -3542,8 +3617,13 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[ ("column must be true or a number from 1", &["column"]), ("column: true floats the whole column and takes no onto, above or below", &["column", "onto", "above", "below"]), ("control must be fillStroke, coloring, drawMode or screenMode", &["control", "fillStroke", "coloring", "drawMode", "screenMode"]), + ("dataBase64 is not base64", &["dataBase64"]), ("decimals must be {_1}–{_2} (got {_3})", &["decimals"]), ("detect must be true or false", &["detect"]), + ("each font must be {name, parts}", &["name", "parts"]), + ("fonts must be a list of composite fonts", &["fonts"]), + ("give fonts: [{name, parts}…]", &["fonts", "name", "parts"]), + ("give one of path, data or dataBase64", &["path", "data", "dataBase64"]), ("give origin: [x, y], or reset: true", &["origin", "reset"]), ("global must be true or false", &["global"]), ("group must be true or false", &["group"]), @@ -3562,12 +3642,16 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[ ("member is for order front, forward, backward or back", &["member", "order", "front", "forward", "backward", "back"]), ("mode is set, add or toggle, not `{other}`", &["mode", "set", "add", "toggle"]), ("names is empty", &["names"]), + ("names must be a list of names", &["names"]), + ("names must be strings", &["names"]), + ("not a composite fonts file: no compositeFonts list", &["compositeFonts"]), ("on must be true or false", &["on"]), ("onto must name a floating panel", &["onto"]), ("others must be true or false", &["others"]), ("page must be a whole number from 1", &["page"]), ("panel is not in a dock group", &["panel"]), ("panel must name a registered panel", &["panel"]), + ("path is empty", &["path"]), ("paths must be strings", &["paths"]), ("percent must be above 0 and at most 6400, not {p}", &["percent"]), ("plane must be left, right or ground", &["plane", "left", "right", "ground"]), @@ -3583,6 +3667,7 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[ ("span: \"selection\" places a linear, radial, conic or diamond gradient, not a freeform one", &["span"]), ("thickness must be from 0.01 to 5000 pt", &["thickness"]), ("title can't be empty", &["title"]), + ("unknown key `{k}`; a font takes name and parts", &["name", "parts"]), ( "unknown light setting: use color, intensity, rotation, height, softness or behind", &["color", "intensity", "rotation", "height", "softness", "behind"], @@ -3595,6 +3680,16 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[ ("`strokeMode` applies to strokes (stroke: true)", &["stroke"]), ("`{GRAPHICS}` entries take symbol, face, x, y, scale and rotation, not `{k}`", &["symbol", "face", "x", "y", "scale", "rotation"]), ("`{GRAPHICS}` face must be front or back", &["face", "front", "back"]), + ("`{name}`: give the kanji part", &["kanji"]), + ("`{name}`: parts must be an object", &["parts"]), + ( + "`{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals", + &["kanji", "kana", "punctuation", "symbols", "roman", "numerals"], + ), + ("{k}: baseline must be {_1} to {_2}%", &["baseline"]), + ("{k}: only kana can be scaled at the center", &["kana"]), + ("{k}: only kana, roman and numerals can be scaled", &["kana", "roman", "numerals"]), + ("{k}: size must be {_1} to {_2}%", &["size"]), ]; /// Every catalog's translation of a [`NAMED_KEYS`] message keeps each name the table lists, as diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f5fc694ec2..b61389b633 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3449,6 +3449,42 @@ digits Malayalam Malayalam Composite Fonts Bileşik Fontlar Export Composite Fonts Bileşik Fontları Dışa Aktar Import Composite Fonts Bileşik Fontları İçe Aktar +@msg each font must be {name, parts} her font {name, parts} biçiminde olmalı +@msg unknown key `{k}`; a font takes name and parts bilinmeyen anahtar `{k}`; bir font name ve parts alır +@msg each font needs a name her fontun bir adı olmalı +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` bir bileşik fontun adı olamaz (1 ile {COMPOSITE_NAME_MAX} karakter arası, denetim karakteri olmadan) +@msg `{name}`: parts must be an object `{name}`: parts bir nesne olmalı +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: bilinmeyen sınıf `{k}`; sınıflar: kanji, kana, punctuation, symbols, roman ve numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} bir nesne olmalı +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: {_1} içinde bilinmeyen anahtar `{k}` +@msg `{name}`: give the kanji part `{name}`: kanji bölümünü verin +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts bir belge en fazla {COMPOSITE_FONTS_MAX} bileşik font tutar +@msg two composite fonts are named `{_1}` iki bileşik fontun adı `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` yüklü bir fontun adı +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2}, bir bileşik font olan `{font}` ile ayarlı +@msg give fonts: [{name, parts}…] fonts verin: [{name, parts}…] +@msg path is empty path boş +@msg fonts must be a list of composite fonts fonts bir bileşik font listesi olmalı +@msg names must be strings names dizeler olmalı +@msg no composite font named `{n}` `{n}` adlı bileşik font yok +@msg names must be a list of names names bir ad listesi olmalı +@msg no composite fonts to export dışa aktarılacak bileşik font yok +@msg not a composite fonts file: {e} bileşik font dosyası değil: {e} +@msg not a composite fonts file: no compositeFonts list bileşik font dosyası değil: compositeFonts listesi yok +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts dosyada {COMPOSITE_FONTS_MAX} taneden fazla bileşik font var +@msg the file is larger than 4 MB dosya 4 MB'tan büyük +@msg dataBase64 is not base64 dataBase64 base64 değil +@msg give one of path, data or dataBase64 path, data veya dataBase64 değerlerinden birini verin +@msg the file holds no composite fonts dosyada bileşik font yok +@msg the file holds two composite fonts named `{_1}` dosyada `{_1}` adlı iki bileşik font var +@msg `{_1}` can't name a composite font `{_1}` bir bileşik fontun adı olamaz +@msg {k}: give a font {k}: bir font verin +@msg {k}: size must be {_1} to {_2}% {k}: size %{_1} ile %{_2} arasında olmalı +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline %{_1} ile %{_2} arasında olmalı +@msg {k}: scale must be {_1} to {_2}% {k}: ölçek %{_1} ile %{_2} arasında olmalı +@msg {k}: only kana, roman and numerals can be scaled {k}: yalnızca kana, roman ve numerals ölçeklenebilir +@msg {k}: only kana can be scaled at the center {k}: merkezden yalnızca kana ölçeklenebilir +@msg no composite font bileşik font yok Delete Font Fontu Sil Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 5a36512db0..361401931f 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3677,6 +3677,42 @@ effect Points Точки Composite Fonts Складені шрифти Export Composite Fonts Експорт складених шрифтів Import Composite Fonts Імпорт складених шрифтів +@msg each font must be {name, parts} кожен шрифт має бути у вигляді {name, parts} +@msg unknown key `{k}`; a font takes name and parts невідомий ключ `{k}`; шрифт приймає name і parts +@msg each font needs a name кожен шрифт повинен мати назву +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` не може бути назвою складеного шрифту (від 1 до {COMPOSITE_NAME_MAX} символів, без керівних символів) +@msg `{name}`: parts must be an object `{name}`: parts має бути об’єктом +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: невідомий клас `{k}`; класи: kanji, kana, punctuation, symbols, roman і numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} має бути об’єктом +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: невідомий ключ `{k}` у {_1} +@msg `{name}`: give the kanji part `{name}`: вкажіть частину kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts документ містить щонайбільше {COMPOSITE_FONTS_MAX} складених шрифтів +@msg two composite fonts are named `{_1}` два складені шрифти мають назву `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` є назвою встановленого шрифту +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} використовує `{font}`, а це складений шрифт +@msg give fonts: [{name, parts}…] вкажіть fonts: [{name, parts}…] +@msg path is empty path порожній +@msg fonts must be a list of composite fonts fonts має бути списком складених шрифтів +@msg names must be strings names мають бути рядками +@msg no composite font named `{n}` немає складеного шрифту з назвою `{n}` +@msg names must be a list of names names має бути списком назв +@msg no composite fonts to export немає складених шрифтів для експорту +@msg not a composite fonts file: {e} це не файл складених шрифтів: {e} +@msg not a composite fonts file: no compositeFonts list це не файл складених шрифтів: немає списку compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts у файлі понад {COMPOSITE_FONTS_MAX} складених шрифтів +@msg the file is larger than 4 MB файл більший за 4 МБ +@msg dataBase64 is not base64 dataBase64 не у форматі base64 +@msg give one of path, data or dataBase64 вкажіть path, data або dataBase64 +@msg the file holds no composite fonts у файлі немає складених шрифтів +@msg the file holds two composite fonts named `{_1}` у файлі два складені шрифти з назвою `{_1}` +@msg `{_1}` can't name a composite font `{_1}` не може бути назвою складеного шрифту +@msg {k}: give a font {k}: вкажіть шрифт +@msg {k}: size must be {_1} to {_2}% {k}: size має бути від {_1} до {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline має бути від {_1} до {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}: масштаб має бути від {_1} до {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}: масштабувати можна лише kana, roman і numerals +@msg {k}: only kana can be scaled at the center {k}: масштабувати від центру можна лише kana +@msg no composite font немає складеного шрифту Delete Font Видалити шрифт Kanji Кандзі Kana Кана diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 839f5c7405..0a7d8e8776 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4022,6 +4022,42 @@ mark-style Japanese Kiểu Nhật Composite Fonts Phông chữ tổ hợp Export Composite Fonts Xuất phông chữ tổ hợp Import Composite Fonts Nhập phông chữ tổ hợp +@msg each font must be {name, parts} mỗi phông chữ phải là {name, parts} +@msg unknown key `{k}`; a font takes name and parts khóa không xác định `{k}`; một phông chữ nhận name và parts +@msg each font needs a name mỗi phông chữ cần có tên +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` không thể là tên phông chữ tổ hợp (từ 1 đến {COMPOSITE_NAME_MAX} ký tự, không có ký tự điều khiển) +@msg `{name}`: parts must be an object `{name}`: parts phải là một đối tượng +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: lớp không xác định `{k}`; các lớp là kanji, kana, punctuation, symbols, roman và numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} phải là một đối tượng +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: khóa không xác định `{k}` trong {_1} +@msg `{name}`: give the kanji part `{name}`: hãy cung cấp phần kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts một tài liệu chứa tối đa {COMPOSITE_FONTS_MAX} phông chữ tổ hợp +@msg two composite fonts are named `{_1}` có hai phông chữ tổ hợp cùng tên `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` là tên của một phông chữ đã cài đặt +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} dùng `{font}`, một phông chữ tổ hợp +@msg give fonts: [{name, parts}…] hãy cung cấp fonts: [{name, parts}…] +@msg path is empty path trống +@msg fonts must be a list of composite fonts fonts phải là danh sách phông chữ tổ hợp +@msg names must be strings names phải là chuỗi +@msg no composite font named `{n}` không có phông chữ tổ hợp nào tên `{n}` +@msg names must be a list of names names phải là danh sách tên +@msg no composite fonts to export không có phông chữ tổ hợp nào để xuất +@msg not a composite fonts file: {e} không phải tệp phông chữ tổ hợp: {e} +@msg not a composite fonts file: no compositeFonts list không phải tệp phông chữ tổ hợp: không có danh sách compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts tệp chứa hơn {COMPOSITE_FONTS_MAX} phông chữ tổ hợp +@msg the file is larger than 4 MB tệp lớn hơn 4 MB +@msg dataBase64 is not base64 dataBase64 không phải base64 +@msg give one of path, data or dataBase64 hãy cung cấp path, data hoặc dataBase64 +@msg the file holds no composite fonts tệp không chứa phông chữ tổ hợp nào +@msg the file holds two composite fonts named `{_1}` tệp chứa hai phông chữ tổ hợp cùng tên `{_1}` +@msg `{_1}` can't name a composite font `{_1}` không thể là tên phông chữ tổ hợp +@msg {k}: give a font {k}: hãy cung cấp một phông chữ +@msg {k}: size must be {_1} to {_2}% {k}: size phải từ {_1} đến {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline phải từ {_1} đến {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}: tỷ lệ phải từ {_1} đến {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}: chỉ kana, roman và numerals mới co giãn được +@msg {k}: only kana can be scaled at the center {k}: chỉ kana mới co giãn từ tâm được +@msg no composite font không có phông chữ tổ hợp Delete Font Xóa phông chữ Kanji Kanji Kana Kana diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b0d6001e32..2a2c0356e8 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3499,6 +3499,42 @@ digits Malayalam 马拉雅拉姆数字 Composite Fonts 复合字体 Export Composite Fonts 导出复合字体 Import Composite Fonts 导入复合字体 +@msg each font must be {name, parts} 每个字体都必须是 {name, parts} +@msg unknown key `{k}`; a font takes name and parts 未知键 `{k}`;字体可用的键为 name 和 parts +@msg each font needs a name 每个字体都需要名称 +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` 不能用作复合字体的名称(1 到 {COMPOSITE_NAME_MAX} 个字符,不含控制字符) +@msg `{name}`: parts must be an object `{name}`:parts 必须是对象 +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`:未知类别 `{k}`;类别为 kanji、kana、punctuation、symbols、roman 和 numerals +@msg `{name}`: {_1} must be an object `{name}`:{_1} 必须是对象 +@msg `{name}`: unknown key `{k}` in {_1} `{name}`:{_1} 中有未知键 `{k}` +@msg `{name}`: give the kanji part `{name}`:请指定 kanji 部分 +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 一个文档最多保存 {COMPOSITE_FONTS_MAX} 个复合字体 +@msg two composite fonts are named `{_1}` 有两个复合字体都名为 `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` 是已安装字体的名称 +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`:{_2} 使用了复合字体 `{font}` +@msg give fonts: [{name, parts}…] 请指定 fonts:[{name, parts}…] +@msg path is empty path 为空 +@msg fonts must be a list of composite fonts fonts 必须是复合字体列表 +@msg names must be strings names 必须是字符串 +@msg no composite font named `{n}` 没有名为 `{n}` 的复合字体 +@msg names must be a list of names names 必须是名称列表 +@msg no composite fonts to export 没有可导出的复合字体 +@msg not a composite fonts file: {e} 不是复合字体文件:{e} +@msg not a composite fonts file: no compositeFonts list 不是复合字体文件:没有 compositeFonts 列表 +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts 文件包含超过 {COMPOSITE_FONTS_MAX} 个复合字体 +@msg the file is larger than 4 MB 文件大于 4 MB +@msg dataBase64 is not base64 dataBase64 不是 base64 +@msg give one of path, data or dataBase64 请指定 path、data 或 dataBase64 之一 +@msg the file holds no composite fonts 文件不含复合字体 +@msg the file holds two composite fonts named `{_1}` 文件中有两个复合字体都名为 `{_1}` +@msg `{_1}` can't name a composite font `{_1}` 不能用作复合字体的名称 +@msg {k}: give a font {k}:请指定字体 +@msg {k}: size must be {_1} to {_2}% {k}:size 必须为 {_1}% 到 {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}:baseline 必须为 {_1}% 到 {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}:缩放必须为 {_1}% 到 {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}:只有 kana、roman 和 numerals 可以缩放 +@msg {k}: only kana can be scaled at the center {k}:只有 kana 可以从中心缩放 +@msg no composite font 没有复合字体 Delete Font 删除字体 Kanji 汉字 Kana 假名 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index c24ae37609..5d7fd456ed 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4738,6 +4738,42 @@ digits Malayalam 馬來亞拉姆數字 Composite Fonts 複合字體 Export Composite Fonts 匯出複合字體 Import Composite Fonts 匯入複合字體 +@msg each font must be {name, parts} 每個字體都必須是 {name, parts} +@msg unknown key `{k}`; a font takes name and parts 未知的鍵 `{k}`;字體可用的鍵為 name 和 parts +@msg each font needs a name 每個字體都需要名稱 +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` 不能作為複合字體的名稱(1 到 {COMPOSITE_NAME_MAX} 個字元,不含控制字元) +@msg `{name}`: parts must be an object `{name}`:parts 必須是物件 +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`:未知的類別 `{k}`;類別為 kanji、kana、punctuation、symbols、roman 和 numerals +@msg `{name}`: {_1} must be an object `{name}`:{_1} 必須是物件 +@msg `{name}`: unknown key `{k}` in {_1} `{name}`:{_1} 中有未知的鍵 `{k}` +@msg `{name}`: give the kanji part `{name}`:請指定 kanji 部分 +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts 一份文件最多保存 {COMPOSITE_FONTS_MAX} 個複合字體 +@msg two composite fonts are named `{_1}` 有兩個複合字體都名為 `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` 是已安裝字體的名稱 +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`:{_2} 使用了複合字體 `{font}` +@msg give fonts: [{name, parts}…] 請指定 fonts:[{name, parts}…] +@msg path is empty path 為空 +@msg fonts must be a list of composite fonts fonts 必須是複合字體清單 +@msg names must be strings names 必須是字串 +@msg no composite font named `{n}` 沒有名為 `{n}` 的複合字體 +@msg names must be a list of names names 必須是名稱清單 +@msg no composite fonts to export 沒有可匯出的複合字體 +@msg not a composite fonts file: {e} 不是複合字體檔案:{e} +@msg not a composite fonts file: no compositeFonts list 不是複合字體檔案:沒有 compositeFonts 清單 +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts 檔案包含超過 {COMPOSITE_FONTS_MAX} 個複合字體 +@msg the file is larger than 4 MB 檔案大於 4 MB +@msg dataBase64 is not base64 dataBase64 不是 base64 +@msg give one of path, data or dataBase64 請指定 path、data 或 dataBase64 其中之一 +@msg the file holds no composite fonts 檔案不含複合字體 +@msg the file holds two composite fonts named `{_1}` 檔案中有兩個複合字體都名為 `{_1}` +@msg `{_1}` can't name a composite font `{_1}` 不能作為複合字體的名稱 +@msg {k}: give a font {k}:請指定字體 +@msg {k}: size must be {_1} to {_2}% {k}:size 必須為 {_1}% 到 {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}:baseline 必須為 {_1}% 到 {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}:縮放必須為 {_1}% 到 {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}:只有 kana、roman 和 numerals 可以縮放 +@msg {k}: only kana can be scaled at the center {k}:只有 kana 可以從中心縮放 +@msg no composite font 沒有複合字體 Delete Font 刪除字體 Kanji 漢字 Kana 假名 From 0c3bc39009e0b0a3f6920943da1d2ce9ed822504 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 20:13:21 -0800 Subject: [PATCH 02/28] .ai save: type in an envelope stays editable An envelope whose contents hold type was written to the editing data as its distorted art, so the type opened as outlines. The type is now written as type in the envelope's contents, in the form the .ai reader opens as a live envelope of type (TypedEnvelope in crates/eps/src/import/ai/envelope.rs), with Edit Contents off or on. An envelope whose type is threaded, has text wrap options or is written as its outlines (mirrored type, type in a symbol's art) is still written as its distorted art, with the warning. A record taken back for the object limit or for outlined type also takes its stories out of the text document. The writer charges one more object for each text object in the dictionary's contents, where the reader reads a copy of it. --- crates/eps/src/ai_editing/tests.rs | 191 +++++++++++++++++- crates/eps/src/ai_editing/writer.rs | 27 ++- crates/eps/src/ai_editing/writer/envelopes.rs | 49 ++++- crates/eps/src/ai_editing/writer/texts.rs | 27 +++ crates/eps/src/import.rs | 4 +- docs/target-app-parity.md | 3 +- 6 files changed, 275 insertions(+), 26 deletions(-) diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index 967c982ad4..1f47e7276c 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -9687,7 +9687,8 @@ fn an_envelope_is_written_in_the_order_the_reader_reads_it() { } /// Envelopes the reader would open in another shape, or not at all, are written as their distorted -/// art with a warning, and open as that art: an envelope of type, one in a turned frame, a warp drawn +/// art with a warning, and open as that art: an envelope of type with text wrap options, one of +/// mirrored type (written as its outlines), one in a turned frame, a warp drawn /// without its style's net, a mesh the reader would lay out the other way round, a top object /// whose sides don't pair up, a Fidelity that isn't a whole number and bounds margins kept with /// Distort Appearance off. Other live objects keep their own warning. @@ -9702,10 +9703,24 @@ fn envelopes_the_reader_cannot_open_live_are_written_as_their_art() { f(&mut doc.node_mut(id).unwrap().kind); doc }; - let typed = change(&|k| { + let type_node = |id: u64| { + let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(50.0, 50.0), "Type", vectorcraft_doc::CharStyle::default()); + Node::new(vectorcraft_doc::NodeId(id), NodeKind::Text(Box::new(t))) + }; + let wrapped_type = change(&|k| { if let NodeKind::Envelope { content, .. } = k { - let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(50.0, 50.0), "Type", vectorcraft_doc::CharStyle::default()); - content.push(Arc::new(Node::new(vectorcraft_doc::NodeId(9_000), NodeKind::Text(Box::new(t))))); + let mut t = type_node(9_000); + t.extras_mut().wrap = Some(vectorcraft_doc::TextWrap { offset: 6.0, invert: false }); + content.push(Arc::new(t)); + } + }); + let mirrored_type = change(&|k| { + if let NodeKind::Envelope { content, .. } = k { + let mut t = type_node(9_001); + if let NodeKind::Text(t) = &mut t.kind { + t.xf = Affine::new([-1.0, 0.0, 0.0, 1.0, 50.0, 50.0]); + } + content.push(Arc::new(t)); } }); let turned = change(&|k| { @@ -9749,7 +9764,8 @@ fn envelopes_the_reader_cannot_open_live_are_written_as_their_art() { } }); let cases = [ - ("type", typed), + ("type with text wrap options", wrapped_type), + ("mirrored type, written as its outlines", mirrored_type), ("turned", turned), ("old warp", old_warp), ("sideways mesh", sideways), @@ -9774,6 +9790,171 @@ fn transpose(points: Vec, rows: usize, cols: usize) -> Vec Node { + let mut envelope = envelope_node(doc, kind, 50.0, editing, vectorcraft_doc::live::EnvelopeOptions::NEW); + let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(104.0, 62.0), "Type", style(24.0, Color::rgb(0.0, 0.0, 1.0))); + let text = text_node(doc, t); + let NodeKind::Envelope { content, .. } = &mut envelope.kind else { unreachable_envelope() }; + content.push(Arc::new(text)); + envelope +} + +/// A 200 × 100 document of `envelope` alone. +fn alone(envelope: Node) -> Document { + let mut doc = Document::new(200.0, 100.0); + let layer = doc.layers[0].id; + doc.insert(Some(layer), usize::MAX, envelope).unwrap(); + doc +} + +/// `doc` written, then read back through its editing copy beside a page that draws as `doc` does, +/// its live objects as their art: the objects on its first layer, the writer's warnings and the +/// reader's notes. +fn read_with_page(doc: &Document) -> (Vec>, Vec, Vec) { + let (p, warnings) = private_data(doc, &opts()).unwrap(); + let mut page = doc.clone(); + let ids: Vec = page.layers[0].children().into_iter().flatten().map(|n| n.id).collect(); + for id in ids { + if let Some(n) = page.node_mut(id) { + *n = vectorcraft_effects::expand_live_deep(Some(doc), n); + } + } + let (read, notes, layers) = crate::layered_ai(&p.data, page, vec![], false, false, 1.0); + assert!(layers, "{notes:?}"); + (read.layers[0].children().cloned().unwrap_or_default(), warnings, notes) +} + +/// The type in `nodes` and their groups, with whether it shows. +fn type_in(nodes: &[Arc]) -> Vec<(String, bool)> { + let mut out = vec![]; + for n in nodes { + match &n.kind { + NodeKind::Text(t) => out.push((t.plain_text(), n.visible)), + NodeKind::Group { children, .. } => out.extend(type_in(children)), + _ => {} + } + } + out +} + +/// An envelope whose contents hold type keeps the type as type: a warp, a mesh and a top object +/// envelope, with Edit Contents off and on, open as the same live envelope with the type shown +/// and editable in its contents beside the rest of them, no copy of the type left on the layer, +/// and again when saved as they opened. +#[test] +fn an_envelope_of_type_is_read_back_live_with_its_type() { + for (name, kind) in live_envelope_kinds() { + for editing in [false, true] { + let what = format!("{name}, editing {editing}"); + let mut doc = Document::new(200.0, 100.0); + let mut envelope = typed_envelope_node(&mut doc, kind.clone(), editing); + for round in ["saved", "saved again"] { + doc = alone(envelope); + let (top, warnings, notes) = read_with_page(&doc); + assert!(!warnings.iter().any(|w| w == writer::ENVELOPES || w == writer::TYPE), "{what}, {round}: {warnings:?}"); + assert!( + !notes.iter().any(|n| n == crate::import::ENVELOPE_TYPE_DRAWN || n == crate::import::ENVELOPE_DRAWN), + "{what}, {round}: {notes:?}" + ); + let [back] = top.as_slice() else { panic!("{what}, {round}: {top:#?} {notes:?}") }; + let NodeKind::Envelope { content, kind: got, editing: e, fidelity, .. } = &back.kind else { + panic!("{what}, {round}: {back:#?} {notes:?}") + }; + assert_eq!((*e, *fidelity), (editing, 50.0), "{what}, {round}"); + assert!(same_envelope_kind(&kind, got), "{what}, {round}: {got:?}"); + assert_eq!(back.name.as_deref(), Some("Bent"), "{what}, {round}"); + assert_eq!(type_in(content), [("Type".to_string(), true)], "{what}, {round}"); + let colors: Vec> = + content.iter().filter(|c| !matches!(c.kind, NodeKind::Text(_))).map(|c| c.appearance.fill_paint().color()).collect(); + assert_eq!(colors, [Some(Color::rgb(1.0, 0.0, 0.0)), Some(Color::rgb(0.0, 0.0, 1.0))], "{what}, {round}"); + envelope = (**back).clone(); + } + } + } +} + +/// An envelope of type is written live with room for every object the reader reads of it, the +/// copy of its type it reads in the envelope's dictionary included. Written as its art, it takes +/// its type's story out of the text document with it: where its record doesn't fit within the +/// object limit, and where other type in its contents is written as its outlines (mirrored type). +/// The file then has no text document. +#[test] +fn an_envelope_of_type_written_as_its_art_leaves_no_story() { + use vectorcraft_doc::live::EnvelopeKind; + let kind = EnvelopeKind::Mesh { rows: 2, cols: 3, points: envelope_points(2, 3), handles: vec![] }; + for editing in [false, true] { + let mut doc = Document::new(200.0, 100.0); + let envelope = typed_envelope_node(&mut doc, kind.clone(), editing); + let doc = alone(envelope); + let program = export_ai_editing(&doc, &opts()).unwrap().program; + let all = String::from_utf8_lossy(&program).replace("\r\n", "\n"); + assert!(all.contains("AIDeformOptionsKey") && all.contains("%AI11_BeginTextDocument"), "editing {editing}: {all}"); + let read = crate::import::objects_read(&program).unwrap(); + let out = export_within(&doc, &opts(), read).unwrap(); + assert!(!out.warnings.iter().any(|w| w == writer::OBJECT_LIMIT), "editing {editing}: {:?}", out.warnings); + let out = export_within(&doc, &opts(), read - 1).unwrap(); + let short = String::from_utf8_lossy(&out.program).replace("\r\n", "\n"); + assert!(out.warnings.iter().any(|w| w == writer::OBJECT_LIMIT), "editing {editing}: {:?}", out.warnings); + assert!(!short.contains("AIDeformOptionsKey") && !short.contains("%AI11_BeginTextDocument"), "editing {editing}: {short}"); + } + + let mut doc = Document::new(200.0, 100.0); + let mut envelope = typed_envelope_node(&mut doc, kind, false); + let mut t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(50.0, 40.0), "Back", style(12.0, Color::BLACK)); + t.xf = Affine::new([-1.0, 0.0, 0.0, 1.0, 50.0, 40.0]); + let mirrored = text_node(&mut doc, t); + let NodeKind::Envelope { content, .. } = &mut envelope.kind else { unreachable_envelope() }; + content.push(Arc::new(mirrored)); + let out = export_ai_editing(&alone(envelope), &opts()).unwrap(); + let text = String::from_utf8_lossy(&out.program).replace("\r\n", "\n"); + assert!(out.warnings.iter().any(|w| w == writer::ENVELOPES) && !out.warnings.iter().any(|w| w == writer::TYPE), "{:?}", out.warnings); + assert!(!text.contains("AIDeformOptionsKey") && !text.contains("%AI11_BeginTextDocument"), "{text}"); +} + +/// Type in an envelope that is a symbol's art, or a frame of a thread, keeps the envelope written +/// as its art: the reader would open its type as drawn art. +#[test] +fn an_envelope_of_type_in_a_symbol_or_a_thread_is_written_as_its_art() { + use vectorcraft_doc::live::EnvelopeKind; + use vectorcraft_doc::{TextKind, TextObject}; + let kind = EnvelopeKind::Mesh { rows: 2, cols: 3, points: envelope_points(2, 3), handles: vec![] }; + let mut doc = Document::new(200.0, 100.0); + let art = typed_envelope_node(&mut doc, kind.clone(), false); + doc.symbols.push(Symbol { dynamic: false, name: "Bent".into(), art: Arc::new(art) }); + let placed = instance(&mut doc, "Bent", Affine::translate((10.0, 5.0))); + let layer = doc.layers[0].id; + doc.insert(Some(layer), usize::MAX, placed).unwrap(); + let (p, warnings) = private_data(&doc, &opts()).unwrap(); + assert!(warnings.iter().any(|w| w == writer::ENVELOPES), "{warnings:?}"); + let (back, notes) = crate::ai_alone(&p.data).unwrap(); + let symbol = back.symbols.iter().find(|s| s.name == "Bent").unwrap_or_else(|| panic!("{:?} {notes:?}", back.symbols)); + let mut live = 0; + symbol.art.walk(&mut |n| live += usize::from(matches!(n.kind, NodeKind::Envelope { .. }))); + assert_eq!(live, 0, "{notes:?}"); + + let mut doc = Document::new(200.0, 100.0); + let mut envelope = envelope_node(&mut doc, kind, 50.0, false, vectorcraft_doc::live::EnvelopeOptions::NEW); + let area = |r: Rect, s: &str| { + let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, s, style(10.0, Color::BLACK)); + (t.kind, t.xf) = (TextKind::Area { frame: shapes::rectangle(r) }, Affine::IDENTITY); + t + }; + let first = text_node(&mut doc, area(Rect::new(50.0, 30.0, 150.0, 70.0), "One two three four")); + let second = text_node(&mut doc, area(Rect::new(10.0, 85.0, 190.0, 95.0), "")); + doc.text_threads.push(vec![first.id, second.id]); + let NodeKind::Envelope { content, .. } = &mut envelope.kind else { unreachable_envelope() }; + content.push(Arc::new(first)); + let layer = doc.layers[0].id; + doc.insert(Some(layer), usize::MAX, envelope).unwrap(); + doc.insert(Some(layer), usize::MAX, second).unwrap(); + let text = program(&doc); + assert!(!text.contains("AIDeformOptionsKey"), "{text}"); + let warnings = export_ai_editing(&doc, &opts()).unwrap().warnings; + assert!(warnings.iter().any(|w| w == writer::ENVELOPES), "{warnings:?}"); +} + /// An envelope with its contents in an opacity mask's art is written as its art there, and an /// envelope in a group stays live. #[test] diff --git a/crates/eps/src/ai_editing/writer.rs b/crates/eps/src/ai_editing/writer.rs index 879eb9d9f5..3e4f23bda9 100644 --- a/crates/eps/src/ai_editing/writer.rs +++ b/crates/eps/src/ai_editing/writer.rs @@ -204,6 +204,12 @@ pub(super) struct Writer<'a> { /// The `hidden` and `nest` values at which the objects of the opacity mask's art being written /// are written: objects there may be written with their art styles. mask_level: Option<(u32, u32)>, + /// The `hidden` and `nest` values at which the objects of the envelope contents being written + /// are written ([`Self::envelope`]): type there may be written as type. With them, whether the + /// contents are in the envelope's dictionary, where the reader reads a copy of each text object. + contents_level: Option<(u32, u32, bool)>, + /// How many text objects were written as their outlines ([`Self::text`]). + outlined: usize, /// The image just written was written as a placed file. placed: bool, /// The type just written was written as type, not as its outlines. @@ -298,6 +304,8 @@ impl<'a> Writer<'a> { payloads: vec![], hidden: 0, mask_level: None, + contents_level: None, + outlined: 0, placed: false, typed: false, bound: vec![], @@ -1284,9 +1292,10 @@ impl<'a> Writer<'a> { // ---------- type ---------- /// Type `n`: a text object stub naming its story in the text document ([`texts`]) when the - /// text document can hold it, outside opacity masks, pattern tiles, symbols' art and art cut to - /// a clipping object; else its outlines ([`Self::outlined_text`]). A frame of threaded area type - /// names the story of its thread, which its first frame holds. + /// text document can hold it, on the layers or in an envelope's contents, outside opacity + /// masks, pattern tiles, symbols' art and art cut to a clipping object; else its outlines + /// ([`Self::outlined_text`]). A frame of threaded area type names the story of its thread, + /// which its first frame holds. fn text(&mut self, n: &Node, xy: Xy) { let NodeKind::Text(t) = &n.kind else { return }; // Dynamic Text is written as the sizes its layout draws the type at. @@ -1294,13 +1303,16 @@ impl<'a> Writer<'a> { let t = fitted.as_deref().unwrap_or(&**t); let doc = self.doc; let thread = doc.text_threads.iter().find(|th| th.contains(&n.id)); + let contents = self.contents_level.filter(|(hidden, nest, _)| (*hidden, *nest) == (self.hidden, self.nest)); // Type that can't be a stub is written as its outlines, which count their own objects. - if self.hidden > 0 || self.clip.is_some() || self.in_symbol || (thread.is_none() && !texts::holds(n, t)) { + if (self.hidden > 0 && contents.is_none()) || self.clip.is_some() || self.in_symbol || (thread.is_none() && !texts::holds(n, t)) { + self.outlined += 1; return self.outlined_text(n, xy); } - // The stub's objects ([`texts::stub_objects`], [`Self::take_objects`]); without room for - // them, the type is left out before its story goes into the text document. - let stub = texts::stub_objects(t); + // The stub's objects ([`texts::stub_objects`], [`Self::take_objects`]), and the copy the + // reader reads of it in an envelope's dictionary; without room for them, the type is left + // out before its story goes into the text document. + let stub = texts::stub_objects(t) + usize::from(contents.is_some_and(|(.., copied)| copied)); if !self.take_objects(stub) { return; } @@ -1314,6 +1326,7 @@ impl<'a> Writer<'a> { let Some(((story, frame), lost)) = placed else { // The text document can't hold it: the outlines count their own objects. self.objects -= stub; + self.outlined += 1; return self.outlined_text(n, xy); }; self.typed = true; diff --git a/crates/eps/src/ai_editing/writer/envelopes.rs b/crates/eps/src/ai_editing/writer/envelopes.rs index dcd7274291..bb8273acf6 100644 --- a/crates/eps/src/ai_editing/writer/envelopes.rs +++ b/crates/eps/src/ai_editing/writer/envelopes.rs @@ -19,14 +19,14 @@ use vectorcraft_doc::live::{ Coons, EnvelopeKind, EnvelopeOptions, GradientMesh, H_DOWN, H_LEFT, H_RIGHT, H_UP, MeshPoint, PreserveShape, WARP_NET, envelope_content_boxes, envelope_grid, envelope_stroke_margin, warp_grid, }; -use vectorcraft_doc::{Node, NodeKind}; +use vectorcraft_doc::{Document, Node, NodeKind}; use vectorcraft_geom::{Affine, PathData}; use super::{MAX_NEST, Writer, Xy, hide}; use crate::import::{DEFORM_PLUGIN, EnvelopeGridPoint as GridPoint, envelope_outline, envelope_upright, warp_number}; use crate::ps; -pub(in crate::ai_editing) const ENVELOPES: &str = "envelopes of type, envelopes without contents, envelopes in opacity masks, pattern tiles, art cut to a clipping object or other envelopes' contents, envelopes on the layers of a large canvas or in a turned or mirrored frame, warps drawn without their style's net, meshes and top objects that the data would open in another shape, envelopes with a Fidelity that isn't a whole number, envelopes that map from a box other than their contents' bounds with or without strokes or keep bounds margins with Distort Appearance off, envelopes after the first 256, and envelopes nested more than 8 deep are written as their art in the editing data for other apps"; +pub(in crate::ai_editing) const ENVELOPES: &str = "envelopes whose type is threaded, has text wrap options or is written as its outlines, envelopes without contents, envelopes in opacity masks, pattern tiles, art cut to a clipping object or other envelopes' contents, envelopes on the layers of a large canvas or in a turned or mirrored frame, warps drawn without their style's net, meshes and top objects that the data would open in another shape, envelopes with a Fidelity that isn't a whole number, envelopes that map from a box other than their contents' bounds with or without strokes or keep bounds margins with Distort Appearance off, envelopes after the first 256, and envelopes nested more than 8 deep are written as their art in the editing data for other apps"; /// The most rows or columns of an envelope's grid written (Make with Mesh's own limit, and the /// reader's). @@ -57,6 +57,15 @@ fn holds_type(n: &Node) -> bool { matches!(n.kind, NodeKind::Text(_)) || n.children().is_some_and(|c| c.iter().any(|k| holds_type(k))) } +/// Is all the type in `n`, and in anything in it, type the reader can make an envelope's contents +/// of: none of it is a frame of threaded type or has text wrap options. +fn type_reads_back(n: &Node, doc: &Document) -> bool { + match &n.kind { + NodeKind::Text(_) => n.extras().wrap.is_none() && !doc.text_threads.iter().any(|th| th.contains(&n.id)), + _ => n.children().is_none_or(|c| c.iter().all(|k| type_reads_back(k, doc))), + } +} + /// The record of envelope `n` (its parts given), or `None` where the reader would open it in /// another shape than it has, or not as an envelope. fn record( @@ -72,7 +81,7 @@ fn record( // Distort Appearance on. let whole = (0.0..=100.0).contains(&fidelity) && fidelity.fract() == 0.0; let margin_kept = !options.distort_appearance && options.bounds_margin != [0.0; 4]; - if content.is_empty() || frame != Affine::IDENTITY || !whole || margin_kept || content.iter().any(|c| holds_type(c)) { + if content.is_empty() || frame != Affine::IDENTITY || !whole || margin_kept { return None; } let mut keys = String::new(); @@ -282,17 +291,24 @@ fn top_object_grid(path: &PathData, corners: Option<[u32; 4]>) -> Option<(usize, impl Writer<'_> { /// Envelope `n`, of transparency `xy`, as a live envelope (see the module's form), outside opacity - /// masks' art, pattern tiles, art cut to a clipping object and the layers of a large canvas. False, - /// writing nothing, where it is written as its art instead: where the reader would open it in - /// another shape, or some of it is left out for the object limit ([`Self::take_objects`]). + /// masks' art, pattern tiles, art cut to a clipping object and the layers of a large canvas. Type + /// in its contents is written as type, which the reader makes the envelope's contents again. + /// False, writing nothing, where it is written as its art instead: where the reader would open it + /// in another shape, or with its type as drawn art (threaded type, type with text wrap options, + /// type written as its outlines), or some of it is left out for the object limit + /// ([`Self::take_objects`]). pub(super) fn envelope(&mut self, n: &Node, xy: Xy) -> bool { let NodeKind::Envelope { content, kind, fidelity, editing, options, frame } = &n.kind else { return false }; if self.hidden > 0 || self.clip.is_some() || self.nest >= MAX_NEST || !self.symbols_live || self.envelopes_left == 0 { return false; } + if content.iter().any(|c| holds_type(c)) && !content.iter().all(|c| type_reads_back(c, self.doc)) { + return false; + } let Some(record) = record(n, content, kind, *fidelity, *editing, options, **frame) else { return false }; let start = self.body.len(); let (state, gs, charges, bound) = (self.state, self.gs, self.charges(), self.bound.len()); + let (outlined, texts, warnings) = (self.outlined, self.texts.written(), self.warnings.len()); self.nest += 1; self.hidden += 1; // The group before the dictionary: the grid, or the contents while they are edited. @@ -300,7 +316,7 @@ impl Writer<'_> { let opened = self.open_group("u"); if opened { if *editing { - self.contents(content); + self.contents(content, false); } else { self.grid(record.grid.as_ref()); } @@ -318,23 +334,31 @@ impl Writer<'_> { if *editing { self.grid(record.grid.as_ref()); } else if self.open_group("u") { - self.contents(content); + self.contents(content, true); self.end_group("U", Xy::PLAIN); } let block = std::mem::replace(&mut self.body, outer); hide(&block, &mut self.body); (self.state, self.gs, self.block_end) = (at_block, None, usize::MAX); let _ = writeln!(self.body, "%_X+\n%_ (AIDeformArtKey) ,\n%_{} /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW", record.style); + // Type in the contents written as its outlines would open as drawn art, not as type. + let outlines = self.outlined != outlined; self.hidden -= 1; self.nest -= 1; // The distorted art, which takes the envelope's lock, visibility, transparency, name and // opacity mask. self.state(n.locked, !n.visible); self.drawn_art(n, xy, None); - if !opened || self.left_out_since(charges) { + if !opened || outlines || self.left_out_since(charges) { let lines = self.body.split_off(start); self.give_back(charges, &lines); self.bound.truncate(bound); + // The stories of the type in the lines taken out go with them. + self.texts.take_back(texts); + // The warnings of art written as its art again are that art's. + if !self.left_out_since(charges) { + self.warnings.truncate(warnings); + } (self.state, self.gs, self.block_end) = (state, gs, usize::MAX); return false; } @@ -343,11 +367,14 @@ impl Writer<'_> { true } - /// An envelope's contents. - fn contents(&mut self, content: &[Arc]) { + /// An envelope's contents, where type may be written as type ([`Self::text`]): `copied` when + /// they are in its dictionary, where the reader reads a copy of each text object. + fn contents(&mut self, content: &[Arc], copied: bool) { + let level = self.contents_level.replace((self.hidden, self.nest, copied)); for c in content { self.node(c); } + self.contents_level = level; } /// An envelope's grid, when it has one, as a gray gradient mesh (`/Mesh X!` … `/End X!`): its diff --git a/crates/eps/src/ai_editing/writer/texts.rs b/crates/eps/src/ai_editing/writer/texts.rs index 6bc3c63b09..ec565c141d 100644 --- a/crates/eps/src/ai_editing/writer/texts.rs +++ b/crates/eps/src/ai_editing/writer/texts.rs @@ -604,6 +604,15 @@ fn place_on(segments: &[[f64; 8]], f: f64) -> f64 { segments.len() as f64 } +/// Where the text document ended at some point ([`TextDoc::written`]). +#[derive(Clone, Copy)] +pub(in crate::ai_editing) struct TextMark { + at: (usize, usize, usize, usize, usize), + values: usize, + lost: bool, + planned: usize, +} + /// The text document being written. #[derive(Default)] pub(in crate::ai_editing) struct TextDoc { @@ -997,6 +1006,24 @@ impl TextDoc { self.threaded.get(&id).copied() } + /// Where the text document written so far ends, for [`Self::take_back`]. + pub(in crate::ai_editing) fn written(&self) -> TextMark { + TextMark { at: self.mark(), values: self.doc_values, lost: self.lost, planned: self.planned.len() } + } + + /// Take the frames, stories and fonts added since `m` out again, with what they counted. Where + /// a thread was planned since, its frames name its story, so nothing is taken out. + pub(in crate::ai_editing) fn take_back(&mut self, m: TextMark) { + if self.planned.len() != m.planned { + return; + } + let (frames, stories, bytes, text, fonts) = m.at; + self.frames.truncate(frames); + self.stories.truncate(stories); + self.fonts.truncate(fonts); + (self.doc_bytes, self.text_bytes, self.doc_values, self.lost) = (bytes, text, m.values, m.lost); + } + /// Whether a thread written since the last call has attributes the text document leaves out. pub(in crate::ai_editing) fn take_lost(&mut self) -> bool { std::mem::take(&mut self.lost) diff --git a/crates/eps/src/import.rs b/crates/eps/src/import.rs index bdb3b9cf50..942e7f3046 100644 --- a/crates/eps/src/import.rs +++ b/crates/eps/src/import.rs @@ -82,8 +82,6 @@ use graphics::{GState, Out}; use interp::{Fault, Interp}; use obj::PsError; -#[cfg(test)] -pub(crate) use ai::ENVELOPE_DRAWN; pub use ai::NameEncoding; #[cfg(test)] pub(crate) use ai::objects_read; @@ -92,6 +90,8 @@ pub(crate) use ai::{ MAX_LISTED, MAX_LISTED_NAME, MAX_SYMBOLS, brush_plugin_name, brushes_kept, envelope_outline, envelope_upright, read_art_style, read_brush_object, read_style_list, style_moves_with, symbol_art_member, warp_number, }; +#[cfg(test)] +pub(crate) use ai::{ENVELOPE_DRAWN, ENVELOPE_TYPE_DRAWN}; pub(crate) use ate::{MAX_BYTES as MAX_TEXT_DOCUMENT, MAX_FRAMES, MAX_SEGMENTS, MAX_STORY, MAX_VALUES as MAX_TEXT_VALUES, record_version}; pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard, is_loss, layered_ai, layered_ai_from}; pub(crate) use native::{MAX_TEXT, scale_document}; diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 296bd84b2b..6cd8e6155e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1638,7 +1638,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur SVG import reads each `` and `` as the ellipse VectorCraft draws: four smooth anchors on the axes, starting at the right, with handles of the standard length, so an ellipse exported as `` comes back with the anchors it was written from and draws the same (`circles_and_ellipses_import_as_the_ellipse_drawn` in `crates/svg/tests/it/basic_shapes.rs`, `filled_ellipse_on_the_artboard_edge_survives_visually` in `prop_svg.rs`). SVG import keeps attributes in other applications' XML namespaces (`plotter:pen="2"`) on the objects and layers they belong to, as it keeps their `data-*` attributes: copies, duplicates and `.vectorcraft` files keep them, and SVG export writes them back with each namespace declared once on the root (the rest of #522; `crates/svg/tests/it/foreign_attributes.rs`, `tests/attributes.rs`). A PDF-compatible `.ai` saved with Include Editing Data for Other Apps defines every symbol in its editing copy with its art, registration point and place in the Symbols panel's order, and writes its instances as symbol instances, which VectorCraft's `.ai` reader opens as symbols and instances again (`symbols_and_their_instances_are_read_back` in `crates/eps/src/ai_editing/tests.rs`; [file-format-parity.md](file-format-parity.md) lists the instances written as their art). - A PDF-compatible `.ai` saved with Include Editing Data for Other Apps keeps envelopes made with a warp, a mesh or a top object editable: its editing copy holds each envelope's contents, shape, Fidelity, Envelope Options and Edit Contents state in the form VectorCraft's `.ai` envelope reader reads, beside its distorted art, so the file opens with the same live envelope, Edit Contents still switching between the contents and the envelope ([Edit the contents of envelopes](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/edit-the-contents-of-envelopes.html)). Envelopes of type, envelopes in opacity masks, pattern tiles or another envelope's contents, and envelopes the reader would open with another shape or other settings (a Fidelity that isn't a whole number, bounds margins kept with Distort Appearance off), and envelopes after the first 256 in a file are written as their distorted art, with a warning (`envelopes_are_read_back_live` and `envelopes_the_reader_cannot_open_live_are_written_as_their_art` in `crates/eps/src/ai_editing/tests.rs`). + A PDF-compatible `.ai` saved with Include Editing Data for Other Apps keeps envelopes made with a warp, a mesh or a top object editable: its editing copy holds each envelope's contents, shape, Fidelity, Envelope Options and Edit Contents state in the form VectorCraft's `.ai` envelope reader reads, beside its distorted art, so the file opens with the same live envelope, Edit Contents still switching between the contents and the envelope ([Edit the contents of envelopes](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/edit-the-contents-of-envelopes.html)). Envelopes whose type is threaded, has text wrap options or is written as its outlines, envelopes in opacity masks, pattern tiles or another envelope's contents, and envelopes the reader would open with another shape or other settings (a Fidelity that isn't a whole number, bounds margins kept with Distort Appearance off), and envelopes after the first 256 in a file are written as their distorted art, with a warning (`envelopes_are_read_back_live` and `envelopes_the_reader_cannot_open_live_are_written_as_their_art` in `crates/eps/src/ai_editing/tests.rs`). + An envelope whose contents hold type keeps the type editable in that `.ai`: the editing copy writes the type as type in the envelope's contents, in the form the `.ai` reader opens as a live envelope of type (`TypedEnvelope` in `crates/eps/src/import/ai/envelope.rs`), so the file opens with the type shown and editable in the envelope's contents, with Edit Contents off or on. An envelope of type in a symbol's art, and one whose type is threaded, has text wrap options or is written as its outlines (mirrored type, for example), is written as its distorted art, as before (`an_envelope_of_type_is_read_back_live_with_its_type`, `an_envelope_of_type_written_as_its_art_leaves_no_story` and `an_envelope_of_type_in_a_symbol_or_a_thread_is_written_as_its_art` in `crates/eps/src/ai_editing/tests.rs`). A PDF-compatible `.ai` saved with Include Editing Data for Other Apps writes eligible paths, compound paths and groups with live effects with an art style when the writer supports their appearance and effects (not a filled path that uses the even-odd rule, and none on a large canvas), which VectorCraft's `.ai` reader opens as the same appearance with its effects live (`objects_with_live_effects_are_read_back` and `every_effect_setting_written_on_an_object_is_read_back` in `crates/eps/src/ai_editing/tests.rs`; [file-format-parity.md](file-format-parity.md) lists the objects written without one and the effects they lose). The same holds for the objects of an opacity mask's art, a mask inside another mask's art and a hidden art root included, so a mask whose art has Offset Path opens drawing the image it masks as it did, with the effect live; geometry effects that no art style holds on a mask's art, such as Roughen, are written applied to the art, which opens drawing what it drew (`a_masked_image_with_live_offset_path_round_trips`, `hidden_and_nested_mask_art_keep_live_offset_path` and `a_masks_art_with_an_effect_no_style_holds_is_written_as_it_draws` in `crates/eps/src/ai_editing/tests.rs`). A PDF-compatible `.ai` saved with Include Editing Data for Other Apps writes point type, area type and type on a path as type in its editing copy, and objects next to it keep their own transparency (`crates/eps/src/ai_editing/tests.rs`, `cmd::fileio::tests_pdfedit::type_comes_back_from_the_editing_copy`; [file-format-parity.md](file-format-parity.md) lists what is written as outlines). From 608c0f77aafa902a426f385937851bbf8f3f0c9e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 02:47:10 -0800 Subject: [PATCH 03/28] i18n: Hindi translates 116 status and error messages that its catalog lacked Adds Hindi rows for 116 image export, color profile, swatch exchange, color book, DXF, PDF knockout group, Revolve and 3D messages that the Hindi catalog did not translate. The Russian and Simplified Chinese catalogs already have rows for 113 of them. Placeholders, parameter names, file formats and values stay as the English writes them. A test checks four of the new rows. --- crates/ui-egui/src/i18n/hi.tsv | 118 +++++++++++++++++++++++++++++++ crates/ui-egui/src/i18n/tests.rs | 19 +++++ docs/target-app-parity.md | 1 + 3 files changed, 138 insertions(+) diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 4a11fad73d..bdea8689d4 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -8889,3 +8889,121 @@ theme Appearance अपीयरेंस Grommets आईलेट Grommet Inset किनारे से आईलेट की दूरी Diameter व्यास + +# Messages of the effects, 3D, image export, color profiles, swatch exchange files, color books and DXF import. +@msg 3D effects require finite coordinates within 100 million points 3D इफ़ेक्ट्स के लिए 100 मिलियन पॉइंट के भीतर परिमित निर्देशांक चाहिए +@msg 3D effects require one to ten lights 3D इफ़ेक्ट्स के लिए एक से दस लाइटें चाहिए +@msg BMP depth {_1}: 1, 4, 8, 16, 24 or 32 bits per pixel BMP डेप्थ {_1}: प्रति पिक्सेल 1, 4, 8, 16, 24 या 32 बिट +@msg BMP encoding failed: {width} × {height} pixels doesn't fit the format or the pixel buffer BMP एन्कोडिंग विफल: {width} × {height} पिक्सेल न फ़ॉर्मैट में फ़िट होते हैं, न पिक्सेल बफ़र में +@msg GIF encoding failed: the palette image is malformed GIF एन्कोडिंग विफल: पैलेट इमेज विकृत है +@msg GIF encoding failed: {e} GIF एन्कोडिंग विफल: {e} +@msg JPEG colour profile: {e} JPEG कलर प्रोफ़ाइल: {e} +@msg JPEG encoding failed: pixel buffer doesn't match the image size JPEG एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता +@msg JPEG encoding failed: {e} JPEG एन्कोडिंग विफल: {e} +@msg PNG encoding failed: pixel buffer doesn't match the image size PNG एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता +@msg PNG encoding failed: the palette image is malformed PNG एन्कोडिंग विफल: पैलेट इमेज विकृत है +@msg PNG encoding failed: {e} PNG एन्कोडिंग विफल: {e} +@msg PNG encoding failed: {width} × {height} pixels is not a valid image size PNG एन्कोडिंग विफल: {width} × {height} पिक्सेल मान्य इमेज आकार नहीं है +@msg PSD colour profile: {e} PSD कलर प्रोफ़ाइल: {e} +@msg PSD encoding failed: the pixel buffer doesn't match the image size PSD एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता +@msg Revolve profile contains invalid coordinates रिवॉल्व प्रोफ़ाइल में अमान्य निर्देशांक हैं +@msg Revolve profile is too complex रिवॉल्व प्रोफ़ाइल बहुत जटिल है +@msg Revolve projection is not finite रिवॉल्व प्रोजेक्शन परिमित नहीं है +@msg Revolve requires finite profile coordinates within 100 million points रिवॉल्व के लिए 100 मिलियन पॉइंट के भीतर परिमित प्रोफ़ाइल निर्देशांक चाहिए +@msg Revolve surface is too complex; reduce Segments or simplify the profile रिवॉल्व सतह बहुत जटिल है; सेगमेंट कम करें या प्रोफ़ाइल सरल करें +@msg Revolve surface is too complex; simplify the profile रिवॉल्व सतह बहुत जटिल है; प्रोफ़ाइल सरल करें +@msg Revolve visibility cannot split the projected boundary रिवॉल्व दृश्यता प्रोजेक्ट की गई सीमा को विभाजित नहीं कर सकती +@msg Revolve visibility creates too many fragments रिवॉल्व दृश्यता बहुत अधिक टुकड़े बनाती है +@msg Revolve visibility creates too many vector anchors रिवॉल्व दृश्यता बहुत अधिक वेक्टर एंकर बनाती है +@msg Revolve visibility exceeds its geometry work limit रिवॉल्व दृश्यता अपनी ज्यामिति गणना की सीमा से आगे निकल जाती है +@msg Revolve visibility exceeds its spatial index limit रिवॉल्व दृश्यता अपनी स्पेशियल इंडेक्स सीमा से आगे निकल जाती है +@msg Revolve visibility has too many boundary crossings रिवॉल्व दृश्यता में सीमा के बहुत अधिक क्रॉसिंग हैं +@msg Revolve visibility has too many input anchors रिवॉल्व दृश्यता में बहुत अधिक इनपुट एंकर हैं +@msg Revolve visibility requires convex cap sectors रिवॉल्व दृश्यता के लिए उत्तल कैप सेक्टर चाहिए +@msg Revolve visibility requires finite projected faces रिवॉल्व दृश्यता के लिए परिमित प्रोजेक्ट किए गए फ़ेस चाहिए +@msg SVG options: {e} SVG ऑप्शन्स: {e} +@msg Save for Web settings: {e} वेब के लिए सेव करें की सेटिंग्स: {e} +@msg TIFF colour profile: {e} TIFF कलर प्रोफ़ाइल: {e} +@msg TIFF encoding failed: the pixel buffer doesn't match the image size TIFF एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता +@msg TIFF encoding failed: {e} TIFF एन्कोडिंग विफल: {e} +@msg Targa depth {_1}: 16, 24 or 32 bits per pixel Targa डेप्थ {_1}: प्रति पिक्सेल 16, 24 या 32 बिट +@msg Targa encoding failed: the pixel buffer doesn't match the image size Targa एन्कोडिंग विफल: पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता +@msg The 3D projection is not finite 3D प्रोजेक्शन परिमित नहीं है +@msg The 3D shadow is too complex; turn Shadows off or simplify the art 3D छाया बहुत जटिल है; छायाएँ बंद करें या आर्टवर्क सरल करें +@msg The shape contains invalid coordinates शेप में अमान्य निर्देशांक हैं +@msg Too many Revolve faces for visibility evaluation दृश्यता की गणना के लिए रिवॉल्व के फ़ेस बहुत अधिक हैं +@msg VectorCraft reads at most {ASE_MAX_ENTRIES} colors and groups from a swatch exchange file, and this library has {entries} VectorCraft स्वॉच एक्सचेंज फ़ाइल से अधिकतम {ASE_MAX_ENTRIES} रंग और ग्रुप पढ़ता है, और इस लाइब्रेरी में {entries} हैं +@msg WebP encoding failed: {e} WebP एन्कोडिंग विफल: {e} +@msg [{j}] `{_1}`: {why} [{j}] `{_1}`: {why} +@msg `index` must be a whole number `index` पूरी संख्या होनी चाहिए +@msg `view` is smallThumb, mediumThumb, largeThumb, smallList or largeList `view` smallThumb, mediumThumb, largeThumb, smallList या largeList होता है +@msg a line can't run from a point to itself कोई लाइन किसी पॉइंट से उसी पॉइंट तक नहीं जा सकती +@msg a line needs at least two points लाइन के लिए कम से कम दो पॉइंट चाहिए +@msg a number cannot be read एक संख्या पढ़ी नहीं जा सकती +@msg a swatch exchange block is too long स्वॉच एक्सचेंज का एक ब्लॉक बहुत लंबा है +@msg a swatch exchange name is too long स्वॉच एक्सचेंज का एक नाम बहुत लंबा है +@msg a {sw} × {sh} image can't be resized to {width} × {height} pixels {sw} × {sh} इमेज का आकार बदलकर {width} × {height} पिक्सेल नहीं किया जा सकता +@msg a {width} × {height} image is too large {width} × {height} इमेज बहुत बड़ी है +@msg a {width} × {height} image: {e} {width} × {height} इमेज: {e} +@msg an image could not be written: {e} एक इमेज लिखी नहीं जा सकी: {e} +@msg an image that couldn't be read was left out जो इमेज पढ़ी नहीं जा सकी, उसे छोड़ दिया गया +@msg can't open “{name}”: {e} “{name}” ओपन नहीं किया जा सकता: {e} +@msg cannot align zip entries to {align} bytes ज़िप एंट्री को {align} बाइट पर अलाइन नहीं किया जा सकता +@msg cannot open the automation {kind} root `{_1}`: {e} ऑटोमेशन {kind} रूट `{_1}` ओपन नहीं किया जा सकता: {e} +@msg cannot read profile: {0} प्रोफ़ाइल पढ़ी नहीं जा सकती: {0} +@msg color book version {version} isn't supported (only 1) कलर बुक वर्ज़न {version} समर्थित नहीं है (केवल 1) +@msg color books in color space {other} aren't supported (only RGB, CMYK and Lab) कलर स्पेस {other} वाली कलर बुक समर्थित नहीं हैं (केवल RGB, CMYK और Lab) +@msg file name too long to zip: {_1} ज़िप करने के लिए फ़ाइल का नाम बहुत लंबा है: {_1} +@msg file name too long to zip: {name} ज़िप करने के लिए फ़ाइल का नाम बहुत लंबा है: {name} +@msg freeform line {i}: {e} फ्रीफॉर्म लाइन {i}: {e} +@msg freeform point {i}: {e} फ्रीफॉर्म पॉइंट {i}: {e} +@msg give `{key}` or `{other}`, not both `{key}` या `{other}` दें, दोनों नहीं +@msg grayscale profiles can't be working spaces yet ग्रेस्केल प्रोफ़ाइल अभी वर्किंग स्पेस नहीं हो सकतीं +@msg input contains NaN or infinite coordinates इनपुट में NaN या अनंत निर्देशांक हैं +@msg input path could not be closed इनपुट पाथ बंद नहीं किया जा सका +@msg invalid ICC profile: {0} अमान्य ICC प्रोफ़ाइल: {0} +@msg invalid scale {scale} अमान्य स्केल {scale} +@msg its localized names take more than {_1} KB इसके स्थानीयकृत नाम {_1} KB से अधिक जगह लेते हैं +@msg its objects are nested too deep ({e}) इसके ऑब्जेक्ट बहुत गहराई तक नेस्ट किए गए हैं ({e}) +@msg its tag table lists the `{_1}` tag more than once इसकी टैग तालिका में `{_1}` टैग एक से अधिक बार है +@msg knockout groups with brushed strokes over other translucent elements are written as PDF knockout groups: their translucent elements knock out what they cover fully जिन नॉकआउट ग्रुप में ब्रश स्ट्रोक दूसरे पारदर्शी एलिमेंट के ऊपर हैं, वे PDF नॉकआउट ग्रुप के रूप में लिखे जाते हैं: उनके पारदर्शी एलिमेंट अपने नीचे का पूरा हिस्सा नॉकआउट करते हैं +@msg knockout groups with too many overlapping translucent elements are written as PDF knockout groups: their translucent elements knock out what they cover fully बहुत अधिक ओवरलैप करने वाले पारदर्शी एलिमेंट वाले नॉकआउट ग्रुप PDF नॉकआउट ग्रुप के रूप में लिखे जाते हैं: उनके पारदर्शी एलिमेंट अपने नीचे का पूरा हिस्सा नॉकआउट करते हैं +@msg not a graphic style library: {e} यह ग्राफ़िक स्टाइल लाइब्रेरी नहीं है: {e} +@msg not a swatch library (.vcswatches or .gpl) यह स्वॉच लाइब्रेरी नहीं है (.vcswatches या .gpl) +@msg not a swatch library (format `{_1}`) यह स्वॉच लाइब्रेरी नहीं है (फ़ॉर्मैट `{_1}`) +@msg not a swatch library: {e} यह स्वॉच लाइब्रेरी नहीं है: {e} +@msg not an action set file ({e}) यह एक्शन सेट फ़ाइल नहीं है ({e}) +@msg profile colour space {other} is not RGB, CMYK or Gray प्रोफ़ाइल का कलर स्पेस {other} RGB, CMYK या Gray नहीं है +@msg regular expression too large रेगुलर एक्सप्रेशन बहुत बड़ा है +@msg script file is not UTF-8 text: {value} स्क्रिप्ट फ़ाइल UTF-8 टेक्स्ट नहीं है: {value} +@msg stop {i}: {e} स्टॉप {i}: {e} +@msg swatch exchange version {major}.{minor} isn't supported (only 1.x) स्वॉच एक्सचेंज वर्ज़न {major}.{minor} समर्थित नहीं है (केवल 1.x) +@msg the color book holds more than {ASE_MAX_ENTRIES} colors कलर बुक में {ASE_MAX_ENTRIES} से अधिक रंग हैं +@msg the color book is cut short कलर बुक अधूरी है +@msg the color book's names take more than {_1} MB कलर बुक के नाम {_1} MB से अधिक जगह लेते हैं +@msg the image would be {w} × {h} pixels; raster exports are limited to {MAX_RASTER_SIDE} pixels per side and {_1} megapixels: lower the scale or resolution इमेज {w} × {h} पिक्सेल की होगी; रास्टर एक्सपोर्ट हर तरफ़ अधिकतम {MAX_RASTER_SIDE} पिक्सेल और {_1} मेगापिक्सेल तक सीमित हैं: स्केल या रिज़ॉल्यूशन कम करें +@msg the optimised image can't be read back: {e} ऑप्टिमाइज़ की गई इमेज वापस पढ़ी नहीं जा सकती: {e} +@msg the pixel buffer doesn't match the image size पिक्सेल बफ़र इमेज के आकार से मेल नहीं खाता +@msg the preview: {e} प्रीव्यू: {e} +@msg the request may not read the New Document profiles folder अनुरोध नया डॉक्यूमेंट प्रोफ़ाइल फ़ोल्डर नहीं पढ़ सकता +@msg the shapes are too degenerate to combine शेप इतने विकृत हैं कि उन्हें मिलाया नहीं जा सकता +@msg the shapes cross themselves and each other too often to combine शेप खुद को और एक-दूसरे को इतनी बार काटते हैं कि उन्हें मिलाया नहीं जा सकता +@msg the swatch exchange file holds more than {ASE_MAX_ENTRIES} colors and groups स्वॉच एक्सचेंज फ़ाइल में {ASE_MAX_ENTRIES} से अधिक रंग और ग्रुप हैं +@msg the swatch exchange file is cut short स्वॉच एक्सचेंज फ़ाइल अधूरी है +@msg the {_1} plate can't be read: {e} {_1} प्लेट पढ़ी नहीं जा सकती: {e} +@msg this binary DXF file is damaged or cut short यह बाइनरी DXF फ़ाइल क्षतिग्रस्त या अधूरी है +@msg this is a DWG drawing, which can't be read: save it from the CAD app as a DXF and open that यह एक DWG ड्रॉइंग है, जिसे पढ़ा नहीं जा सकता: इसे CAD ऐप से DXF के रूप में सेव करें और वह फ़ाइल ओपन करें +@msg too many classes बहुत अधिक क्लास +@msg too many groups बहुत अधिक ग्रुप +@msg too many layers for PSD (at most 32767) PSD के लिए बहुत अधिक लेयर्स (अधिकतम 32767) +@msg too many swatch exchange blocks स्वॉच एक्सचेंज के बहुत अधिक ब्लॉक +@msg unknown colour profile: {0} अज्ञात कलर प्रोफ़ाइल: {0} +@msg {_1} clashes with a built-in profile name {_1} किसी अंतर्निहित प्रोफ़ाइल के नाम से टकराता है +@msg {_1} × {_2} pixels is too large for GIF (at most 65535 pixels a side): lower the resolution {_1} × {_2} पिक्सेल GIF के लिए बहुत बड़ा है (हर तरफ़ अधिकतम 65535 पिक्सेल): रिज़ॉल्यूशन कम करें +@msg {_1}: no usable transform to/from sRGB {_1}: sRGB में या sRGB से बदलने का कोई उपयोगी ट्रांसफ़ॉर्म नहीं है +@msg {name} is not a CMYK profile {name} CMYK प्रोफ़ाइल नहीं है +@msg {name} is not an RGB profile {name} RGB प्रोफ़ाइल नहीं है +@msg {path}: could not be read {path}: पढ़ा नहीं जा सका +@msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} पिक्सेल JPEG के लिए बहुत बड़ा है (हर तरफ़ अधिकतम 65535 पिक्सेल): रिज़ॉल्यूशन कम करें +@msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} पिक्सेल Targa के लिए बहुत बड़ा है (हर तरफ़ अधिकतम 65535 पिक्सेल): रिज़ॉल्यूशन कम करें +@msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} पिक्सेल PSD के लिए बहुत बड़ा है (हर तरफ़ अधिकतम {MAX_SIDE} पिक्सेल): रिज़ॉल्यूशन कम करें diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index 4f9f03e5f6..c89a43363b 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -135,6 +135,25 @@ fn hindi_rows_keep_the_meaning_of_their_english() { "पेज थंबनेल RGB पिक्सल का होना चाहिए, हर तरफ़ 1 से 256 (मिला 0 × 4)" ); assert_eq!(message(hi, "invalid angle"), "अमान्य कोण"); + let limits = message( + hi, + "the image would be 9000 × 9000 pixels; raster exports are limited to 8192 pixels per side and 64 megapixels: lower the scale or resolution", + ); + assert!( + limits.contains("9000 × 9000 पिक्सेल") && limits.contains("8192 पिक्सेल और 64 मेगापिक्सेल") && limits.contains("स्केल या रिज़ॉल्यूशन कम करें"), + "{limits}" + ); + assert_eq!( + message(hi, "color books in color space HSB aren't supported (only RGB, CMYK and Lab)"), + "कलर स्पेस HSB वाली कलर बुक समर्थित नहीं हैं (केवल RGB, CMYK और Lab)" + ); + assert_eq!(message(hi, "`index` must be a whole number"), "`index` पूरी संख्या होनी चाहिए"); + let knockout = message( + hi, + "knockout groups with too many overlapping translucent elements are written as PDF knockout groups: \ + their translucent elements knock out what they cover fully", + ); + assert!(knockout.starts_with("बहुत अधिक ओवरलैप") && knockout.contains("PDF नॉकआउट ग्रुप"), "{knockout}"); } /// Hindi messages keep every `a|b|c` list of accepted values exactly as their English writes it. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7e36c7f318..5ad047c71f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -895,6 +895,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Gujarati (2026-11-14, #6613): `gu` has 4,673 catalog rows, zero/one singular plurals, installed desktop font fallback and support for optional `Gujr` craft-font faces. The original 4,286 rows remain unchanged; all 387 additions are contributor-reviewed and approved, with 11 wording corrections. Status/error messages remain partial; the pinned fonts and web startup list have no Gujarati face, so web selection remains unavailable until one is bundled. Current-head validation is recorded in the PR; scores and estimates unchanged. - Hindi wording (2026-11-13): rows outside the plug-in messages keep their English's meaning, quantities and clauses, and use the catalog's save, swatch, crop, resolution, path and type terms; a test checks those terms and the point unit in every row; native review remains; scores and estimates unchanged. - Hindi terms (2026-11-13): every row that names a variable (data merge, variable fonts and widths, environment variables) says वेरिएबल, and joining paths, anchor points, point type and lines says जोड़ें instead of शामिल हों, which stays for joining the community; a test checks both terms in every row; scores and estimates unchanged. + - Hindi messages its catalog lacked (2026-11-14): 116 status and error messages that the Hindi catalog did not translate, among them the image export, color profile, swatch exchange, color book, DXF, PDF knockout group, Revolve and 3D messages, have Hindi rows; the translations are VectorCraft's own and await native review; scores and estimates unchanged. - Hindi English words (2026-11-13): 264 rows no longer keep English words in their sentences, repeat a parameter's name in Hindi or garble them, among them "unknown align", which now names the alignment; lists of accepted values such as `(left|right|top|bottom)` stay as their English writes them; two tests check every row; native review remains; scores and estimates unchanged. - Message field names and values (2026-11-13): 49 status and error messages in the Hindi, Bulgarian, Danish, Indonesian, Dutch, Brazilian Portuguese, Swedish and Turkish catalogs keep the field names in brackets, such as `[subpath, anchor]`, and the values an `unknown` message lists, such as `(points, lines)`, as their English writes them; a test checks every catalog; scores and estimates unchanged. - Italian messages (2026-11-13): the Italian catalog (`it`) has 1,802 more status and error message rows, and the untranslated report's missing Italian messages go from 1,842 to 40; the translations are VectorCraft's own and await native review; scores and estimates unchanged. See [localization-parity.md](localization-parity.md). From 8b1802349e3c44bc425fce95dbbae7831277b1e3 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 05:37:32 -0800 Subject: [PATCH 04/28] i18n: composite font errors with a part name translate behind the font name The engine reports a bad part as `name`: part: reason, which the generic `{name}`: {_1}: {e} row matched first and left untranslated. Each complete catalog gets name-prefixed rows for the six part errors, and the test lists both forms. --- crates/ui-egui/src/i18n/ca.tsv | 6 ++++++ crates/ui-egui/src/i18n/cs.tsv | 6 ++++++ crates/ui-egui/src/i18n/de.tsv | 6 ++++++ crates/ui-egui/src/i18n/el.tsv | 6 ++++++ crates/ui-egui/src/i18n/es.tsv | 6 ++++++ crates/ui-egui/src/i18n/fi.tsv | 6 ++++++ crates/ui-egui/src/i18n/fr.tsv | 6 ++++++ crates/ui-egui/src/i18n/hi.tsv | 6 ++++++ crates/ui-egui/src/i18n/hu.tsv | 6 ++++++ crates/ui-egui/src/i18n/it.tsv | 6 ++++++ crates/ui-egui/src/i18n/ja.tsv | 6 ++++++ crates/ui-egui/src/i18n/ka.tsv | 6 ++++++ crates/ui-egui/src/i18n/ko.tsv | 6 ++++++ crates/ui-egui/src/i18n/nl.tsv | 6 ++++++ crates/ui-egui/src/i18n/pl.tsv | 6 ++++++ crates/ui-egui/src/i18n/pt-br.tsv | 6 ++++++ crates/ui-egui/src/i18n/ru.tsv | 6 ++++++ crates/ui-egui/src/i18n/sv.tsv | 6 ++++++ crates/ui-egui/src/i18n/tests.rs | 10 ++++++++++ crates/ui-egui/src/i18n/tr.tsv | 6 ++++++ crates/ui-egui/src/i18n/uk.tsv | 6 ++++++ crates/ui-egui/src/i18n/vi.tsv | 6 ++++++ crates/ui-egui/src/i18n/zh-hans.tsv | 6 ++++++ crates/ui-egui/src/i18n/zh-hant.tsv | 6 ++++++ 24 files changed, 148 insertions(+) diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 5ba4d1b5c2..f14714f6bf 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -4680,11 +4680,17 @@ digits Malayalam Malaiàlam @msg the file holds two composite fonts named `{_1}` el fitxer conté dos tipus de lletra compostos anomenats `{_1}` @msg `{_1}` can't name a composite font `{_1}` no pot anomenar un tipus de lletra compost @msg {k}: give a font {k}: indiqueu un tipus de lletra +@msg `{name}`: {k}: give a font `{name}`: {k}: indiqueu un tipus de lletra @msg {k}: size must be {_1} to {_2}% {k}: size ha d’estar entre {_1} i {_2} % +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size ha d’estar entre {_1} i {_2} % @msg {k}: baseline must be {_1} to {_2}% {k}: baseline ha d’estar entre {_1} i {_2} % +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline ha d’estar entre {_1} i {_2} % @msg {k}: scale must be {_1} to {_2}% {k}: l’escala ha d’estar entre {_1} i {_2} % +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: l’escala ha d’estar entre {_1} i {_2} % @msg {k}: only kana, roman and numerals can be scaled {k}: només kana, roman i numerals es poden escalar +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: només kana, roman i numerals es poden escalar @msg {k}: only kana can be scaled at the center {k}: només kana es pot escalar des del centre +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: només kana es pot escalar des del centre @msg no composite font cap tipus de lletra compost Delete Font Suprimeix el tipus de lletra Kanji Kanji diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 339306a7e8..a9fff03d03 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1678,11 +1678,17 @@ digits Malayalam Malajálamské @msg the file holds two composite fonts named `{_1}` soubor obsahuje dvě složená písma s názvem `{_1}` @msg `{_1}` can't name a composite font `{_1}` nemůže být názvem složeného písma @msg {k}: give a font {k}: zadejte písmo +@msg `{name}`: {k}: give a font `{name}`: {k}: zadejte písmo @msg {k}: size must be {_1} to {_2}% {k}: size musí být {_1} až {_2} % +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size musí být {_1} až {_2} % @msg {k}: baseline must be {_1} to {_2}% {k}: baseline musí být {_1} až {_2} % +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline musí být {_1} až {_2} % @msg {k}: scale must be {_1} to {_2}% {k}: měřítko musí být {_1} až {_2} % +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: měřítko musí být {_1} až {_2} % @msg {k}: only kana, roman and numerals can be scaled {k}: měnit měřítko lze jen u kana, roman a numerals +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: měnit měřítko lze jen u kana, roman a numerals @msg {k}: only kana can be scaled at the center {k}: měřítko od středu lze měnit jen u kana +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: měřítko od středu lze měnit jen u kana @msg no composite font žádné složené písmo Delete Font Odstranit písmo Kanji Kandži diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index c79364b8f6..158034d575 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3659,11 +3659,17 @@ effect Points Punkte @msg the file holds two composite fonts named `{_1}` die Datei enthält zwei zusammengesetzte Schriften namens `{_1}` @msg `{_1}` can't name a composite font `{_1}` kann keine zusammengesetzte Schrift benennen @msg {k}: give a font {k}: geben Sie eine Schrift an +@msg `{name}`: {k}: give a font `{name}`: {k}: geben Sie eine Schrift an @msg {k}: size must be {_1} to {_2}% {k}: size muss {_1} bis {_2} % sein +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size muss {_1} bis {_2} % sein @msg {k}: baseline must be {_1} to {_2}% {k}: baseline muss {_1} bis {_2} % sein +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline muss {_1} bis {_2} % sein @msg {k}: scale must be {_1} to {_2}% {k}: die Skalierung muss {_1} bis {_2} % sein +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: die Skalierung muss {_1} bis {_2} % sein @msg {k}: only kana, roman and numerals can be scaled {k}: nur kana, roman und numerals lassen sich skalieren +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: nur kana, roman und numerals lassen sich skalieren @msg {k}: only kana can be scaled at the center {k}: nur kana lässt sich am Mittelpunkt skalieren +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: nur kana lässt sich am Mittelpunkt skalieren @msg no composite font keine zusammengesetzte Schrift Delete Font Schrift löschen Kanji Kanji diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 385746ed1e..97ec8ca3d9 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4192,11 +4192,17 @@ digits Malayalam Μαλαγιάλαμ @msg the file holds two composite fonts named `{_1}` το αρχείο περιέχει δύο σύνθετες γραμματοσειρές με όνομα `{_1}` @msg `{_1}` can't name a composite font το `{_1}` δεν μπορεί να είναι όνομα σύνθετης γραμματοσειράς @msg {k}: give a font {k}: δώστε γραμματοσειρά +@msg `{name}`: {k}: give a font `{name}`: {k}: δώστε γραμματοσειρά @msg {k}: size must be {_1} to {_2}% {k}: το size πρέπει να είναι από {_1} έως {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: το size πρέπει να είναι από {_1} έως {_2}% @msg {k}: baseline must be {_1} to {_2}% {k}: το baseline πρέπει να είναι από {_1} έως {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: το baseline πρέπει να είναι από {_1} έως {_2}% @msg {k}: scale must be {_1} to {_2}% {k}: η κλίμακα πρέπει να είναι από {_1} έως {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: η κλίμακα πρέπει να είναι από {_1} έως {_2}% @msg {k}: only kana, roman and numerals can be scaled {k}: μόνο τα kana, roman και numerals αλλάζουν κλίμακα +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: μόνο τα kana, roman και numerals αλλάζουν κλίμακα @msg {k}: only kana can be scaled at the center {k}: μόνο το kana αλλάζει κλίμακα από το κέντρο +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: μόνο το kana αλλάζει κλίμακα από το κέντρο @msg no composite font καμία σύνθετη γραμματοσειρά Delete Font Διαγραφή γραμματοσειράς Kanji Κάντζι diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index c8d2a3a976..dff2eb955a 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3707,11 +3707,17 @@ digits Malayalam Malayálam @msg the file holds two composite fonts named `{_1}` el archivo contiene dos fuentes compuestas llamadas `{_1}` @msg `{_1}` can't name a composite font `{_1}` no puede nombrar una fuente compuesta @msg {k}: give a font {k}: indique una fuente +@msg `{name}`: {k}: give a font `{name}`: {k}: indique una fuente @msg {k}: size must be {_1} to {_2}% {k}: size debe estar entre {_1} y {_2} % +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size debe estar entre {_1} y {_2} % @msg {k}: baseline must be {_1} to {_2}% {k}: baseline debe estar entre {_1} y {_2} % +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline debe estar entre {_1} y {_2} % @msg {k}: scale must be {_1} to {_2}% {k}: la escala debe estar entre {_1} y {_2} % +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: la escala debe estar entre {_1} y {_2} % @msg {k}: only kana, roman and numerals can be scaled {k}: solo kana, roman y numerals se pueden escalar +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: solo kana, roman y numerals se pueden escalar @msg {k}: only kana can be scaled at the center {k}: solo kana se puede escalar desde el centro +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: solo kana se puede escalar desde el centro @msg no composite font ninguna fuente compuesta Delete Font Eliminar fuente Kanji Kanji diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 469a79077e..86659781e9 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1651,11 +1651,17 @@ digits Malayalam Malajalam @msg the file holds two composite fonts named `{_1}` tiedostossa on kaksi yhdistelmäfonttia nimeltä `{_1}` @msg `{_1}` can't name a composite font `{_1}` ei kelpaa yhdistelmäfontin nimeksi @msg {k}: give a font {k}: anna fontti +@msg `{name}`: {k}: give a font `{name}`: {k}: anna fontti @msg {k}: size must be {_1} to {_2}% {k}: size on oltava {_1}–{_2} % +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size on oltava {_1}–{_2} % @msg {k}: baseline must be {_1} to {_2}% {k}: baseline on oltava {_1}–{_2} % +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline on oltava {_1}–{_2} % @msg {k}: scale must be {_1} to {_2}% {k}: skaalauksen on oltava {_1}–{_2} % +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skaalauksen on oltava {_1}–{_2} % @msg {k}: only kana, roman and numerals can be scaled {k}: vain kana, roman ja numerals voidaan skaalata +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: vain kana, roman ja numerals voidaan skaalata @msg {k}: only kana can be scaled at the center {k}: vain kana voidaan skaalata keskeltä +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: vain kana voidaan skaalata keskeltä @msg no composite font ei yhdistelmäfonttia Delete Font Poista fontti Kanji Kanji diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 14c2a7edd0..a1b7a53ef6 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3699,11 +3699,17 @@ digits Malayalam Malayalam @msg the file holds two composite fonts named `{_1}` le fichier contient deux polices composites nommées `{_1}` @msg `{_1}` can't name a composite font `{_1}` ne peut pas nommer une police composite @msg {k}: give a font {k} : indiquez une police +@msg `{name}`: {k}: give a font `{name}`: {k} : indiquez une police @msg {k}: size must be {_1} to {_2}% {k} : size doit être compris entre {_1} et {_2} % +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k} : size doit être compris entre {_1} et {_2} % @msg {k}: baseline must be {_1} to {_2}% {k} : baseline doit être compris entre {_1} et {_2} % +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k} : baseline doit être compris entre {_1} et {_2} % @msg {k}: scale must be {_1} to {_2}% {k} : l’échelle doit être comprise entre {_1} et {_2} % +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k} : l’échelle doit être comprise entre {_1} et {_2} % @msg {k}: only kana, roman and numerals can be scaled {k} : seuls kana, roman et numerals peuvent être mis à l’échelle +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k} : seuls kana, roman et numerals peuvent être mis à l’échelle @msg {k}: only kana can be scaled at the center {k} : seul kana peut être mis à l’échelle depuis le centre +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k} : seul kana peut être mis à l’échelle depuis le centre @msg no composite font aucune police composite Delete Font Supprimer la police Kanji Kanji diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 6b0144ae13..ec57c34ad3 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -5065,11 +5065,17 @@ antiAlias Sharp शार्प @msg the file holds two composite fonts named `{_1}` फ़ाइल में `{_1}` नाम के दो कंपोज़िट फ़ॉन्ट हैं @msg `{_1}` can't name a composite font `{_1}` कंपोज़िट फ़ॉन्ट का नाम नहीं हो सकता @msg {k}: give a font {k}: एक फ़ॉन्ट दें +@msg `{name}`: {k}: give a font `{name}`: {k}: एक फ़ॉन्ट दें @msg {k}: size must be {_1} to {_2}% {k}: size {_1} से {_2}% तक होना चाहिए +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size {_1} से {_2}% तक होना चाहिए @msg {k}: baseline must be {_1} to {_2}% {k}: baseline {_1} से {_2}% तक होना चाहिए +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline {_1} से {_2}% तक होना चाहिए @msg {k}: scale must be {_1} to {_2}% {k}: स्केल {_1} से {_2}% तक होना चाहिए +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: स्केल {_1} से {_2}% तक होना चाहिए @msg {k}: only kana, roman and numerals can be scaled {k}: केवल kana, roman और numerals को स्केल किया जा सकता है +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: केवल kana, roman और numerals को स्केल किया जा सकता है @msg {k}: only kana can be scaled at the center {k}: केंद्र से केवल kana को स्केल किया जा सकता है +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: केंद्र से केवल kana को स्केल किया जा सकता है @msg no composite font कोई कंपोज़िट फ़ॉन्ट नहीं Delete Font फ़ॉन्ट हटाएँ Kanji कांजी diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 9dc6095770..c8130d54a1 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4375,11 +4375,17 @@ mark-style Japanese Japán @msg the file holds two composite fonts named `{_1}` a fájl két `{_1}` nevű összetett betűtípust tartalmaz @msg `{_1}` can't name a composite font `{_1}` nem lehet összetett betűtípus neve @msg {k}: give a font {k}: adjon meg egy betűtípust +@msg `{name}`: {k}: give a font `{name}`: {k}: adjon meg egy betűtípust @msg {k}: size must be {_1} to {_2}% {k}: a size értéke {_1}–{_2}% lehet +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: a size értéke {_1}–{_2}% lehet @msg {k}: baseline must be {_1} to {_2}% {k}: a baseline értéke {_1}–{_2}% lehet +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: a baseline értéke {_1}–{_2}% lehet @msg {k}: scale must be {_1} to {_2}% {k}: a méretezés {_1}–{_2}% lehet +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: a méretezés {_1}–{_2}% lehet @msg {k}: only kana, roman and numerals can be scaled {k}: csak a kana, roman és numerals méretezhető +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: csak a kana, roman és numerals méretezhető @msg {k}: only kana can be scaled at the center {k}: csak a kana méretezhető a középpontból +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: csak a kana méretezhető a középpontból @msg no composite font nincs összetett betűtípus Delete Font Betűtípus törlése Kanji Kandzsi diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 8c92a434fe..19cc1215a1 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3753,11 +3753,17 @@ digits Malayalam Malayalam @msg the file holds two composite fonts named `{_1}` il file contiene due font compositi di nome `{_1}` @msg `{_1}` can't name a composite font `{_1}` non può essere il nome di un font composito @msg {k}: give a font {k}: indica un font +@msg `{name}`: {k}: give a font `{name}`: {k}: indica un font @msg {k}: size must be {_1} to {_2}% {k}: size deve essere tra {_1} e {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size deve essere tra {_1} e {_2}% @msg {k}: baseline must be {_1} to {_2}% {k}: baseline deve essere tra {_1} e {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline deve essere tra {_1} e {_2}% @msg {k}: scale must be {_1} to {_2}% {k}: la scala deve essere tra {_1} e {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: la scala deve essere tra {_1} e {_2}% @msg {k}: only kana, roman and numerals can be scaled {k}: solo kana, roman e numerals si possono ridimensionare +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: solo kana, roman e numerals si possono ridimensionare @msg {k}: only kana can be scaled at the center {k}: solo kana si può ridimensionare dal centro +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: solo kana si può ridimensionare dal centro @msg no composite font nessun font composito Delete Font Elimina font Kanji Kanji diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 8906c0e6c0..fe2e9f90bb 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4082,11 +4082,17 @@ file-filter Templates テンプレート @msg the file holds two composite fonts named `{_1}` ファイルに `{_1}` という名前の合成フォントが 2 つあります @msg `{_1}` can't name a composite font `{_1}` は合成フォントの名前にできません @msg {k}: give a font {k}: フォントを指定してください +@msg `{name}`: {k}: give a font `{name}`: {k}: フォントを指定してください @msg {k}: size must be {_1} to {_2}% {k}: size は {_1}~{_2}% にしてください +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size は {_1}~{_2}% にしてください @msg {k}: baseline must be {_1} to {_2}% {k}: baseline は {_1}~{_2}% にしてください +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline は {_1}~{_2}% にしてください @msg {k}: scale must be {_1} to {_2}% {k}: 比率は {_1}~{_2}% にしてください +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: 比率は {_1}~{_2}% にしてください @msg {k}: only kana, roman and numerals can be scaled {k}: 比率を変更できるのは kana、roman、numerals だけです +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: 比率を変更できるのは kana、roman、numerals だけです @msg {k}: only kana can be scaled at the center {k}: 中央を基準に比率を変更できるのは kana だけです +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: 中央を基準に比率を変更できるのは kana だけです @msg no composite font 合成フォントがありません Delete Font フォントを削除 Kanji 漢字 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index de2c7a7dc7..6b4826f8c3 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1522,11 +1522,17 @@ digits Malayalam მალაიალამური @msg the file holds two composite fonts named `{_1}` ფაილი შეიცავს ორ კომპოზიტურ შრიფტს სახელით `{_1}` @msg `{_1}` can't name a composite font `{_1}` ვერ იქნება კომპოზიტური შრიფტის სახელი @msg {k}: give a font {k}: მიუთითეთ შრიფტი +@msg `{name}`: {k}: give a font `{name}`: {k}: მიუთითეთ შრიფტი @msg {k}: size must be {_1} to {_2}% {k}: size უნდა იყოს {_1}-დან {_2}%-მდე +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size უნდა იყოს {_1}-დან {_2}%-მდე @msg {k}: baseline must be {_1} to {_2}% {k}: baseline უნდა იყოს {_1}-დან {_2}%-მდე +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline უნდა იყოს {_1}-დან {_2}%-მდე @msg {k}: scale must be {_1} to {_2}% {k}: მასშტაბი უნდა იყოს {_1}-დან {_2}%-მდე +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: მასშტაბი უნდა იყოს {_1}-დან {_2}%-მდე @msg {k}: only kana, roman and numerals can be scaled {k}: მასშტაბირება შეიძლება მხოლოდ kana, roman და numerals ნაწილებში +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: მასშტაბირება შეიძლება მხოლოდ kana, roman და numerals ნაწილებში @msg {k}: only kana can be scaled at the center {k}: ცენტრიდან მასშტაბირება შეიძლება მხოლოდ kana ნაწილში +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: ცენტრიდან მასშტაბირება შეიძლება მხოლოდ kana ნაწილში @msg no composite font კომპოზიტური შრიფტი არ არის Delete Font შრიფტის წაშლა Kanji კანძი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 2aa70c2d5d..ec34bd50f0 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4070,11 +4070,17 @@ file-filter Templates 템플릿 @msg the file holds two composite fonts named `{_1}` 파일에 이름이 `{_1}`인 합성 글꼴이 두 개 있습니다 @msg `{_1}` can't name a composite font `{_1}`은(는) 합성 글꼴 이름으로 쓸 수 없습니다 @msg {k}: give a font {k}: 글꼴을 지정하세요 +@msg `{name}`: {k}: give a font `{name}`: {k}: 글꼴을 지정하세요 @msg {k}: size must be {_1} to {_2}% {k}: size는 {_1}~{_2}%여야 합니다 +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size는 {_1}~{_2}%여야 합니다 @msg {k}: baseline must be {_1} to {_2}% {k}: baseline은 {_1}~{_2}%여야 합니다 +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline은 {_1}~{_2}%여야 합니다 @msg {k}: scale must be {_1} to {_2}% {k}: 비율은 {_1}~{_2}%여야 합니다 +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: 비율은 {_1}~{_2}%여야 합니다 @msg {k}: only kana, roman and numerals can be scaled {k}: 비율을 바꿀 수 있는 것은 kana, roman, numerals뿐입니다 +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: 비율을 바꿀 수 있는 것은 kana, roman, numerals뿐입니다 @msg {k}: only kana can be scaled at the center {k}: 가운데를 기준으로 비율을 바꿀 수 있는 것은 kana뿐입니다 +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: 가운데를 기준으로 비율을 바꿀 수 있는 것은 kana뿐입니다 @msg no composite font 합성 글꼴이 없습니다 Delete Font 글꼴 삭제 Kanji 한자 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 8b49530620..e2eacab182 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2102,11 +2102,17 @@ digits Malayalam Malayalam @msg the file holds two composite fonts named `{_1}` het bestand bevat twee samengestelde lettertypen met de naam `{_1}` @msg `{_1}` can't name a composite font `{_1}` kan geen naam van een samengesteld lettertype zijn @msg {k}: give a font {k}: geef een lettertype op +@msg `{name}`: {k}: give a font `{name}`: {k}: geef een lettertype op @msg {k}: size must be {_1} to {_2}% {k}: size moet tussen {_1} en {_2}% liggen +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size moet tussen {_1} en {_2}% liggen @msg {k}: baseline must be {_1} to {_2}% {k}: baseline moet tussen {_1} en {_2}% liggen +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline moet tussen {_1} en {_2}% liggen @msg {k}: scale must be {_1} to {_2}% {k}: de schaal moet tussen {_1} en {_2}% liggen +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: de schaal moet tussen {_1} en {_2}% liggen @msg {k}: only kana, roman and numerals can be scaled {k}: alleen kana, roman en numerals kunnen worden geschaald +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: alleen kana, roman en numerals kunnen worden geschaald @msg {k}: only kana can be scaled at the center {k}: alleen kana kan vanuit het midden worden geschaald +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: alleen kana kan vanuit het midden worden geschaald @msg no composite font geen samengesteld lettertype Delete Font Lettertype verwijderen Kanji Kanji diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index b80eee81d7..0becf9d142 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1590,11 +1590,17 @@ digits Malayalam Malajalam @msg the file holds two composite fonts named `{_1}` plik zawiera dwa fonty złożone o nazwie `{_1}` @msg `{_1}` can't name a composite font `{_1}` nie może być nazwą fontu złożonego @msg {k}: give a font {k}: podaj font +@msg `{name}`: {k}: give a font `{name}`: {k}: podaj font @msg {k}: size must be {_1} to {_2}% {k}: size musi wynosić od {_1} do {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size musi wynosić od {_1} do {_2}% @msg {k}: baseline must be {_1} to {_2}% {k}: baseline musi wynosić od {_1} do {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline musi wynosić od {_1} do {_2}% @msg {k}: scale must be {_1} to {_2}% {k}: skala musi wynosić od {_1} do {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skala musi wynosić od {_1} do {_2}% @msg {k}: only kana, roman and numerals can be scaled {k}: skalować można tylko kana, roman i numerals +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: skalować można tylko kana, roman i numerals @msg {k}: only kana can be scaled at the center {k}: od środka można skalować tylko kana +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: od środka można skalować tylko kana @msg no composite font brak fontu złożonego Delete Font Usuń font Kanji Kanji diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 479212841b..9a6a2d6082 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5267,11 +5267,17 @@ axis Both Ambos @msg the file holds two composite fonts named `{_1}` o arquivo contém duas fontes compostas chamadas `{_1}` @msg `{_1}` can't name a composite font `{_1}` não pode nomear uma fonte composta @msg {k}: give a font {k}: informe uma fonte +@msg `{name}`: {k}: give a font `{name}`: {k}: informe uma fonte @msg {k}: size must be {_1} to {_2}% {k}: size deve estar entre {_1} e {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size deve estar entre {_1} e {_2}% @msg {k}: baseline must be {_1} to {_2}% {k}: baseline deve estar entre {_1} e {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline deve estar entre {_1} e {_2}% @msg {k}: scale must be {_1} to {_2}% {k}: a escala deve estar entre {_1} e {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: a escala deve estar entre {_1} e {_2}% @msg {k}: only kana, roman and numerals can be scaled {k}: só kana, roman e numerals podem ser escalados +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: só kana, roman e numerals podem ser escalados @msg {k}: only kana can be scaled at the center {k}: só kana pode ser escalado a partir do centro +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: só kana pode ser escalado a partir do centro @msg no composite font nenhuma fonte composta Delete Font Excluir fonte Kanji Kanji diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 07b1369c88..9f3c78c1d1 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3739,11 +3739,17 @@ digits Malayalam Малаялам @msg the file holds two composite fonts named `{_1}` в файле два составных шрифта с именем `{_1}` @msg `{_1}` can't name a composite font `{_1}` не может быть именем составного шрифта @msg {k}: give a font {k}: укажите шрифт +@msg `{name}`: {k}: give a font `{name}`: {k}: укажите шрифт @msg {k}: size must be {_1} to {_2}% {k}: size должен быть от {_1} до {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size должен быть от {_1} до {_2}% @msg {k}: baseline must be {_1} to {_2}% {k}: baseline должен быть от {_1} до {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline должен быть от {_1} до {_2}% @msg {k}: scale must be {_1} to {_2}% {k}: масштаб должен быть от {_1} до {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: масштаб должен быть от {_1} до {_2}% @msg {k}: only kana, roman and numerals can be scaled {k}: масштабировать можно только kana, roman и numerals +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: масштабировать можно только kana, roman и numerals @msg {k}: only kana can be scaled at the center {k}: масштабировать от центра можно только kana +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: масштабировать от центра можно только kana @msg no composite font нет составного шрифта Delete Font Удалить шрифт Kanji Кандзи diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index dc0b4fc494..42e71340e5 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3629,11 +3629,17 @@ digits Malayalam Malayalam @msg the file holds two composite fonts named `{_1}` filen innehåller två sammansatta typsnitt som heter `{_1}` @msg `{_1}` can't name a composite font `{_1}` kan inte vara namnet på ett sammansatt typsnitt @msg {k}: give a font {k}: ange ett typsnitt +@msg `{name}`: {k}: give a font `{name}`: {k}: ange ett typsnitt @msg {k}: size must be {_1} to {_2}% {k}: size måste vara {_1} till {_2} % +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size måste vara {_1} till {_2} % @msg {k}: baseline must be {_1} to {_2}% {k}: baseline måste vara {_1} till {_2} % +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline måste vara {_1} till {_2} % @msg {k}: scale must be {_1} to {_2}% {k}: skalan måste vara {_1} till {_2} % +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skalan måste vara {_1} till {_2} % @msg {k}: only kana, roman and numerals can be scaled {k}: bara kana, roman och numerals kan skalas +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: bara kana, roman och numerals kan skalas @msg {k}: only kana can be scaled at the center {k}: bara kana kan skalas från mitten +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: bara kana kan skalas från mitten @msg no composite font inget sammansatt typsnitt Delete Font Ta bort typsnitt Kanji Kanji diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index 671ce6ce92..3bc53ebb8c 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -2103,6 +2103,12 @@ fn composite_font_errors_translate_in_complete_languages() { "{k}: scale must be {_1} to {_2}%", "{k}: only kana, roman and numerals can be scaled", "{k}: only kana can be scaled at the center", + "`{name}`: {k}: give a font", + "`{name}`: {k}: size must be {_1} to {_2}%", + "`{name}`: {k}: baseline must be {_1} to {_2}%", + "`{name}`: {k}: scale must be {_1} to {_2}%", + "`{name}`: {k}: only kana, roman and numerals can be scaled", + "`{name}`: {k}: only kana can be scaled at the center", "no composite font", ]; // An error, English words its translation replaces, and the text it keeps. @@ -4077,6 +4083,10 @@ const NAMED_KEYS: &[(&str, &[&str])] = &[ ("{k}: only kana can be scaled at the center", &["kana"]), ("{k}: only kana, roman and numerals can be scaled", &["kana", "roman", "numerals"]), ("{k}: size must be {_1} to {_2}%", &["size"]), + ("`{name}`: {k}: baseline must be {_1} to {_2}%", &["baseline"]), + ("`{name}`: {k}: only kana can be scaled at the center", &["kana"]), + ("`{name}`: {k}: only kana, roman and numerals can be scaled", &["kana", "roman", "numerals"]), + ("`{name}`: {k}: size must be {_1} to {_2}%", &["size"]), ]; /// Every catalog's translation of a [`NAMED_KEYS`] message keeps each name the table lists, as diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index da3b7bd26a..36572fa017 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3493,11 +3493,17 @@ digits Malayalam Malayalam @msg the file holds two composite fonts named `{_1}` dosyada `{_1}` adlı iki bileşik font var @msg `{_1}` can't name a composite font `{_1}` bir bileşik fontun adı olamaz @msg {k}: give a font {k}: bir font verin +@msg `{name}`: {k}: give a font `{name}`: {k}: bir font verin @msg {k}: size must be {_1} to {_2}% {k}: size %{_1} ile %{_2} arasında olmalı +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size %{_1} ile %{_2} arasında olmalı @msg {k}: baseline must be {_1} to {_2}% {k}: baseline %{_1} ile %{_2} arasında olmalı +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline %{_1} ile %{_2} arasında olmalı @msg {k}: scale must be {_1} to {_2}% {k}: ölçek %{_1} ile %{_2} arasında olmalı +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: ölçek %{_1} ile %{_2} arasında olmalı @msg {k}: only kana, roman and numerals can be scaled {k}: yalnızca kana, roman ve numerals ölçeklenebilir +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: yalnızca kana, roman ve numerals ölçeklenebilir @msg {k}: only kana can be scaled at the center {k}: merkezden yalnızca kana ölçeklenebilir +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: merkezden yalnızca kana ölçeklenebilir @msg no composite font bileşik font yok Delete Font Fontu Sil Kanji Kanji diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 243479fcb3..1a5b662ecd 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3746,11 +3746,17 @@ effect Points Точки @msg the file holds two composite fonts named `{_1}` у файлі два складені шрифти з назвою `{_1}` @msg `{_1}` can't name a composite font `{_1}` не може бути назвою складеного шрифту @msg {k}: give a font {k}: вкажіть шрифт +@msg `{name}`: {k}: give a font `{name}`: {k}: вкажіть шрифт @msg {k}: size must be {_1} to {_2}% {k}: size має бути від {_1} до {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size має бути від {_1} до {_2}% @msg {k}: baseline must be {_1} to {_2}% {k}: baseline має бути від {_1} до {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline має бути від {_1} до {_2}% @msg {k}: scale must be {_1} to {_2}% {k}: масштаб має бути від {_1} до {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: масштаб має бути від {_1} до {_2}% @msg {k}: only kana, roman and numerals can be scaled {k}: масштабувати можна лише kana, roman і numerals +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: масштабувати можна лише kana, roman і numerals @msg {k}: only kana can be scaled at the center {k}: масштабувати від центру можна лише kana +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: масштабувати від центру можна лише kana @msg no composite font немає складеного шрифту Delete Font Видалити шрифт Kanji Кандзі diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 7698b3d145..2fe710c328 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4092,11 +4092,17 @@ mark-style Japanese Kiểu Nhật @msg the file holds two composite fonts named `{_1}` tệp chứa hai phông chữ tổ hợp cùng tên `{_1}` @msg `{_1}` can't name a composite font `{_1}` không thể là tên phông chữ tổ hợp @msg {k}: give a font {k}: hãy cung cấp một phông chữ +@msg `{name}`: {k}: give a font `{name}`: {k}: hãy cung cấp một phông chữ @msg {k}: size must be {_1} to {_2}% {k}: size phải từ {_1} đến {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size phải từ {_1} đến {_2}% @msg {k}: baseline must be {_1} to {_2}% {k}: baseline phải từ {_1} đến {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline phải từ {_1} đến {_2}% @msg {k}: scale must be {_1} to {_2}% {k}: tỷ lệ phải từ {_1} đến {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: tỷ lệ phải từ {_1} đến {_2}% @msg {k}: only kana, roman and numerals can be scaled {k}: chỉ kana, roman và numerals mới co giãn được +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: chỉ kana, roman và numerals mới co giãn được @msg {k}: only kana can be scaled at the center {k}: chỉ kana mới co giãn từ tâm được +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: chỉ kana mới co giãn từ tâm được @msg no composite font không có phông chữ tổ hợp Delete Font Xóa phông chữ Kanji Kanji diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 4f796f9090..c7cbd36aa6 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3568,11 +3568,17 @@ digits Malayalam 马拉雅拉姆数字 @msg the file holds two composite fonts named `{_1}` 文件中有两个复合字体都名为 `{_1}` @msg `{_1}` can't name a composite font `{_1}` 不能用作复合字体的名称 @msg {k}: give a font {k}:请指定字体 +@msg `{name}`: {k}: give a font `{name}`: {k}:请指定字体 @msg {k}: size must be {_1} to {_2}% {k}:size 必须为 {_1}% 到 {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}:size 必须为 {_1}% 到 {_2}% @msg {k}: baseline must be {_1} to {_2}% {k}:baseline 必须为 {_1}% 到 {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}:baseline 必须为 {_1}% 到 {_2}% @msg {k}: scale must be {_1} to {_2}% {k}:缩放必须为 {_1}% 到 {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}:缩放必须为 {_1}% 到 {_2}% @msg {k}: only kana, roman and numerals can be scaled {k}:只有 kana、roman 和 numerals 可以缩放 +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}:只有 kana、roman 和 numerals 可以缩放 @msg {k}: only kana can be scaled at the center {k}:只有 kana 可以从中心缩放 +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}:只有 kana 可以从中心缩放 @msg no composite font 没有复合字体 Delete Font 删除字体 Kanji 汉字 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 4af3d3a822..5e74d83d12 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4823,11 +4823,17 @@ digits Malayalam 馬來亞拉姆數字 @msg the file holds two composite fonts named `{_1}` 檔案中有兩個複合字體都名為 `{_1}` @msg `{_1}` can't name a composite font `{_1}` 不能作為複合字體的名稱 @msg {k}: give a font {k}:請指定字體 +@msg `{name}`: {k}: give a font `{name}`: {k}:請指定字體 @msg {k}: size must be {_1} to {_2}% {k}:size 必須為 {_1}% 到 {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}:size 必須為 {_1}% 到 {_2}% @msg {k}: baseline must be {_1} to {_2}% {k}:baseline 必須為 {_1}% 到 {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}:baseline 必須為 {_1}% 到 {_2}% @msg {k}: scale must be {_1} to {_2}% {k}:縮放必須為 {_1}% 到 {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}:縮放必須為 {_1}% 到 {_2}% @msg {k}: only kana, roman and numerals can be scaled {k}:只有 kana、roman 和 numerals 可以縮放 +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}:只有 kana、roman 和 numerals 可以縮放 @msg {k}: only kana can be scaled at the center {k}:只有 kana 可以從中心縮放 +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}:只有 kana 可以從中心縮放 @msg no composite font 沒有複合字體 Delete Font 刪除字體 Kanji 漢字 From a134cf6f8dee085c54b349293e1c83d67a3e5345 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 10:27:41 -0800 Subject: [PATCH 05/28] Graphs: Graph Type is a dialog of its own with type buttons and a page per axis Object > Graph > Type... and a double click on a graph tool open Graph Type (dialog `graphType`) instead of the generic parameter dialog. A menu at the top shows Graph Options, Value Axis (Left Axis and Right Axis with the value axis on both sides) or Category Axis. Graph Options has a button per graph type, Value Axis with Separate Scales, the Style checkboxes and the options of the type picked. The axis pages hold Tick Values with Override Calculated Values, Tick Marks and Add Labels. Override Calculated Values starts from the values the axis would be calculated with for the options shown; the engine exposes them as `graph_type_subject` and `graph_calculated_axes`. With series picked by Group Selection, the type and value axis go to those series. OK changes the graph and series the dialog opened on, and with nothing changed it adds no step of history. Docs: docs/mcp.md, docs/ui-parity.md and the tool.options text. --- crates/engine/src/cmd/graph.rs | 2 + crates/engine/src/cmd/graph/edit.rs | 24 +- crates/engine/src/cmd/graph/generate.rs | 131 ++- crates/engine/src/cmd/graph/tests.rs | 52 + crates/engine/src/lib.rs | 1 + crates/mcp/src/ui_catalog.rs | 2 +- crates/ui-egui/src/dialogs.rs | 4 + crates/ui-egui/src/dialogs/command.rs | 59 - crates/ui-egui/src/dialogs/graph_type.rs | 1314 ++++++++++++++++++++++ crates/ui-egui/src/i18n/bg.tsv | 34 + crates/ui-egui/src/i18n/bs.tsv | 34 + crates/ui-egui/src/i18n/ca.tsv | 34 + crates/ui-egui/src/i18n/cs.tsv | 34 + crates/ui-egui/src/i18n/da.tsv | 34 + crates/ui-egui/src/i18n/de.tsv | 34 + crates/ui-egui/src/i18n/el.tsv | 34 + crates/ui-egui/src/i18n/es.tsv | 34 + crates/ui-egui/src/i18n/fi.tsv | 34 + crates/ui-egui/src/i18n/fr.tsv | 34 + crates/ui-egui/src/i18n/gu.tsv | 34 + crates/ui-egui/src/i18n/hi.tsv | 34 + crates/ui-egui/src/i18n/hu.tsv | 34 + crates/ui-egui/src/i18n/id.tsv | 34 + crates/ui-egui/src/i18n/it.tsv | 34 + crates/ui-egui/src/i18n/ja.tsv | 34 + crates/ui-egui/src/i18n/ka.tsv | 34 + crates/ui-egui/src/i18n/ko.tsv | 34 + crates/ui-egui/src/i18n/nl.tsv | 34 + crates/ui-egui/src/i18n/pl.tsv | 34 + crates/ui-egui/src/i18n/pt-br.tsv | 34 + crates/ui-egui/src/i18n/ru.tsv | 34 + crates/ui-egui/src/i18n/sv.tsv | 34 + crates/ui-egui/src/i18n/tr.tsv | 34 + crates/ui-egui/src/i18n/uk.tsv | 34 + crates/ui-egui/src/i18n/vi.tsv | 34 + crates/ui-egui/src/i18n/zh-hans.tsv | 34 + crates/ui-egui/src/i18n/zh-hant.tsv | 34 + crates/ui-egui/src/menus/invoke.rs | 4 +- crates/ui-egui/src/menus/ui_commands.rs | 2 +- crates/ui-egui/src/toolbar/tests.rs | 4 +- docs/mcp.md | 25 +- docs/ui-parity.md | 1 + 42 files changed, 2474 insertions(+), 103 deletions(-) create mode 100644 crates/ui-egui/src/dialogs/graph_type.rs diff --git a/crates/engine/src/cmd/graph.rs b/crates/engine/src/cmd/graph.rs index da23bd4f04..fe33e30036 100644 --- a/crates/engine/src/cmd/graph.rs +++ b/crates/engine/src/cmd/graph.rs @@ -40,6 +40,8 @@ pub use commands::specs; pub(crate) use data::apply_csv; #[cfg(test)] use data::{fmt_value, parse_csv, to_csv}; +pub use edit::{GraphTypeSubject, graph_type_subject}; +pub use generate::calculated_axes; #[cfg(test)] use generate::generate; #[cfg(test)] diff --git a/crates/engine/src/cmd/graph/edit.rs b/crates/engine/src/cmd/graph/edit.rs index c7f6d52ea8..283c46e956 100644 --- a/crates/engine/src/cmd/graph/edit.rs +++ b/crates/engine/src/cmd/graph/edit.rs @@ -1,6 +1,6 @@ use super::axis::{MAX_TICK_MARKS, count_given, one_line}; use super::data::apply_data; -use super::generate::{children_bounds, generate}; +use super::generate::{calculated_axes, children_bounds, generate}; use super::series::{picked_series, reselect_series}; use super::{ Affine, Document, EngineError, GraphKind, GraphSpec, Node, NodeId, Rect, Result, Session, TickLength, Value, ValueAxisSide, bad, f64_req, json, @@ -97,6 +97,28 @@ pub(super) fn spec_of(s: &Session, id: NodeId) -> Result { s.doc()?.doc.node(id).and_then(|n| n.extras().graph.as_deref().cloned()).ok_or(EngineError::NoNode(id)) } +/// What `graph.setType` without `id` or `seriesIndexes` acts on, for the Graph Type dialog. +pub struct GraphTypeSubject { + /// The selected graph, or the graph that holds the selection. + pub id: NodeId, + /// The series picked in it with Group Selection, in order; empty when the type goes to the whole graph. + pub series: Vec, + /// The least value, the greatest and the number of divisions each value axis is drawn with while its tick values + /// are calculated: the left (or only) axis, then the right axis' own scale (the left one's without Separate Scales). + pub calculated: [(f64, f64, usize); 2], +} + +/// The graph and series that `graph.setType {}` reads its options from, as [`GraphTypeSubject`]. An error without a +/// selected graph, as the command gives. +pub fn graph_type_subject(s: &Session) -> Result { + const C: &str = "graph.setType"; + let p = json!({}); + let id = target(s, &p, C)?; + let spec = spec_of(s, id)?; + let count = spec.rows.iter().map(Vec::len).max().unwrap_or(0).min(vectorcraft_doc::MAX_GRAPH_SERIES); + Ok(GraphTypeSubject { id, series: picked_series(s, &p, id, count, C)?, calculated: calculated_axes(&spec) }) +} + fn query_type_options(spec: &GraphSpec, count: usize, picked: &[usize]) -> Value { // With series picked, the type and value axis they share, so the dialog's OK keeps them. let kinds: Vec = picked.iter().map(|i| spec.series_kind(*i)).collect(); diff --git a/crates/engine/src/cmd/graph/generate.rs b/crates/engine/src/cmd/graph/generate.rs index fe8ffb98f8..c31934f999 100644 --- a/crates/engine/src/cmd/graph/generate.rs +++ b/crates/engine/src/cmd/graph/generate.rs @@ -38,6 +38,101 @@ fn nth(list: &[Option], s: usize) -> Option<&T> { list.get(s).and_then(Option::as_ref) } +/// The data a value range is read from: each series' type, the cells (a blank is `None`), which series use the right +/// axis' own scale, and the number of categories. +struct Values<'a> { + kinds: &'a [GraphKind], + cells: &'a [Vec>], + on_right: &'a [bool], + ncat: usize, +} + +impl Values<'_> { + fn cell(&self, c: usize, s: usize) -> Option { + self.cells.get(c).and_then(|r| r.get(s)).copied().flatten() + } + + fn axis(&self, s: usize) -> usize { + usize::from(self.on_right.get(s).copied().unwrap_or(false)) + } + + /// The value range per axis (stacked graphs stack positives and negatives separately), and a scatter's x range. + fn range(&self, scatter: bool) -> ([f64; 2], [f64; 2], f64, f64) { + let (mut lo, mut hi) = ([0.0f64; 2], [0.0f64; 2]); + let (mut xlo, mut xhi) = (f64::MAX, f64::MIN); + for c in 0..self.ncat { + if !scatter { + for (a, (lo, hi)) in lo.iter_mut().zip(hi.iter_mut()).enumerate() { + self.extend(c, a, (lo, hi)); + } + } else { + for s in (0..self.kinds.len()).step_by(2) { + if let (Some(y), Some(x)) = (self.cell(c, s), self.cell(c, s + 1)) { + hi[0] = hi[0].max(y); + lo[0] = lo[0].min(y); + xlo = xlo.min(x); + xhi = xhi.max(x); + } + } + } + } + (lo, hi, xlo, xhi) + } + + /// Widen the range of axis `a` by category `c`'s values. + fn extend(&self, c: usize, a: usize, (lo, hi): (&mut f64, &mut f64)) { + let val = |s: usize| self.cell(c, s).unwrap_or(0.0); + let on = |s: &usize, kinds: &[GraphKind]| self.axis(*s) == a && self.kinds.get(*s).is_some_and(|k| kinds.contains(k)); + // Stacked columns (or bars) stack together, and so do areas; other series count one value each. + for stack in [[GraphKind::StackedColumn, GraphKind::StackedBar], [GraphKind::Area, GraphKind::Area]] { + let members = || (0..self.kinds.len()).filter(|s| on(s, &stack)); + *hi = hi.max(members().map(|s| val(s).max(0.0)).sum()); + *lo = lo.min(members().map(|s| val(s).min(0.0)).sum()); + } + let single = |s: &usize| on(s, &[GraphKind::Column, GraphKind::Bar, GraphKind::Line]); + for v in (0..self.kinds.len()).filter(single).filter_map(|s| self.cell(c, s)) { + *hi = hi.max(v); + *lo = lo.min(v); + } + } +} + +/// The tick values `g`'s value axes are drawn with while they are calculated from the data (Graph Type › Tick Values +/// without Override Calculated Values): the least value, the greatest and the number of divisions of the left (or +/// only) axis, then of the right axis' own scale, which is the left one's unless Separate Scales gives the series on +/// the right axis a scale of their own. A pie graph has no value axis and gives the axis of a graph without data. A +/// single bound the graph has (only a minimum or only a maximum) is calculated with, as the axis is drawn. +pub fn calculated_axes(g: &GraphSpec) -> [(f64, f64, usize); 2] { + // Adding 0 makes a negative zero, which a nice axis around 0 can start at, a plain 0. + let divisions = |(lo, hi, step): (f64, f64, f64)| (lo + 0.0, hi + 0.0, ((hi - lo) / step).round().clamp(1.0, 100.0) as usize); + // The bounds the axis is calculated with: all but a pair that overrides the calculation. + let partial = |min: Option, max: Option| match (min, max) { + (Some(a), Some(b)) if b > a && (b - a).is_finite() => (None, None), + bounds => bounds, + }; + let (min, max) = partial(g.axis_min, g.axis_max); + if g.kind == GraphKind::Radar { + let top = g.rows.iter().flatten().copied().fold(0.0, f64::max); + return [divisions(value_axis(g.ticks, min, max, 0.0, top)); 2]; + } + let nser = g.rows.iter().map(Vec::len).max().unwrap_or(0).clamp(1, vectorcraft_doc::MAX_GRAPH_SERIES); + let kinds: Vec = (0..nser).map(|s| g.series_kind(s)).collect(); + let (scatter, horizontal) = (g.kind == GraphKind::Scatter, matches!(g.kind, GraphKind::Bar | GraphKind::StackedBar)); + let separate = g.separate_scales && g.value_axis == ValueAxisSide::Both && !horizontal && !scatter; + let on_right: Vec = (0..nser).map(|s| separate && g.right_series.contains(&s)).collect(); + let cells = if g.kind == GraphKind::Pie { vec![] } else { g.cells() }; + let ncat = g.rows.len().clamp(1, vectorcraft_doc::MAX_GRAPH_CATEGORIES); + let (lo, hi, ..) = Values { kinds: &kinds, cells: &cells, on_right: &on_right, ncat }.range(scatter); + let left = divisions(value_axis(g.ticks, min, max, lo[0], hi[0])); + let used = |right: bool| on_right.contains(&right); + if !used(true) { + return [left; 2]; + } + let (right_min, right_max) = partial(g.right_axis_min, g.right_axis_max); + let right = divisions(value_axis(g.right_ticks, right_min, right_max, lo[1], hi[1])); + if used(false) { [left, right] } else { [right; 2] } +} + /// One graph being generated: its data, the nodes drawn so far and what the legend and shadows still need. struct Plot<'a> { g: &'a GraphSpec, @@ -526,41 +621,7 @@ impl<'a> Plot<'a> { /// The value range per axis (stacked graphs stack positives and negatives separately), and a scatter's x range. fn value_range(&self, geo: &Geo, scatter: bool) -> ([f64; 2], [f64; 2], f64, f64) { - let (mut lo, mut hi) = ([0.0f64; 2], [0.0f64; 2]); - let (mut xlo, mut xhi) = (f64::MAX, f64::MIN); - for c in 0..self.ncat { - if !scatter { - for (a, (lo, hi)) in lo.iter_mut().zip(hi.iter_mut()).enumerate() { - self.extend_range(geo, c, a, (lo, hi)); - } - } else { - for s in (0..self.nser).step_by(2) { - if let (Some(y), Some(x)) = (self.cell(c, s), self.cell(c, s + 1)) { - hi[0] = hi[0].max(y); - lo[0] = lo[0].min(y); - xlo = xlo.min(x); - xhi = xhi.max(x); - } - } - } - } - (lo, hi, xlo, xhi) - } - - /// Widen the range of axis `a` by category `c`'s values. - fn extend_range(&self, geo: &Geo, c: usize, a: usize, (lo, hi): (&mut f64, &mut f64)) { - let nser = self.nser; - // Stacked columns (or bars) stack together, and so do areas; other series count one value each. - for stack in [[GraphKind::StackedColumn, GraphKind::StackedBar], [GraphKind::Area, GraphKind::Area]] { - let members = || (0..nser).filter(|s| geo.axis(*s) == a && stack.contains(&self.kind(*s))); - *hi = hi.max(members().map(|s| self.val(c, s).max(0.0)).sum()); - *lo = lo.min(members().map(|s| self.val(c, s).min(0.0)).sum()); - } - let single = |s: &usize| geo.axis(*s) == a && matches!(self.kind(*s), GraphKind::Column | GraphKind::Bar | GraphKind::Line); - for v in (0..nser).filter(single).filter_map(|s| self.cell(c, s)) { - *hi = hi.max(v); - *lo = lo.min(v); - } + Values { kinds: &self.kinds, cells: &self.cells, on_right: &geo.on_right, ncat: self.ncat }.range(scatter) } /// The value axes with ticks and labels, the legend's start after them, and the axis lines. diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index 0a788c4330..da176ea49c 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -3848,6 +3848,58 @@ fn group_selected_series_go_on_the_chosen_axis() { assert!(spec(&s, id).right_series.is_empty()); } +/// The Graph Type dialog's subject: the selected graph, the series picked in it, and the tick values each value axis +/// is drawn with while they are calculated, which are the ones the graph's value labels show. +#[test] +fn graph_type_subject_gives_the_picked_series_and_the_calculated_tick_values() { + let mut s = Session::new(); + assert!(super::graph_type_subject(&s).is_err(), "no document"); + s.execute("file.new", &json!({"width": 800, "height": 800})).unwrap(); + assert!(super::graph_type_subject(&s).is_err(), "no graph selected"); + // Q1 3, 2; Q2 5, 4; Q3 -1, 6. + let id = graph(&mut s, "column"); + let subject = super::graph_type_subject(&s).unwrap(); + assert_eq!((subject.id, subject.series), (id, vec![])); + assert_eq!(subject.calculated, [(-2.0, 6.0, 4); 2]); + let shown = |s: &Session| value_labels(s, id).into_iter().map(|(t, _)| t).collect::>(); + assert_eq!(shown(&s), ["-2", "0", "2", "4", "6"]); + // Overriding with them draws the same axis, and the calculated values stay the data's. + s.execute("graph.setType", &json!({"axisMin": -2, "axisMax": 6, "ticks": 4})).unwrap(); + assert_eq!(shown(&s), ["-2", "0", "2", "4", "6"]); + s.execute("graph.setType", &json!({"axisMin": 0, "axisMax": 100, "ticks": 0})).unwrap(); + assert_eq!(super::graph_type_subject(&s).unwrap().calculated[0], (-2.0, 6.0, 4)); + // A single bound is calculated with, as the axis is drawn. + s.execute("graph.setType", &json!({"axisMin": -10, "axisMax": null})).unwrap(); + let (lo, hi, _) = super::graph_type_subject(&s).unwrap().calculated[0]; + let labels = shown(&s); + assert!(lo <= -10.0, "{lo}"); + assert_eq!((labels.first().and_then(|t| t.parse::().ok()), labels.last().and_then(|t| t.parse::().ok())), (Some(lo), Some(hi))); + s.execute("graph.setType", &json!({"axisMin": null, "axisMax": null})).unwrap(); + // Stacked columns stack their positive and their negative values: Q2 reaches 9, Q3 from -1 to 6. + s.execute("graph.setType", &json!({"type": "stackedColumn"})).unwrap(); + assert_eq!(super::graph_type_subject(&s).unwrap().calculated[0], (-2.0, 10.0, 6)); + assert_eq!(shown(&s), ["-2", "0", "2", "4", "6", "8", "10"]); + // A radar graph's axis starts at 0, and a pie graph has none. + s.execute("graph.setType", &json!({"type": "radar"})).unwrap(); + assert_eq!(super::graph_type_subject(&s).unwrap().calculated, [(0.0, 6.0, 3); 2]); + s.execute("graph.setType", &json!({"type": "pie"})).unwrap(); + assert_eq!(super::graph_type_subject(&s).unwrap().calculated, [(0.0, 1.0, 5); 2]); + // Series picked with Group Selection. + s.execute("graph.setType", &json!({"type": "column"})).unwrap(); + let picked = series(s.doc().unwrap().doc.node(id).unwrap(), 1).id; + s.execute("select.set", &json!({"ids": [picked.0]})).unwrap(); + let subject = super::graph_type_subject(&s).unwrap(); + assert_eq!((subject.id, subject.series), (id, vec![1])); + // Separate Scales: the right axis has the tick values of its own series. + let mut s = Session::new(); + let id = two_scale_graph(&mut s); + assert_eq!(super::graph_type_subject(&s).unwrap().calculated, [(0.0, 1000.0, 5); 2]); + s.execute("graph.setType", &json!({"seriesIndexes": [1], "valueAxis": "right"})).unwrap(); + s.execute("graph.setType", &json!({"valueAxis": "both", "separateScales": true})).unwrap(); + assert_eq!(super::graph_type_subject(&s).unwrap().calculated, [(0.0, 10.0, 5), (0.0, 1000.0, 5)]); + assert_eq!(spec(&s, id).right_series, [1]); +} + #[test] fn bar_graphs_keep_their_value_axis_along_the_bottom() { let mut s = Session::new(); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 4b47966463..9c097c190b 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -29,6 +29,7 @@ use vectorcraft_tools::{PaintDefaults, Tool}; pub use cmd::clipboard::{Clipboard, SourceLayer}; pub use cmd::create::constrain_click; pub use cmd::distortcmds::perspective_click; +pub use cmd::graph::{GraphTypeSubject, calculated_axes as graph_calculated_axes, graph_type_subject}; pub use cmd::params::{check_keys, check_params}; pub use cmd::pathops::auto_simplify_level; pub use cmd::rasterfx::{export_pdf, flatten_raster_effects}; diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index e801df4a7a..d08a38ae8a 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -937,7 +937,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "tool.options", "Tool Options…", "", - "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x, y and preview (on initially), previews one there and accepts it as one Undo step; Cancel rolls back the preview), lineSegment|arc|rectangularGrid|polarGrid → that tool's Tool Options, its click dialog without x and y (dialog kind = the tool id, `__toolOptions: true`; lineSegment: length (pt), angle (°), fillLine; arc: lengthX, lengthY (pt), reference (0–8), arcType: open|closed, baseAlong: x|y, slope (−100…100), fillArc; rectangularGrid: width, height (pt, the Default Size), reference (0–8), rows, columns, rowSkew, columnSkew (%), frame, fill; polarGrid: the same with concentric, radial, concentricSkew, radialSkew and compound for rows, columns, the skews and frame; OK runs tool.setOption {tool, values} and draws nothing: the next drag uses them (Fill Line, the arc's type, base, slope and fill, the grids' dividers, skews and switches) and a click's dialog opens at them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), typeOnPath|verticalTypeOnPath → Type on a Path Options for the selected type on a path (dialog `command` running type.pathOptions; error `nothing selected` without a selection, and the command's error when no type on a path is selected),symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (px for pencil, paintbrush and smooth, the same at any zoom; pt for blobBrush), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), livePreview (pencil, on: off draws a thin line while the stroke is drawn, and the path's fill and stroke on release), size (pt), angle (°, −180–180; 0) and roundness (%, 1–100; 100) of the eraser's tip; OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), livePaintBucket → Live Paint Bucket Options (dialog `livePaintBucketOptions`, fields paintFills (on) and paintStrokes (off), cursorSwatchPreview (on: the active fill or stroke shows above the pointer between the color swatches before and after it; Left and Right without modifiers step it through the color swatches whether it is on or off), highlight (on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the face highlight's width, 1–20 px; 4; an edge's highlight is half as wide); OK runs tool.setOption {tool: \"livePaintBucket\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values}; a highlightColor with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), objectsOnPath → Objects on Path Tool Options (dialog `objectsOnPathOptions`, fields pivot (0–8, row by row from the top left; 4, the center) and rotate (°, −360–360; 0); Reset puts 4 and 0 back; Save runs tool.setOption {tool: \"objectsOnPath\", values}: the objects attached next use them, groups attached before keep theirs)", + "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x, y and preview (on initially), previews one there and accepts it as one Undo step; Cancel rolls back the preview), lineSegment|arc|rectangularGrid|polarGrid → that tool's Tool Options, its click dialog without x and y (dialog kind = the tool id, `__toolOptions: true`; lineSegment: length (pt), angle (°), fillLine; arc: lengthX, lengthY (pt), reference (0–8), arcType: open|closed, baseAlong: x|y, slope (−100…100), fillArc; rectangularGrid: width, height (pt, the Default Size), reference (0–8), rows, columns, rowSkew, columnSkew (%), frame, fill; polarGrid: the same with concentric, radial, concentricSkew, radialSkew and compound for rows, columns, the skews and frame; OK runs tool.setOption {tool, values} and draws nothing: the next drag uses them (Fill Line, the arc's type, base, slope and fill, the grids' dividers, skews and switches) and a click's dialog opens at them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph, or for the series picked in it with Group Selection (dialog `graphType`, fields the options graph.setType {} returns under their names, with legend, pieLegend, piePosition, pieSort, firstRowInFront, firstColumnInFront and filledLines held for every type; overrideValues and rightOverrideValues, Override Calculated Values of the left or only value axis and of the right one: while one is on OK sends axisMin, axisMax and ticks, or rightAxisMin, rightAxisMax and rightTicks, filled with the calculated values where the graph has none, and one turned off sends null, null and 0; __page: options|valueAxis|rightAxis|categoryAxis, the page shown, options when the type picked has no such page; OK runs graph.setType, and with nothing changed closes without a step of history; an overridden value axis whose maximum is not more than its minimum is an error and the dialog stays open; an error without a selected graph), typeOnPath|verticalTypeOnPath → Type on a Path Options for the selected type on a path (dialog `command` running type.pathOptions; error `nothing selected` without a selection, and the command's error when no type on a path is selected),symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (px for pencil, paintbrush and smooth, the same at any zoom; pt for blobBrush), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), livePreview (pencil, on: off draws a thin line while the stroke is drawn, and the path's fill and stroke on release), size (pt), angle (°, −180–180; 0) and roundness (%, 1–100; 100) of the eraser's tip; OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), livePaintBucket → Live Paint Bucket Options (dialog `livePaintBucketOptions`, fields paintFills (on) and paintStrokes (off), cursorSwatchPreview (on: the active fill or stroke shows above the pointer between the color swatches before and after it; Left and Right without modifiers step it through the color swatches whether it is on or off), highlight (on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the face highlight's width, 1–20 px; 4; an edge's highlight is half as wide); OK runs tool.setOption {tool: \"livePaintBucket\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values}; a highlightColor with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), objectsOnPath → Objects on Path Tool Options (dialog `objectsOnPathOptions`, fields pivot (0–8, row by row from the top left; 4, the center) and rotate (°, −360–360; 0); Reset puts 4 and 0 back; Save runs tool.setOption {tool: \"objectsOnPath\", values}: the objects attached next use them, groups attached before keep theirs)", ), ( "ui.colorGuideOptions", diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 5afad21b0d..0a11a16afa 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -56,6 +56,7 @@ pub mod freehand; mod gradient_stop; pub mod graph_data; pub mod graph_design; +pub mod graph_type; pub mod graphic_style_options; pub mod halftone; pub mod hyphenation; @@ -339,6 +340,7 @@ registry! { GraphDesign: [graph_design::DESIGN] => graph_design::DESIGN_SPEC, GraphColumn: [graph_design::COLUMN] => graph_design::COLUMN_SPEC, GraphMarker: [graph_design::MARKER] => graph_design::MARKER_SPEC, + GraphType: [graph_type::KIND] => graph_type::SPEC, GradientStop: ["gradientStop"] => gradient_stop::SPEC, ColorPicker: [color_picker::KIND] => color_picker::SPEC, GraphicStyleOptions: [graphic_style_options::KIND] => graphic_style_options::SPEC, @@ -514,6 +516,8 @@ fn hidden_controls(d: &Dialog) -> &'static [&'static str] { Some(DialogKind::FileInfo) => &["__keyword"], // The preset name being typed. Some(DialogKind::FlattenTransparency) => &["__presetName"], + // The page shown. + Some(DialogKind::GraphType) => &[graph_type::PAGE], // The Resolution menu's Other. Some(DialogKind::RasterOptions | DialogKind::RasterFormatOptions | DialogKind::PsdOptions) => &["__otherPpi"], // The category shown, the search text and the three settings kept outside the preferences. diff --git a/crates/ui-egui/src/dialogs/command.rs b/crates/ui-egui/src/dialogs/command.rs index 6656b45bf5..4e43ad904c 100644 --- a/crates/ui-egui/src/dialogs/command.rs +++ b/crates/ui-egui/src/dialogs/command.rs @@ -1,8 +1,6 @@ //! The generic parameter dialog (`ui.paramDialog`): edits a command's parameters (`__command`, //! headed `__label`) and runs it on OK, with the fixed ones in `__params` (not shown) added. -use std::sync::LazyLock; - use serde_json::{Value, json}; use super::{DialogSpec, form}; @@ -173,12 +171,6 @@ fn choices(command: &str, key: &str) -> Option { ("text.areaOptions", "firstBaseline") => Some(FIRST_BASELINE), ("text.areaOptions", "verticalAlign") => Some(VERTICAL_ALIGN), (crate::addons::cutting::WEED, "direction") => Some(crate::addons::cutting::WEED_DIRECTIONS), - ("graph.setType", "type") => Some(GRAPH_KINDS.as_slice()), - ("graph.setType", "valueAxis") => Some(GRAPH_VALUE_AXIS), - ("graph.setType", "tickLength" | "rightTickLength" | "categoryTickLength") => Some(GRAPH_TICK_LENGTH), - ("graph.setType", "pieLegend") => Some(PIE_LEGEND), - ("graph.setType", "piePosition") => Some(PIE_POSITION), - ("graph.setType", "pieSort") => Some(PIE_SORT), (crate::addons::image::REMOVE_BACKGROUND | crate::addons::image::COLOR_MASK, "background") => Some(crate::addons::image::BACKGROUNDS), (crate::addons::image::REMOVE_BACKGROUND | crate::addons::image::COLOR_MASK, "match") => Some(crate::addons::image::MATCHES), _ => None, @@ -211,21 +203,6 @@ const FIRST_BASELINE: form::Choices = /// Area Type Options › Align (vertical alignment of the lines in each row/column). const VERTICAL_ALIGN: form::Choices = &[("Top", "top"), ("Center", "center"), ("Bottom", "bottom"), ("Justify", "justify")]; -/// Graph Type › Type: the nine graph kinds, in the Graph tool group's order. -static GRAPH_KINDS: LazyLock> = - LazyLock::new(|| vectorcraft_doc::GraphKind::ALL.iter().map(|k| (k.label(), k.id())).collect()); - -/// Graph Type › Value Axis (series picked on both axes show none, and OK leaves each where it is). -const GRAPH_VALUE_AXIS: form::Choices = &[("On Left Side", "left"), ("On Right Side", "right"), ("On Both Sides", "both")]; - -/// Graph Type › Legend, Position and Sort for pie graphs. -const PIE_LEGEND: form::Choices = &[("No Legend", "none"), ("Standard Legend", "standard"), ("Legends in Wedges", "wedges")]; -const PIE_POSITION: form::Choices = &[("Ratio", "ratio"), ("Even", "even"), ("Stacked", "stacked")]; -const PIE_SORT: form::Choices = &[("All", "all"), ("First", "first"), ("None", "none")]; - -/// Graph Type › Tick Marks › Length. -const GRAPH_TICK_LENGTH: form::Choices = &[("None", "none"), ("Short", "short"), ("Full Width", "full")]; - /// Closes before running, so a dialog the command opens stays open. A previewed one keeps its /// preview as the one undo step. fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { @@ -402,42 +379,6 @@ mod tests { assert!(p[0] == p[1] && p[1] == p[2] && p[0] < 128, "red in grayscale: {p:?}"); } - /// Graph Type › Value Axis is a dropdown of the values `graph.setType` takes, and OK puts the - /// value axis on the side picked. - #[test] - fn graph_type_value_axis_is_a_dropdown_of_its_sides() { - let doc = vectorcraft_engine::find_command("graph.setType").unwrap().params; - let values: Vec<&str> = choices("graph.setType", "valueAxis").unwrap().iter().map(|(_, v)| *v).collect(); - assert!(doc.contains(&format!("valueAxis?: {}", values.join("|"))), "{values:?}"); - let mut app = VectorcraftApp::new(Session::new(), Default::default()); - app.run("file.new", json!({"width": 400, "height": 400})).unwrap(); - app.run("graph.create", json!({"type": "column", "x": 0, "y": 0, "width": 200, "height": 150})).unwrap(); - crate::menus::graph_dialog(&mut app, "graph.setType").unwrap(); - assert_eq!(app.ui.dialog.as_ref().unwrap().str("valueAxis"), "left"); - app.ui.dialog.as_mut().unwrap().fields.insert("valueAxis".into(), json!("both")); - crate::dialogs::confirm(&mut app).unwrap(); - assert_eq!(app.session.execute("graph.setType", &json!({})).unwrap()["valueAxis"], "both"); - } - - /// Graph Type › Type is a dropdown of the nine graph kinds (#2175), and OK changes the graph's - /// type and keeps its data. - #[test] - fn graph_type_is_a_dropdown_of_the_graph_kinds() { - let kinds: Vec<(&str, &str)> = vectorcraft_doc::GraphKind::ALL.iter().map(|k| (k.label(), k.id())).collect(); - assert_eq!(choices("graph.setType", "type").unwrap().to_vec(), kinds); - let mut app = VectorcraftApp::new(Session::new(), Default::default()); - app.run("file.new", json!({"width": 400, "height": 400})).unwrap(); - app.run("graph.create", json!({"type": "column", "x": 0, "y": 0, "width": 200, "height": 150, "csv": ",a,b\nQ1,1,2\nQ2,3,4"})).unwrap(); - let data = app.session.execute("graph.setData", &json!({})).unwrap(); - assert_eq!(data["rows"], json!([[1.0, 2.0], [3.0, 4.0]])); - crate::menus::graph_dialog(&mut app, "graph.setType").unwrap(); - assert_eq!(app.ui.dialog.as_ref().unwrap().str("type"), "column"); - app.ui.dialog.as_mut().unwrap().fields.insert("type".into(), json!("stackedBar")); - crate::dialogs::confirm(&mut app).unwrap(); - assert_eq!(app.session.execute("graph.setType", &json!({})).unwrap()["type"], "stackedBar"); - assert_eq!(app.session.execute("graph.setData", &json!({})).unwrap(), data); - } - /// A document with a rectangle made a 2 x 2 Grid Repeat and selected, its Repeat Options /// dialog open. fn grid_options_app() -> VectorcraftApp { diff --git a/crates/ui-egui/src/dialogs/graph_type.rs b/crates/ui-egui/src/dialogs/graph_type.rs new file mode 100644 index 0000000000..14bc371e74 --- /dev/null +++ b/crates/ui-egui/src/dialogs/graph_type.rs @@ -0,0 +1,1314 @@ +//! Object › Graph › Type…: Graph Type, for the selected graph or for the series picked in it with +//! Group Selection. +//! +//! A menu at the top shows one page at a time. Graph Options has the Type buttons, the Value Axis +//! side with Separate Scales, the Style checkboxes and the options of the type picked: Column or +//! Bar Width and Cluster Width, the data point and line checkboxes, or a pie graph's Legend, +//! Position and Sort. A page per axis follows for the axes the type has: Value Axis (Left Axis and +//! Right Axis while the value axis is on both sides) with Tick Values, Tick Marks and Add Labels, +//! and Category Axis with Tick Marks. OK runs `graph.setType`, and with nothing changed it closes +//! without a step of history. +//! +//! Fields: the options `graph.setType {}` returns under the command's names for them, with +//! `legend`, `pieLegend`, `piePosition`, `pieSort`, `firstRowInFront`, `firstColumnInFront` and +//! `filledLines` held for every type, and `type` or `valueAxis` null while the picked series +//! differ in it (OK then leaves each series as it is); `overrideValues` and `rightOverrideValues` (Override +//! Calculated Values of the left or only value axis and of the right one: while one is on, OK +//! sends `axisMin`, `axisMax` and `ticks`, or `rightAxisMin`, `rightAxisMax` and `rightTicks`); and +//! `__page`, the page shown (`options`, `valueAxis`, `rightAxis` or `categoryAxis`). + +use std::ops::RangeInclusive; + +use serde_json::{Map, Value, json}; +use vectorcraft_doc::GraphKind; + +use super::{DialogSpec, form, run_and_close}; +use crate::state::Dialog; +use crate::theme::Tokens; +use crate::{VectorcraftApp, widgets}; + +/// The dialog kind of Graph Type. +pub const KIND: &str = "graphType"; + +/// The field that holds the page shown. +pub(super) const PAGE: &str = "__page"; + +const COMMAND: &str = "graph.setType"; + +/// The graph the dialog opened on, the series picked with Group Selection, the graph's own type and +/// value axis side, each series' own type, the series on the right axis, the tick values each value axis is calculated with for the +/// options the dialog shows and the options they were calculated for, and the parameters OK would +/// have sent as the dialog opened. +const GRAPH_ID: &str = "__graphId"; +const SERIES: &str = "__series"; +const GRAPH: &str = "__graph"; +const GRAPH_AXIS: &str = "__graphAxis"; +const SERIES_KINDS: &str = "__seriesKinds"; +const RIGHT_SERIES: &str = "__rightSeries"; +const CALCULATED: &str = "__calculated"; +const CALCULATED_FOR: &str = "__calculatedFor"; +const OPENED: &str = "__opened"; + +const LABEL_W: f32 = 170.0; +const FIELD_W: f32 = 160.0; +const NUMBER_W: f32 = 72.0; +const TYPE_BUTTON: f32 = 34.0; + +/// The widths `graph.setType` takes for columns, bars and clusters, in percent. +const WIDTHS: RangeInclusive = 1.0..=1000.0; +/// The divisions it takes for a value axis, and the tick marks it takes per division. +const DIVISIONS: RangeInclusive = 1.0..=100.0; +const TICK_MARKS: RangeInclusive = 1.0..=20.0; +/// The characters it keeps of a prefix or suffix. +const AFFIX_CHARS: usize = 64; +/// The divisions it draws an overridden value axis in while the axis has no count of its own. +const DEFAULT_DIVISIONS: usize = 5; + +/// The types whose value axis goes on a side, those whose right axis takes a scale of its own, and +/// those with a category axis. +const SIDED: [GraphKind; 5] = [GraphKind::Column, GraphKind::StackedColumn, GraphKind::Line, GraphKind::Area, GraphKind::Scatter]; +const SCALED: [GraphKind; 4] = [GraphKind::Column, GraphKind::StackedColumn, GraphKind::Line, GraphKind::Area]; +const CATEGORIES: [GraphKind; 6] = + [GraphKind::Column, GraphKind::StackedColumn, GraphKind::Bar, GraphKind::StackedBar, GraphKind::Line, GraphKind::Area]; +/// The types drawn as columns or bars, and those drawn as data points and lines. +const COLUMNS: [GraphKind; 4] = [GraphKind::Column, GraphKind::StackedColumn, GraphKind::Bar, GraphKind::StackedBar]; +const LINES: [GraphKind; 3] = [GraphKind::Line, GraphKind::Scatter, GraphKind::Radar]; + +/// Value Axis for the whole graph and for picked series (a series goes on one axis), Tick Marks › +/// Length, and a pie graph's Legend, Position and Sort, each as (value, label). +const SIDES: [(&str, &str); 3] = [("left", "On Left Side"), ("right", "On Right Side"), ("both", "On Both Sides")]; +const SERIES_SIDES: [(&str, &str); 2] = [("left", "On Left Side"), ("right", "On Right Side")]; +const TICK_LENGTHS: [(&str, &str); 3] = [("none", "None"), ("short", "Short"), ("full", "Full Width")]; +const PIE_LEGENDS: [(&str, &str); 3] = [("none", "No Legend"), ("standard", "Standard Legend"), ("wedges", "Legends in Wedges")]; +const PIE_POSITIONS: [(&str, &str); 3] = [("ratio", "Ratio"), ("even", "Even"), ("stacked", "Stacked")]; +const PIE_SORTS: [(&str, &str); 3] = [("all", "All"), ("first", "First"), ("none", "None")]; + +/// The options OK always sends. `valueAxis` is left out while the picked series are on both axes, +/// and the legend and the tick values follow the type and Override Calculated Values. +const SENT: [&str; 24] = [ + "type", + "valueAxis", + "separateScales", + "dropShadow", + "legendAcrossTop", + "firstRowInFront", + "firstColumnInFront", + "columnWidth", + "clusterWidth", + "markPoints", + "connectPoints", + "edgeToEdge", + "filledLines", + "tickLength", + "tickMarks", + "rightTickLength", + "rightTickMarks", + "categoryTickLength", + "categoryTickMarks", + "ticksBetweenLabels", + "prefix", + "suffix", + "rightPrefix", + "rightSuffix", +]; + +/// The fields of one value axis: its place in the calculated tick values, Override Calculated +/// Values as the dialog shows it and as it opened, the tick values, the tick marks and the labels. +struct Axis { + index: usize, + overridden: &'static str, + opened_overridden: &'static str, + min: &'static str, + max: &'static str, + ticks: &'static str, + length: &'static str, + marks: &'static str, + prefix: &'static str, + suffix: &'static str, +} + +const LEFT: Axis = Axis { + index: 0, + overridden: "overrideValues", + opened_overridden: "__override", + min: "axisMin", + max: "axisMax", + ticks: "ticks", + length: "tickLength", + marks: "tickMarks", + prefix: "prefix", + suffix: "suffix", +}; + +const RIGHT: Axis = Axis { + index: 1, + overridden: "rightOverrideValues", + opened_overridden: "__rightOverride", + min: "rightAxisMin", + max: "rightAxisMax", + ticks: "rightTicks", + length: "rightTickLength", + marks: "rightTickMarks", + prefix: "rightPrefix", + suffix: "rightSuffix", +}; + +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("Graph Type").into(), + body, + confirm, + ok_enabled: Some(|_, d| problem(d).is_none()), + min_width: 440.0, + ..DialogSpec::FORM +}; + +/// Open Graph Type on the options of the selected graph, or of the series picked in it with Group +/// Selection. An error without a selected graph. +pub fn open(app: &mut VectorcraftApp) -> Result { + let queried = app.session.execute(COMMAND, &json!({})).map_err(|e| e.to_string())?; + let subject = vectorcraft_engine::graph_type_subject(&app.session).map_err(|e| e.to_string())?; + let st = app.session.active().ok_or("no document open")?; + let spec = st.doc.node(subject.id).and_then(|n| n.extras().graph.as_deref()).ok_or("select a graph")?; + let mut fields = queried.as_object().cloned().unwrap_or_default(); + // The options the query leaves out for the graph's types: the dialog shows each one once the + // type picked uses it. + let pie_legend = match (spec.legend, spec.pie_legend_in_wedges) { + (false, _) => "none", + (true, true) => "wedges", + (true, false) => "standard", + }; + for (key, value) in [ + ("firstRowInFront", json!(spec.first_row_in_front)), + ("firstColumnInFront", json!(spec.first_column_in_front)), + ("filledLines", json!(spec.filled_lines)), + ("legend", json!(spec.legend)), + ("pieLegend", json!(pie_legend)), + ("piePosition", json!(spec.pie_position.id())), + ("pieSort", json!(spec.pie_sort.id())), + ] { + fields.entry(key).or_insert(value); + } + // Picked series of different types show no type, and OK leaves each one's type as it is. + let mut picked_kinds = subject.series.iter().map(|i| spec.series_kind(*i)); + if picked_kinds.next().is_some_and(|first| picked_kinds.any(|kind| kind != first)) { + fields.insert("type".into(), Value::Null); + } + let count = spec.rows.iter().map(Vec::len).max().unwrap_or(0).min(vectorcraft_doc::MAX_GRAPH_SERIES); + let series_kinds: Vec> = (0..count).map(|i| spec.series_kinds.get(i).copied().flatten().map(GraphKind::id)).collect(); + let calculated: Vec = subject.calculated.iter().map(|(lo, hi, divisions)| json!([lo, hi, divisions])).collect(); + for (axis, min, max) in [(&LEFT, spec.axis_min, spec.axis_max), (&RIGHT, spec.right_axis_min, spec.right_axis_max)] { + let overridden = json!(min.is_some() && max.is_some()); + fields.insert(axis.overridden.into(), overridden.clone()); + fields.insert(axis.opened_overridden.into(), overridden); + } + fields.insert(GRAPH_ID.into(), json!(subject.id.0)); + fields.insert(SERIES.into(), json!(subject.series)); + fields.insert(GRAPH.into(), json!(spec.kind.id())); + fields.insert(GRAPH_AXIS.into(), json!(spec.value_axis.id())); + fields.insert(SERIES_KINDS.into(), json!(series_kinds)); + fields.insert(RIGHT_SERIES.into(), json!(spec.right_series.iter().filter(|i| **i < count).collect::>())); + fields.insert(CALCULATED.into(), json!(calculated)); + fields.insert(PAGE.into(), json!("options")); + let mut d = Dialog { kind: KIND.into(), fields }; + let key = pending(&d); + d.fields.insert(CALCULATED_FOR.into(), key); + settle(&mut d); + let opened = params(&d); + d.fields.insert(OPENED.into(), opened); + app.ui.dialog = Some(d); + Ok(json!({ "dialog": KIND })) +} + +/// The series the type and the value axis go to: those picked with Group Selection. None: the +/// whole graph. +fn picked(d: &Dialog) -> Vec { + let indexes = d.fields.get(SERIES).and_then(Value::as_array); + indexes.map(|a| a.iter().filter_map(|v| v.as_u64().and_then(|i| usize::try_from(i).ok())).collect()).unwrap_or_default() +} + +/// The type the Type buttons show. +fn chosen(d: &Dialog) -> Option { + d.fields.get("type").and_then(Value::as_str).and_then(GraphKind::parse) +} + +/// The graph's own type as OK leaves it: the type picked, or with series picked the type the graph +/// has. +fn graph_kind(d: &Dialog) -> GraphKind { + let own = d.fields.get(GRAPH).and_then(Value::as_str).and_then(GraphKind::parse).unwrap_or_default(); + if picked(d).is_empty() { chosen(d).unwrap_or(own) } else { own } +} + +/// The type each series draws as once OK is pressed: the type picked for the picked series (their +/// own while they differ and none is picked), its own type where that combines with the graph's, +/// else the graph's. +fn series_kinds(d: &Dialog) -> Vec { + let (graph, picked, chosen) = (graph_kind(d), picked(d), chosen(d)); + let own: Vec> = d + .fields + .get(SERIES_KINDS) + .and_then(Value::as_array) + .map(|a| a.iter().map(|v| v.as_str().and_then(GraphKind::parse)).collect()) + .unwrap_or_default(); + (0..own.len().max(1)) + .map(|i| { + let kept = own.get(i).copied().flatten(); + let kind = if picked.contains(&i) { chosen.or(kept) } else { kept }; + kind.filter(|k| k.combines_with(graph)).unwrap_or(graph) + }) + .collect() +} + +/// Whether a series draws as one of `kinds` once OK is pressed. +fn draws(d: &Dialog, kinds: &[GraphKind]) -> bool { + series_kinds(d).iter().any(|k| kinds.contains(k)) +} + +/// The side or sides the graph's value axis is on once OK is pressed. With series picked, Value +/// Axis is theirs and the graph keeps its own. +fn graph_axis(d: &Dialog) -> String { + d.str(if picked(d).is_empty() { "valueAxis" } else { GRAPH_AXIS }) +} + +/// Whether the graph draws a value axis on both sides. +fn both_sides(d: &Dialog) -> bool { + SIDED.contains(&graph_kind(d)) && graph_axis(d) == "both" +} + +/// Whether the right value axis has a scale of its own: Separate Scales with the value axis on +/// both sides of a type that takes one. +fn separate(d: &Dialog) -> bool { + d.bool("separateScales") && both_sides(d) && SCALED.contains(&graph_kind(d)) +} + +/// Whether the graph draws a legend beside or above the plot. +fn legend_shown(d: &Dialog) -> bool { + if graph_kind(d) == GraphKind::Pie { d.str("pieLegend") == "standard" } else { d.bool("legend") } +} + +/// The pages the menu lists for the type picked, as (id, name): Graph Options, a page per value +/// axis side (none for a pie graph) and Category Axis for the types that have one. +fn pages(d: &Dialog) -> Vec<(&'static str, &'static str)> { + let kind = graph_kind(d); + let mut pages = vec![("options", tl!("Graph Options"))]; + if kind != GraphKind::Pie { + if both_sides(d) { + pages.extend([("valueAxis", tl!("Left Axis")), ("rightAxis", tl!("Right Axis"))]); + } else { + pages.push(("valueAxis", tl!("Value Axis"))); + } + } + if CATEGORIES.contains(&kind) { + pages.push(("categoryAxis", tl!("Category Axis"))); + } + pages +} + +/// The least value, the greatest and the divisions `axis` is drawn with while its tick values are +/// calculated. +fn calculated(d: &Dialog, axis: &Axis) -> (f64, f64, usize) { + let of = |i: usize| d.fields.get(CALCULATED)?.get(axis.index)?.get(i)?.as_f64(); + let divisions = of(2).filter(|n| *n >= 1.0).map_or(DEFAULT_DIVISIONS, |n| n.min(*DIVISIONS.end()) as usize); + (of(0).unwrap_or(0.0), of(1).unwrap_or(1.0), divisions) +} + +/// The series on the right value axis once OK is pressed: the graph's, with the picked series moved +/// to the side picked for them. +fn right_series(d: &Dialog) -> Vec { + let mut right: Vec = d + .fields + .get(RIGHT_SERIES) + .and_then(Value::as_array) + .map(|a| a.iter().filter_map(|v| v.as_u64().and_then(|i| usize::try_from(i).ok())).collect()) + .unwrap_or_default(); + let picked = picked(d); + match d.fields.get("valueAxis").and_then(Value::as_str) { + Some("right") => right.extend(picked.into_iter().filter(|i| !right.contains(i)).collect::>()), + Some("left") => right.retain(|i| !picked.contains(i)), + _ => {} + } + right +} + +/// Whether the right axis' own scale (`right`) or the left one draws a value axis: the left scale +/// draws both sides without Separate Scales, and with it each scale needs a series on its side (the +/// other side's scale draws both sides when one side has none). +fn scale_used(d: &Dialog, right: bool) -> bool { + if !separate(d) { + return !right; + } + let on_right = right_series(d); + let count = series_kinds(d).len(); + (0..count).any(|i| on_right.contains(&i) == right) +} + +/// The options the calculated tick values depend on, as the dialog shows them: the graph's type, +/// each series' type, the value axis side, Separate Scales, and the side picked series go on. +fn pending(d: &Dialog) -> Value { + let kinds: Vec<&str> = series_kinds(d).into_iter().map(GraphKind::id).collect(); + let side = d.fields.get("valueAxis").cloned().unwrap_or(Value::Null); + json!([graph_kind(d).id(), kinds, graph_axis(d), d.bool("separateScales"), side]) +} + +/// Calculate the tick values again when the options they depend on changed since they were last +/// calculated: the graph with the type, the value axis side and Separate Scales the dialog shows, and +/// the picked series on the type and side picked for them. +fn recalculate(app: &VectorcraftApp, d: &mut Dialog) { + let key = pending(d); + if d.fields.get(CALCULATED_FOR) == Some(&key) { + return; + } + let id = d.fields.get(GRAPH_ID).and_then(Value::as_u64).map(vectorcraft_doc::NodeId); + let Some(spec) = id.and_then(|id| app.session.active()?.doc.node(id)?.extras().graph.as_deref().cloned()) else { + return; + }; + let mut spec = spec; + let graph = graph_kind(d); + spec.series_kinds = series_kinds(d).into_iter().map(|kind| (kind != graph).then_some(kind)).collect(); + spec.kind = graph; + if let Some(side) = vectorcraft_doc::ValueAxisSide::parse(&graph_axis(d)) { + spec.value_axis = side; + } + spec.separate_scales = d.bool("separateScales"); + spec.right_series = right_series(d); + let calculated: Vec = + vectorcraft_engine::graph_calculated_axes(&spec).iter().map(|(lo, hi, divisions)| json!([lo, hi, divisions])).collect(); + let before = d.fields.insert(CALCULATED.into(), json!(calculated)); + d.fields.insert(CALCULATED_FOR.into(), key); + // An overridden axis that still holds the values calculated before takes the new ones. + for axis in [&LEFT, &RIGHT] { + let was = |i: usize| before.as_ref()?.get(axis.index)?.get(i)?.as_f64(); + let keys = [axis.min, axis.max, axis.ticks]; + if d.bool(axis.overridden) && keys.iter().enumerate().all(|(i, key)| number(d, key).is_some_and(|v| Some(v) == was(i))) { + for key in keys { + d.fields.remove(key); + } + } + } +} + +/// Field `key` as a number. `None` when it is missing, null or not a number. +fn number(d: &Dialog, key: &str) -> Option { + d.fields.get(key).and_then(Value::as_f64) +} + +/// The least and greatest value `axis` shows: its own, else the calculated ones. +fn bounds(d: &Dialog, axis: &Axis) -> (f64, f64) { + let (lo, hi, _) = calculated(d, axis); + (number(d, axis.min).unwrap_or(lo), number(d, axis.max).unwrap_or(hi)) +} + +/// The divisions `axis` shows: its own count, else the count it is drawn in. +fn divisions(d: &Dialog, axis: &Axis) -> usize { + let own = number(d, axis.ticks).filter(|n| *n >= 1.0).map(|n| n.min(*DIVISIONS.end()) as usize); + let both = number(d, axis.min).is_some() && number(d, axis.max).is_some(); + own.unwrap_or(if both { DEFAULT_DIVISIONS } else { calculated(d, axis).2 }) +} + +/// Give each axis whose Override Calculated Values is on the tick values it lacks: the calculated +/// ones, which the fields then show and OK sends. +fn settle(d: &mut Dialog) { + for axis in [&LEFT, &RIGHT] { + if !d.bool(axis.overridden) { + continue; + } + let ((lo, hi), count) = (bounds(d, axis), divisions(d, axis)); + for (key, value) in [(axis.min, json!(lo)), (axis.max, json!(hi))] { + if number(d, key).is_none() { + d.fields.insert(key.into(), value); + } + } + if number(d, axis.ticks).is_none_or(|n| n < 1.0) { + d.fields.insert(axis.ticks.into(), json!(count)); + } + } +} + +/// Why OK can't apply the dialog: an overridden value axis whose maximum is not more than its +/// minimum, which `graph.setType` would draw with calculated values instead. +fn problem(d: &Dialog) -> Option<&'static str> { + let out_of_order = |axis: &Axis| { + let (lo, hi) = bounds(d, axis); + d.bool(axis.overridden) && !(hi > lo && (hi - lo).is_finite()) + }; + let refused = graph_kind(d) != GraphKind::Pie && [&LEFT, &RIGHT].into_iter().any(|axis| scale_used(d, axis.index == 1) && out_of_order(axis)); + refused.then(|| tl!("Maximum must be more than Minimum.")) +} + +/// `graph.setType`'s parameters for the dialog as it is. The type and the value axis go to the +/// series picked with Group Selection, as the command reads the selection. +fn params(d: &Dialog) -> Value { + let mut d = d.clone(); + settle(&mut d); + let mut p = Map::new(); + let send = |p: &mut Map, key: &str| { + if let Some(v) = d.fields.get(key).filter(|v| !v.is_null()) { + p.insert(key.into(), v.clone()); + } + }; + for key in SENT { + send(&mut p, key); + } + // A pie graph's Legend menu stands for the legend checkbox. + let legend: &[&str] = if graph_kind(&d) == GraphKind::Pie { &["pieLegend", "piePosition", "pieSort"] } else { &["legend"] }; + for key in legend { + send(&mut p, key); + } + for axis in [&LEFT, &RIGHT] { + if d.bool(axis.overridden) { + for key in [axis.min, axis.max, axis.ticks] { + send(&mut p, key); + } + } else if d.bool(axis.opened_overridden) { + // Turned off: the tick values are calculated again. + p.extend([(axis.min.into(), Value::Null), (axis.max.into(), Value::Null), (axis.ticks.into(), json!(0))]); + } + } + Value::Object(p) +} + +/// Whether two sets of parameters hold the same values, a count being the same as the number a +/// field gives for it. +fn same(a: &Value, b: &Value) -> bool { + match (a, b) { + (Value::Number(x), Value::Number(y)) => x.as_f64() == y.as_f64(), + (Value::Object(x), Value::Object(y)) => x.len() == y.len() && x.iter().all(|(k, v)| y.get(k).is_some_and(|w| same(v, w))), + _ => a == b, + } +} + +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let mut d = d.clone(); + recalculate(app, &mut d); + if let Some(why) = problem(&d) { + return Err(why.into()); + } + let mut p = params(&d); + // Nothing changed: no step of history. + if d.fields.get(OPENED).is_some_and(|opened| same(opened, &p)) { + app.ui.dialog = None; + return Ok(Value::Null); + } + aim(app, &d, &mut p); + run_and_close(app, COMMAND, p) +} + +/// Point `p` at the graph and the series the dialog opened on when the selection no longer holds +/// them: `id`, and `seriesIndexes` while series are picked and a type or a value axis goes to them. +/// While the selection still holds them, the command reads it and keeps picked series selected. +fn aim(app: &VectorcraftApp, d: &Dialog, p: &mut Value) { + let Some(id) = d.fields.get(GRAPH_ID).and_then(Value::as_u64) else { + return; + }; + let now = vectorcraft_engine::graph_type_subject(&app.session).ok(); + if now.is_some_and(|now| now.id.0 == id && now.series == picked(d)) { + return; + } + let Some(p) = p.as_object_mut() else { + return; + }; + p.insert("id".into(), json!(id)); + let picked = picked(d); + // Picked series of different types on both axes send neither, and the other options go to the + // whole graph. + if !picked.is_empty() && (p.contains_key("type") || p.contains_key("valueAxis")) { + p.insert("seriesIndexes".into(), json!(picked)); + } +} + +fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + recalculate(app, d); + settle(d); + let pages = pages(d); + let (page, shown) = pages.iter().find(|(id, _)| *id == d.str(PAGE)).or(pages.first()).copied().unwrap_or(("options", "")); + let names: Vec<&str> = pages.iter().map(|(_, name)| *name).collect(); + let pick = widgets::dropdown_names_named(ui, "graph-type-page", Some(tl!("Graph Type")), shown, &names, FIELD_W); + if let Some((id, _)) = pick.and_then(|i| pages.get(i)) { + d.fields.insert(PAGE.into(), json!(id)); + } + ui.add_space(8.0); + match page { + "valueAxis" => value_page(ui, d, false), + "rightAxis" => value_page(ui, d, true), + "categoryAxis" => category_page(ui, d), + _ => options_page(ui, d), + } + if let Some(why) = problem(d) { + ui.add_space(6.0); + ui.colored_label(Tokens::get(ui.ctx()).error, why); + } + false +} + +/// `text` with the colon of a field label. +fn colon(text: &str) -> String { + format!("{text}:") +} + +/// A checkbox bound to `d.fields[key]`, dimmed while the type picked has no use for it. +fn check(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, enabled: bool) { + let on = d.bool(key); + if widgets::check(ui, label, on, enabled) { + d.fields.insert(key.into(), json!(!on)); + } +} + +/// A dropdown row bound to the string `d.fields[key]`; `options` are (value, label). A value that +/// is not among them, such as the null of series on both value axes, shows a blank field. +fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, options: &[(&str, &str)]) { + widgets::label_row(ui, label, LABEL_W, |ui| { + let current = d.fields.get(key).and_then(Value::as_str).unwrap_or_default(); + let shown = options.iter().find(|(value, _)| *value == current).map_or("", |(_, label)| *label); + let labels: Vec<&str> = options.iter().map(|(_, label)| *label).collect(); + if let Some((value, _)) = widgets::dropdown(ui, ("graph-type", key), shown, &labels, FIELD_W).and_then(|i| options.get(i)) { + d.fields.insert(key.into(), json!(value)); + } + }); +} + +/// A number row bound to `d.fields[key]`, kept within `range` and rounded to `decimals` places. +fn ranged(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, shown: f64, (range, suffix, decimals): (RangeInclusive, &str, usize)) { + widgets::label_row(ui, label, LABEL_W, |ui| { + if let Some(v) = widgets::range_field(ui, ("graph-type", key), shown, range, suffix, decimals, NUMBER_W) { + d.fields.insert(key.into(), json!(v)); + } + }); +} + +/// A text row bound to `d.fields[key]` that takes the characters `graph.setType` keeps. +fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) { + widgets::label_row(ui, label, LABEL_W, |ui| { + form::text_edit_limited(ui, d, key, FIELD_W, AFFIX_CHARS); + }); +} + +fn options_page(ui: &mut egui::Ui, d: &mut Dialog) { + widgets::subheader(ui, tl!("Type")); + type_row(ui, d); + ui.add_space(4.0); + let kind = graph_kind(d); + ui.add_enabled_ui(SIDED.contains(&kind), |ui| { + let sides: &[(&str, &str)] = if picked(d).is_empty() { &SIDES } else { &SERIES_SIDES }; + choice(ui, d, "valueAxis", &colon(tl!("Value Axis")), sides); + }); + let scaled = both_sides(d) && SCALED.contains(&kind); + check(ui, d, "separateScales", tl!("Separate Scales"), scaled); + ui.add_space(8.0); + widgets::subheader(ui, tl!("Style")); + let columns = draws(d, &COLUMNS); + egui::Grid::new("graph-type-style").num_columns(2).spacing([24.0, 6.0]).show(ui, |ui| { + check(ui, d, "dropShadow", tl!("Add Drop Shadow"), true); + check(ui, d, "firstRowInFront", tl!("First Row in Front"), columns); + ui.end_row(); + check(ui, d, "legendAcrossTop", tl!("Add Legend Across Top"), legend_shown(d)); + check(ui, d, "firstColumnInFront", tl!("First Column in Front"), columns); + ui.end_row(); + // A pie graph has its Legend menu among its options. + if kind != GraphKind::Pie { + check(ui, d, "legend", tl!("Legend"), true); + ui.end_row(); + } + }); + type_options(ui, d, kind); +} + +/// The Options of the type picked: widths for columns and bars, data points and lines for line, +/// scatter and radar series, and Legend, Position and Sort for a pie graph. An area graph has none. +fn type_options(ui: &mut egui::Ui, d: &mut Dialog, kind: GraphKind) { + let (columns, lines, pie) = (draws(d, &COLUMNS), draws(d, &LINES), kind == GraphKind::Pie); + if !(columns || lines || pie) { + return; + } + ui.add_space(8.0); + widgets::subheader(ui, tl!("Options")); + if columns { + let width = if matches!(kind, GraphKind::Bar | GraphKind::StackedBar) { tl!("Bar Width") } else { tl!("Column Width") }; + for (key, label) in [("columnWidth", width), ("clusterWidth", tl!("Cluster Width"))] { + let shown = number(d, key).unwrap_or(100.0); + ranged(ui, d, key, &colon(label), shown, (WIDTHS, "%", 2)); + } + } + if lines { + // Edge-to-Edge Lines is for line series, and Draw Filled Lines for connected data points. + let (line, connected) = (draws(d, &[GraphKind::Line]), d.bool("connectPoints")); + egui::Grid::new("graph-type-lines").num_columns(2).spacing([24.0, 6.0]).show(ui, |ui| { + check(ui, d, "markPoints", tl!("Mark Data Points"), true); + check(ui, d, "edgeToEdge", tl!("Edge-to-Edge Lines"), line); + ui.end_row(); + check(ui, d, "connectPoints", tl!("Connect Data Points"), true); + check(ui, d, "filledLines", tl!("Draw Filled Lines"), connected); + ui.end_row(); + }); + } + if pie { + choice(ui, d, "pieLegend", &colon(tl!("Legend")), &PIE_LEGENDS); + choice(ui, d, "piePosition", &colon(tl!("Position")), &PIE_POSITIONS); + choice(ui, d, "pieSort", &colon(tl!("Sort")), &PIE_SORTS); + } +} + +/// The page of a value axis: the tick values of the scale it shows, and the tick marks and labels +/// of its side. An only value axis on the right side shows the left scale with the right side's +/// tick marks and labels, as the graph draws it. +fn value_page(ui: &mut egui::Ui, d: &mut Dialog, right: bool) { + let scale = if right { &RIGHT } else { &LEFT }; + let side = if right || (SIDED.contains(&graph_kind(d)) && graph_axis(d) == "right") { &RIGHT } else { &LEFT }; + widgets::subheader(ui, tl!("Tick Values")); + // Without Separate Scales the right axis shows the left axis' tick values, and with it a side + // without series shows the other side's. + let own_scale = scale_used(d, right); + check(ui, d, scale.overridden, tl!("Override Calculated Values"), own_scale); + ui.add_enabled_ui(own_scale && d.bool(scale.overridden), |ui| { + let ((lo, hi), count) = (bounds(d, scale), divisions(d, scale)); + for (key, label, shown) in [(scale.min, tl!("Minimum:"), lo), (scale.max, tl!("Maximum:"), hi)] { + widgets::label_row(ui, label, LABEL_W, |ui| { + if let Some(v) = widgets::plain_field(ui, ("graph-type", key), shown, "", 4, NUMBER_W) { + d.fields.insert(key.into(), json!(v)); + } + }); + } + ranged(ui, d, scale.ticks, &colon(tl!("Divisions")), count as f64, (DIVISIONS, "", 0)); + }); + ui.add_space(8.0); + tick_marks(ui, d, side.length, side.marks); + ui.add_space(8.0); + widgets::subheader(ui, tl!("Add Labels")); + text(ui, d, side.prefix, tl!("Prefix:")); + text(ui, d, side.suffix, &colon(tl!("Suffix"))); +} + +fn category_page(ui: &mut egui::Ui, d: &mut Dialog) { + tick_marks(ui, d, "categoryTickLength", "categoryTickMarks"); + let drawn = d.str("categoryTickLength") != "none"; + check(ui, d, "ticksBetweenLabels", tl!("Draw Tick Marks Between Labels"), drawn); +} + +/// Tick Marks: the length, and the tick marks per division while there are tick marks to draw. +fn tick_marks(ui: &mut egui::Ui, d: &mut Dialog, length: &str, marks: &str) { + widgets::subheader(ui, tl!("Tick Marks")); + choice(ui, d, length, &colon(tl!("Length")), &TICK_LENGTHS); + let shown = number(d, marks).unwrap_or(1.0); + ui.add_enabled_ui(d.str(length) != "none", |ui| { + ranged(ui, d, marks, &colon(tl!("Tick Marks per Division")), shown, (TICK_MARKS, "", 0)); + }); +} + +/// The nine Type buttons. With series picked, the types that cannot go in the graph are dimmed. +fn type_row(ui: &mut egui::Ui, d: &mut Dialog) { + let own = (!picked(d).is_empty()).then(|| graph_kind(d)); + let current = chosen(d); + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 4.0; + for kind in GraphKind::ALL { + let enabled = own.is_none_or(|graph| kind.combines_with(graph)); + if type_button(ui, kind, current == Some(kind), enabled) { + d.fields.insert("type".into(), json!(kind.id())); + } + } + }); +} + +/// The name of a graph type, in the UI language. +fn kind_name(kind: GraphKind) -> &'static str { + match kind { + GraphKind::Column => tl!("Column Graph"), + GraphKind::StackedColumn => tl!("Stacked Column Graph"), + GraphKind::Bar => tl!("Bar Graph"), + GraphKind::StackedBar => tl!("Stacked Bar Graph"), + GraphKind::Line => tl!("Line Graph"), + GraphKind::Area => tl!("Area Graph"), + GraphKind::Scatter => tl!("Scatter Graph"), + GraphKind::Pie => tl!("Pie Graph"), + GraphKind::Radar => tl!("Radar Graph"), + } +} + +/// A Type button: the type's picture, pressed while it is the type picked. Whether it was clicked. +fn type_button(ui: &mut egui::Ui, kind: GraphKind, selected: bool, enabled: bool) -> bool { + let t = Tokens::get(ui.ctx()); + let sense = if enabled { egui::Sense::click() } else { egui::Sense::hover() }; + let (rect, resp) = ui.allocate_exact_size(egui::Vec2::splat(TYPE_BUTTON), sense); + let fill = if selected { + t.tool_active + } else if enabled && resp.hovered() { + t.hover + } else { + egui::Color32::TRANSPARENT + }; + ui.painter().rect_filled(rect, egui::CornerRadius::same(5), fill); + let ink = if !enabled { + t.text_disabled + } else if selected { + t.text_strong + } else { + t.icon + }; + paint_kind(ui.painter(), rect.shrink(7.0), kind, ink); + let name = kind_name(kind); + resp.widget_info(|| widgets::icon_button_info(enabled, selected, name)); + resp.on_hover_text(name).clicked() +} + +/// Draw the picture of graph type `kind` in `r` with `ink`. +fn paint_kind(p: &egui::Painter, r: egui::Rect, kind: GraphKind, ink: egui::Color32) { + let at = |x: f32, y: f32| egui::pos2(r.left() + x * r.width(), r.top() + y * r.height()); + let faint = ink.gamma_multiply(0.45); + let block = |from: (f32, f32), to: (f32, f32), color: egui::Color32| { + p.rect_filled(egui::Rect::from_two_pos(at(from.0, from.1), at(to.0, to.1)), 0.0, color); + }; + let stroke = egui::Stroke::new(1.5, ink); + // Three columns or bars: where each one starts across the picture, and how long it is. + let marks = [(0.04, 0.55), (0.37, 0.95), (0.7, 0.75)]; + match kind { + GraphKind::Column | GraphKind::StackedColumn => { + for (x, len) in marks { + block((x, 1.0 - len), (x + 0.26, 1.0), ink); + if kind == GraphKind::StackedColumn { + block((x, 1.0 - len), (x + 0.26, 1.0 - len * 0.55), faint); + } + } + } + GraphKind::Bar | GraphKind::StackedBar => { + for (y, len) in marks { + block((0.0, y), (len, y + 0.26), ink); + if kind == GraphKind::StackedBar { + block((len * 0.55, y), (len, y + 0.26), faint); + } + } + } + GraphKind::Line | GraphKind::Area | GraphKind::Scatter => paint_points(p, &at, kind, ink), + GraphKind::Pie => { + let (centre, radius) = (r.center(), r.width().min(r.height()) / 2.0); + p.circle_stroke(centre, radius - 0.75, stroke); + let arc = (0..=8).map(|i| { + let a = -std::f32::consts::FRAC_PI_2 + std::f32::consts::FRAC_PI_2 * 1.3 * i as f32 / 8.0; + centre + egui::vec2(a.cos(), a.sin()) * radius + }); + let wedge: Vec = std::iter::once(centre).chain(arc).collect(); + p.add(egui::Shape::convex_polygon(wedge, ink, egui::Stroke::NONE)); + } + GraphKind::Radar => { + let (centre, radius) = (r.center(), r.width().min(r.height()) / 2.0); + let ring = |reach: [f32; 5]| -> Vec { + let corner = |(i, len): (usize, &f32)| { + let a = -std::f32::consts::FRAC_PI_2 + std::f32::consts::TAU * i as f32 / 5.0; + centre + egui::vec2(a.cos(), a.sin()) * radius * *len + }; + reach.iter().enumerate().map(corner).collect() + }; + p.add(egui::Shape::closed_line(ring([1.0; 5]), egui::Stroke::new(1.0, faint))); + p.add(egui::Shape::closed_line(ring([0.85, 0.5, 0.75, 0.45, 0.7]), stroke)); + } + } +} + +/// The pictures of the line, area and scatter types: four data points joined by a line, filled +/// down to the baseline, or on their own with a second row of points. +fn paint_points(p: &egui::Painter, at: &dyn Fn(f32, f32) -> egui::Pos2, kind: GraphKind, ink: egui::Color32) { + let points = [(0.05, 0.7), (0.35, 0.3), (0.65, 0.5), (0.95, 0.1)]; + match kind { + GraphKind::Area => { + // A band under each segment: the area under the line is not convex as a whole. + for pair in points.windows(2) { + if let [(x0, y0), (x1, y1)] = *pair { + let band = vec![at(x0, 1.0), at(x0, y0), at(x1, y1), at(x1, 1.0)]; + p.add(egui::Shape::convex_polygon(band, ink, egui::Stroke::NONE)); + } + } + } + GraphKind::Scatter => { + for (x, y) in points.into_iter().chain([(0.2, 0.25), (0.5, 0.85), (0.8, 0.6)]) { + p.circle_filled(at(x, y), 1.8, ink); + } + } + _ => { + p.add(egui::Shape::line(points.iter().map(|(x, y)| at(*x, *y)).collect(), egui::Stroke::new(1.5, ink))); + for (x, y) in points { + p.circle_filled(at(x, y), 2.0, ink); + } + } + } +} + +#[cfg(test)] +mod tests { + use egui::accesskit::Role; + use egui_kittest::kittest::{NodeT, Queryable}; + use vectorcraft_engine::Session; + + use super::*; + + type Harness = egui_kittest::Harness<'static, VectorcraftApp>; + + /// A 400 by 400 document with a selected graph of type `kind`: series a and b over Q1 (1, 2) + /// and Q2 (3, 4). + fn app_with_graph(kind: &str) -> VectorcraftApp { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 400})).unwrap(); + app.run("graph.create", json!({"type": kind, "x": 0, "y": 0, "width": 200, "height": 150, "csv": ",a,b\nQ1,1,2\nQ2,3,4"})).unwrap(); + app + } + + /// The open dialog drawn in a kittest harness. + fn harness(app: VectorcraftApp) -> Harness { + let mut fonts = false; + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1100.0, 900.0)).build_ui_state( + move |ui, app: &mut VectorcraftApp| { + if !std::mem::replace(&mut fonts, true) { + crate::theme::install_fonts(ui.ctx()); + return; + } + crate::dialogs::show(app, ui.ctx()); + }, + app, + ); + h.run_steps(5); + h + } + + fn query(app: &mut VectorcraftApp) -> Value { + app.session.execute(COMMAND, &json!({})).unwrap() + } + + fn set(app: &mut VectorcraftApp, key: &str, value: Value) { + app.ui.dialog.as_mut().unwrap().fields.insert(key.into(), value); + } + + fn steps(app: &VectorcraftApp) -> usize { + app.session.active().unwrap().history.undo.len() + } + + /// Clicks the Type button named `name`. + fn pick_type(h: &mut Harness, name: &str) { + h.get_by_role_and_label(Role::Button, name).click(); + h.run_steps(3); + } + + /// Object › Graph › Type… opens the dialog on the selected graph's options, on its Graph + /// Options page, and passes kittest's check that every input has a name on each page. + #[test] + fn the_graph_menu_opens_graph_type_on_the_selected_graph() { + let mut app = app_with_graph("column"); + crate::menus::invoke(&mut app, COMMAND, json!({})); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((d.kind.as_str(), d.str("type"), d.str("valueAxis"), d.str(PAGE)), (KIND, "column".into(), "left".into(), "options".into())); + assert!(picked(d).is_empty() && !d.bool("overrideValues") && !d.bool("rightOverrideValues")); + for key in ["legend", "pieLegend", "piePosition", "pieSort", "firstRowInFront", "firstColumnInFront", "filledLines"] { + assert!(d.fields.contains_key(key), "{key} is held for every type"); + } + set(&mut app, "valueAxis", json!("both")); + let mut h = harness(app); + let _ = h.get_by_role_and_label(Role::ComboBox, "Graph Type"); + let _ = h.get_by_role_and_label(Role::Button, "Pie Graph"); + // The right axis shows the left axis' tick values until Separate Scales gives it its own. + for (page, own_scale) in [("valueAxis", true), ("rightAxis", false)] { + set(h.state_mut(), PAGE, json!(page)); + h.run_steps(3); + h.check_accessibility(); + let overridden = h.get_by_role_and_label(Role::CheckBox, "Override Calculated Values"); + assert_eq!(overridden.accesskit_node().is_disabled(), !own_scale, "{page}"); + let _ = h.get_by_role_and_label(Role::TextInput, "Prefix:"); + } + set(h.state_mut(), PAGE, json!("categoryAxis")); + h.run_steps(3); + h.check_accessibility(); + assert!(h.get_by_role_and_label(Role::CheckBox, "Draw Tick Marks Between Labels").accesskit_node().is_disabled(), "no tick marks to place"); + // Without a selected graph the menu item opens no dialog. + let app = h.state_mut(); + app.ui.dialog = None; + app.run("select.set", json!({"ids": []})).unwrap(); + assert!(open(app).is_err()); + assert!(app.ui.dialog.is_none()); + } + + /// A Type button changes the graph's type and keeps its data. + #[test] + fn a_type_button_changes_the_graph_type_and_keeps_its_data() { + let mut app = app_with_graph("column"); + let data = app.session.execute("graph.setData", &json!({})).unwrap(); + assert_eq!(data["rows"], json!([[1.0, 2.0], [3.0, 4.0]])); + open(&mut app).unwrap(); + let mut h = harness(app); + for kind in GraphKind::ALL { + pick_type(&mut h, kind_name(kind)); + assert_eq!(h.state().ui.dialog.as_ref().unwrap().str("type"), kind.id()); + } + pick_type(&mut h, "Stacked Bar Graph"); + let app = h.state_mut(); + let before = steps(app); + crate::dialogs::confirm(app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(query(app)["type"], "stackedBar"); + assert_eq!(app.session.execute("graph.setData", &json!({})).unwrap(), data); + assert_eq!(steps(app), before + 1, "one step of history"); + } + + /// The menus list the values `graph.setType` takes, and Value Axis puts the value axis on the + /// side picked. + #[test] + fn the_menus_list_the_values_the_command_takes() { + let doc = vectorcraft_engine::find_command(COMMAND).unwrap().params; + let lists: [(&str, &[(&str, &str)]); 4] = + [("valueAxis", &SIDES), ("tickLength", &TICK_LENGTHS), ("pieLegend", &PIE_LEGENDS), ("pieSort", &PIE_SORTS)]; + for (key, options) in lists { + let values: Vec<&str> = options.iter().map(|(value, _)| *value).collect(); + assert!(doc.contains(&format!("{key}?: {}", values.join("|"))), "{key}: {values:?}"); + } + assert!(doc.contains("piePosition?: ratio|even|stacked")); + let mut app = app_with_graph("column"); + open(&mut app).unwrap(); + let mut h = harness(app); + assert!(h.get_by_role_and_label(Role::CheckBox, "Separate Scales").accesskit_node().is_disabled(), "one side: one scale"); + h.get_by_role_and_label(Role::ComboBox, "Value Axis:").click(); + h.run_steps(3); + h.get_by_label("On Both Sides").click(); + h.run_steps(3); + assert_eq!(h.state().ui.dialog.as_ref().unwrap().str("valueAxis"), "both"); + assert!(!h.get_by_role_and_label(Role::CheckBox, "Separate Scales").accesskit_node().is_disabled()); + let app = h.state_mut(); + crate::dialogs::confirm(app).unwrap(); + assert_eq!(query(app)["valueAxis"], "both"); + } + + /// The Options group shows the options of the type picked: widths for columns and bars, the + /// data point and line checkboxes for line, scatter and radar graphs, the three menus for a + /// pie graph, and none for an area graph. + #[test] + fn the_options_follow_the_type_picked() { + let mut app = app_with_graph("column"); + open(&mut app).unwrap(); + let mut h = harness(app); + let field = |h: &Harness, name: &str| h.query_by_role_and_label(Role::TextInput, name).is_some(); + let checkbox = |h: &Harness, name: &str| h.query_by_role_and_label(Role::CheckBox, name).is_some(); + let menu = |h: &Harness, name: &str| h.query_by_role_and_label(Role::ComboBox, name).is_some(); + let dimmed = |h: &Harness, name: &str| h.get_by_role_and_label(Role::CheckBox, name).accesskit_node().is_disabled(); + assert!(field(&h, "Column Width:") && field(&h, "Cluster Width:") && !checkbox(&h, "Mark Data Points") && !menu(&h, "Legend:")); + assert!(!dimmed(&h, "First Row in Front") && !dimmed(&h, "First Column in Front") && checkbox(&h, "Legend")); + pick_type(&mut h, "Bar Graph"); + assert!(field(&h, "Bar Width:") && !field(&h, "Column Width:") && field(&h, "Cluster Width:")); + assert!( + h.get_by_role_and_label(Role::ComboBox, "Value Axis:").accesskit_node().is_disabled(), + "a bar graph's value axis stays along the bottom" + ); + pick_type(&mut h, "Line Graph"); + assert!(!field(&h, "Cluster Width:") && dimmed(&h, "First Row in Front") && dimmed(&h, "First Column in Front")); + for name in ["Mark Data Points", "Connect Data Points", "Edge-to-Edge Lines", "Draw Filled Lines"] { + assert!(checkbox(&h, name) && !dimmed(&h, name), "{name}"); + } + // Draw Filled Lines is for connected data points, and Edge-to-Edge Lines for line graphs. + set(h.state_mut(), "connectPoints", json!(false)); + h.run_steps(3); + assert!(dimmed(&h, "Draw Filled Lines")); + for name in ["Scatter Graph", "Radar Graph"] { + pick_type(&mut h, name); + assert!(checkbox(&h, "Mark Data Points") && dimmed(&h, "Edge-to-Edge Lines"), "{name}"); + } + pick_type(&mut h, "Pie Graph"); + assert!(menu(&h, "Legend:") && menu(&h, "Position:") && menu(&h, "Sort:") && !checkbox(&h, "Legend") && !checkbox(&h, "Mark Data Points")); + assert!(h.get_by_role_and_label(Role::ComboBox, "Value Axis:").accesskit_node().is_disabled(), "a pie graph has no value axis"); + // Add Legend Across Top is for a legend outside the wedges. + assert!(!dimmed(&h, "Add Legend Across Top")); + set(h.state_mut(), "pieLegend", json!("wedges")); + h.run_steps(3); + assert!(dimmed(&h, "Add Legend Across Top")); + pick_type(&mut h, "Area Graph"); + assert!(!field(&h, "Column Width:") && !checkbox(&h, "Mark Data Points") && !menu(&h, "Legend:") && h.query_by_label("Options").is_none()); + } + + /// The menu at the top lists a page per axis of the type picked, and a second value axis page + /// while the value axis is on both sides. + #[test] + fn the_menu_lists_a_page_per_axis_of_the_type() { + let mut app = app_with_graph("column"); + open(&mut app).unwrap(); + let mut d = app.ui.dialog.clone().unwrap(); + let ids = |d: &Dialog| pages(d).iter().map(|(id, _)| *id).collect::>(); + let names = |d: &Dialog| pages(d).iter().map(|(_, name)| *name).collect::>(); + for kind in ["column", "stackedColumn", "bar", "stackedBar", "line", "area"] { + d.fields.insert("type".into(), json!(kind)); + assert_eq!(ids(&d), ["options", "valueAxis", "categoryAxis"], "{kind}"); + } + assert_eq!(names(&d), ["Graph Options", "Value Axis", "Category Axis"]); + for kind in ["scatter", "radar"] { + d.fields.insert("type".into(), json!(kind)); + assert_eq!(ids(&d), ["options", "valueAxis"], "{kind}"); + } + d.fields.insert("type".into(), json!("pie")); + assert_eq!(ids(&d), ["options"]); + d.fields.insert("valueAxis".into(), json!("both")); + for kind in ["column", "stackedColumn", "line", "area"] { + d.fields.insert("type".into(), json!(kind)); + assert_eq!(ids(&d), ["options", "valueAxis", "rightAxis", "categoryAxis"], "{kind}"); + assert_eq!(names(&d), ["Graph Options", "Left Axis", "Right Axis", "Category Axis"], "{kind}"); + } + d.fields.insert("type".into(), json!("scatter")); + assert_eq!(ids(&d), ["options", "valueAxis", "rightAxis"]); + // A bar or radar graph's value axis has no sides, and a pie graph has no value axis. + for (kind, pages) in + [("bar", vec!["options", "valueAxis", "categoryAxis"]), ("radar", vec!["options", "valueAxis"]), ("pie", vec!["options"])] + { + d.fields.insert("type".into(), json!(kind)); + assert_eq!(ids(&d), pages, "{kind}"); + } + // A page the type picked lacks shows Graph Options. + d.fields.insert(PAGE.into(), json!("rightAxis")); + app.ui.dialog = Some(d); + let h = harness(app); + assert!(h.query_by_role_and_label(Role::ComboBox, "Legend:").is_some()); + } + + /// Override Calculated Values starts from the tick values the axis is drawn with, OK sends + /// them, and turning it off again gives the axis its calculated values back. Left off, it + /// leaves a count the graph has alone. + #[test] + fn override_calculated_values_starts_from_the_calculated_tick_values() { + let mut app = app_with_graph("column"); + open(&mut app).unwrap(); + assert_eq!(calculated(app.ui.dialog.as_ref().unwrap(), &LEFT), (0.0, 4.0, 4)); + set(&mut app, PAGE, json!("valueAxis")); + let mut h = harness(app); + let dimmed = |h: &Harness, name: &str| h.get_by_role_and_label(Role::TextInput, name).accesskit_node().is_disabled(); + assert!(dimmed(&h, "Minimum:") && dimmed(&h, "Maximum:") && dimmed(&h, "Divisions:")); + h.get_by_role_and_label(Role::CheckBox, "Override Calculated Values").click(); + h.run_steps(3); + assert!(!dimmed(&h, "Minimum:") && !dimmed(&h, "Maximum:") && !dimmed(&h, "Divisions:")); + let app = h.state_mut(); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((number(d, "axisMin"), number(d, "axisMax"), number(d, "ticks")), (Some(0.0), Some(4.0), Some(4.0))); + let before = steps(app); + crate::dialogs::confirm(app).unwrap(); + let q = query(app); + assert_eq!((q["axisMin"].as_f64(), q["axisMax"].as_f64(), q["ticks"].as_u64()), (Some(0.0), Some(4.0), Some(4))); + assert_eq!(steps(app), before + 1); + // It opens on, with the graph's values, and off it sends the calculated values back. + open(app).unwrap(); + assert!(app.ui.dialog.as_ref().unwrap().bool("overrideValues")); + set(app, "axisMax", json!(8.0)); + crate::dialogs::confirm(app).unwrap(); + assert_eq!(query(app)["axisMax"], 8.0); + open(app).unwrap(); + set(app, "overrideValues", json!(false)); + crate::dialogs::confirm(app).unwrap(); + let q = query(app); + assert_eq!((q["axisMin"].clone(), q["axisMax"].clone(), q["ticks"].clone()), (Value::Null, Value::Null, json!(0))); + // Left off, a count the graph has stays, whatever else OK changes. + app.run(COMMAND, json!({"ticks": 7})).unwrap(); + open(app).unwrap(); + set(app, "dropShadow", json!(true)); + crate::dialogs::confirm(app).unwrap(); + let q = query(app); + assert_eq!((q["ticks"].clone(), q["dropShadow"].clone(), q["axisMin"].clone()), (json!(7), json!(true), Value::Null)); + } + + /// An overridden value axis whose maximum is not more than its minimum is refused: the dialog + /// stays open and the graph is not changed. The right axis counts while it has a scale of its + /// own. + #[test] + fn an_overridden_axis_needs_a_maximum_above_its_minimum() { + let mut app = app_with_graph("column"); + open(&mut app).unwrap(); + for (key, value) in [("overrideValues", json!(true)), ("axisMin", json!(10)), ("axisMax", json!(10))] { + set(&mut app, key, value); + } + let before = steps(&app); + assert_eq!(problem(app.ui.dialog.as_ref().unwrap()), Some("Maximum must be more than Minimum.")); + assert!(crate::dialogs::confirm(&mut app).is_err()); + assert!(app.ui.dialog.is_some(), "the dialog stays open"); + assert_eq!((steps(&app), query(&mut app)["axisMax"].clone()), (before, Value::Null)); + // A range too wide to divide is refused too. + set(&mut app, "axisMin", json!(-1.7e308)); + set(&mut app, "axisMax", json!(1.7e308)); + assert!(problem(app.ui.dialog.as_ref().unwrap()).is_some()); + set(&mut app, "axisMin", json!(10)); + set(&mut app, "axisMax", json!(20)); + crate::dialogs::confirm(&mut app).unwrap(); + let q = query(&mut app); + assert_eq!((q["axisMin"].as_f64(), q["axisMax"].as_f64()), (Some(10.0), Some(20.0))); + // The right axis shows the left scale until Separate Scales gives it one. + open(&mut app).unwrap(); + for (key, value) in [("rightOverrideValues", json!(true)), ("rightAxisMin", json!(5)), ("rightAxisMax", json!(1))] { + set(&mut app, key, value); + } + assert_eq!(problem(app.ui.dialog.as_ref().unwrap()), None); + set(&mut app, "valueAxis", json!("both")); + set(&mut app, "separateScales", json!(true)); + // A scale without series on its side draws no axis of its own. + assert_eq!(problem(app.ui.dialog.as_ref().unwrap()), None); + set(&mut app, RIGHT_SERIES, json!([1])); + assert!(problem(app.ui.dialog.as_ref().unwrap()).is_some()); + // A pie graph has no value axis to refuse. + set(&mut app, "type", json!("pie")); + assert_eq!(problem(app.ui.dialog.as_ref().unwrap()), None); + } + + /// OK with nothing changed closes the dialog without a step of history, on every page and + /// for every type. + #[test] + fn ok_with_nothing_changed_adds_no_step_of_history() { + for kind in GraphKind::ALL { + let mut app = app_with_graph(kind.id()); + app.run(COMMAND, json!({"valueAxis": "both", "axisMin": 0, "axisMax": 10})).unwrap(); + let before = steps(&app); + open(&mut app).unwrap(); + let mut h = harness(app); + for page in ["valueAxis", "rightAxis", "categoryAxis", "options"] { + set(h.state_mut(), PAGE, json!(page)); + h.run_steps(3); + } + let app = h.state_mut(); + assert_eq!(crate::dialogs::confirm(app), Ok(Value::Null), "{kind:?}"); + assert!(app.ui.dialog.is_none()); + assert_eq!(steps(app), before, "{kind:?}"); + } + } + + /// With series picked by Group Selection, the type and the value axis go to those series: the + /// types that cannot go in the graph are dimmed, Value Axis offers one side, and the other + /// options still go to the whole graph. + #[test] + fn picked_series_take_the_type_and_the_value_axis() { + let mut app = app_with_graph("column"); + let (graph, series) = { + let st = app.session.active().unwrap(); + let graph = st.selection.objects[0]; + (graph, st.doc.node(graph).unwrap().children().unwrap().iter().find(|c| c.extras().series_index == Some(1)).unwrap().id) + }; + app.run("select.set", json!({"ids": [series.0]})).unwrap(); + open(&mut app).unwrap(); + assert_eq!(picked(app.ui.dialog.as_ref().unwrap()), [1]); + let mut h = harness(app); + for name in ["Bar Graph", "Stacked Bar Graph", "Scatter Graph", "Pie Graph", "Radar Graph"] { + assert!(h.get_by_role_and_label(Role::Button, name).accesskit_node().is_disabled(), "{name}"); + pick_type(&mut h, name); + assert_eq!(h.state().ui.dialog.as_ref().unwrap().str("type"), "column", "{name}"); + } + h.get_by_role_and_label(Role::ComboBox, "Value Axis:").click(); + h.run_steps(3); + assert!(h.query_by_label("On Both Sides").is_none(), "a series goes on one axis"); + h.get_by_label("On Right Side").click(); + h.run_steps(3); + pick_type(&mut h, "Line Graph"); + // The picked line series brings the line options beside the columns'. + assert!(h.query_by_role_and_label(Role::CheckBox, "Mark Data Points").is_some()); + assert!(h.query_by_role_and_label(Role::TextInput, "Column Width:").is_some()); + let app = h.state_mut(); + set(app, "columnWidth", json!(50.0)); + crate::dialogs::confirm(app).unwrap(); + // The series stays picked, on its own type and axis. + let q = query(app); + assert_eq!((q["type"].clone(), q["valueAxis"].clone(), q["columnWidth"].clone()), (json!("line"), json!("right"), json!(50.0))); + let whole = app.session.execute(COMMAND, &json!({"id": graph.0})).unwrap(); + assert_eq!((whole["type"].clone(), whole["valueAxis"].clone()), (json!("column"), json!("left"))); + // Series on both value axes open with no side, and OK leaves each where it is. + let both: Vec = { + let st = app.session.active().unwrap(); + st.doc.node(graph).unwrap().children().unwrap().iter().filter(|c| c.extras().series_index.is_some()).map(|c| c.id.0).collect() + }; + app.run("select.set", json!({"ids": both})).unwrap(); + open(app).unwrap(); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((d.fields["valueAxis"].clone(), d.fields["type"].clone()), (Value::Null, Value::Null)); + assert_eq!(series_kinds(d), [GraphKind::Column, GraphKind::Line]); + set(app, "dropShadow", json!(true)); + crate::dialogs::confirm(app).unwrap(); + let st = app.session.active().unwrap(); + let spec = st.doc.node(graph).unwrap().extras().graph.as_deref().unwrap(); + assert_eq!((spec.right_series.clone(), spec.drop_shadow), (vec![1], true)); + assert_eq!(spec.series_kinds, [None, Some(GraphKind::Line)], "each series keeps its type"); + } + + /// `ui.dialog.set` takes the page shown and refuses the fields that hold what OK acts on. + #[test] + fn the_page_is_a_control_and_the_subject_is_protected() { + let mut app = app_with_graph("line"); + open(&mut app).unwrap(); + let d = app.ui.dialog.as_ref().unwrap(); + assert!(crate::dialogs::accepts_field(d, PAGE) && crate::dialogs::accepts_field(d, "overrideValues")); + for field in + [GRAPH_ID, SERIES, GRAPH, GRAPH_AXIS, SERIES_KINDS, RIGHT_SERIES, CALCULATED, CALCULATED_FOR, OPENED, "__override", "__rightOverride"] + { + assert!(crate::dialogs::protected_field(d, field), "{field}"); + } + } + + /// OK changes the graph and the series the dialog opened on, even when the selection changed + /// while it was open. + #[test] + fn ok_changes_the_graph_the_dialog_opened_on() { + let mut app = app_with_graph("column"); + let first = app.session.active().unwrap().selection.objects[0]; + app.run("graph.create", json!({"type": "column", "x": 0, "y": 200, "width": 200, "height": 150, "csv": ",a,b\nQ1,1,2\nQ2,3,4"})).unwrap(); + let second = app.session.active().unwrap().selection.objects[0]; + app.run("select.set", json!({"ids": [first.0]})).unwrap(); + open(&mut app).unwrap(); + set(&mut app, "type", json!("line")); + app.run("select.set", json!({"ids": [second.0]})).unwrap(); + crate::dialogs::confirm(&mut app).unwrap(); + let kind = + |app: &mut VectorcraftApp, id: vectorcraft_doc::NodeId| app.session.execute(COMMAND, &json!({"id": id.0})).unwrap()["type"].clone(); + assert_eq!((kind(&mut app, first), kind(&mut app, second)), (json!("line"), json!("column"))); + // Picked series keep the type picked for them when the selection moves to the whole graph. + let series = { + let st = app.session.active().unwrap(); + st.doc.node(first).unwrap().children().unwrap().iter().find(|c| c.extras().series_index == Some(1)).unwrap().id + }; + app.run("select.set", json!({"ids": [series.0]})).unwrap(); + open(&mut app).unwrap(); + set(&mut app, "type", json!("area")); + app.run("select.set", json!({"ids": [first.0]})).unwrap(); + crate::dialogs::confirm(&mut app).unwrap(); + let st = app.session.active().unwrap(); + let spec = st.doc.node(first).unwrap().extras().graph.as_deref().unwrap(); + assert_eq!((spec.kind, spec.series_kind(0), spec.series_kind(1)), (GraphKind::Line, GraphKind::Line, GraphKind::Area)); + } + + /// Override Calculated Values starts from the tick values of the type picked in the dialog, not + /// of the type the graph had when it opened. + #[test] + fn override_starts_from_the_tick_values_of_the_type_picked() { + let mut app = app_with_graph("column"); + open(&mut app).unwrap(); + assert_eq!(calculated(app.ui.dialog.as_ref().unwrap(), &LEFT).1, 4.0); + // Stacked, Q2 reaches 3 + 4 = 7. + set(&mut app, "type", json!("stackedColumn")); + set(&mut app, "overrideValues", json!(true)); + crate::dialogs::confirm(&mut app).unwrap(); + let q = query(&mut app); + assert_eq!(q["type"], "stackedColumn"); + assert!(q["axisMax"].as_f64().is_some_and(|max| max >= 7.0), "{}", q["axisMax"]); + let st = app.session.active().unwrap(); + let spec = st.doc.node(st.selection.objects[0]).unwrap().extras().graph.as_deref().unwrap(); + // The calculated values do not depend on the override the graph now has. + let stacked = vectorcraft_engine::graph_calculated_axes(spec)[0]; + let sent = |q: &Value| (q["axisMin"].as_f64(), q["axisMax"].as_f64(), q["ticks"].as_u64()); + assert_eq!(sent(&q), (Some(stacked.0), Some(stacked.1), Some(stacked.2 as u64))); + // Turned on first, the values filled for the column graph follow the type picked after it. + let mut app = app_with_graph("column"); + open(&mut app).unwrap(); + set(&mut app, "overrideValues", json!(true)); + settle(app.ui.dialog.as_mut().unwrap()); + assert_eq!(number(app.ui.dialog.as_ref().unwrap(), "axisMax"), Some(4.0)); + set(&mut app, "type", json!("stackedColumn")); + crate::dialogs::confirm(&mut app).unwrap(); + assert_eq!(sent(&query(&mut app)), (Some(stacked.0), Some(stacked.1), Some(stacked.2 as u64))); + // Values typed in stay. + open(&mut app).unwrap(); + set(&mut app, "axisMax", json!(50.0)); + set(&mut app, "type", json!("column")); + crate::dialogs::confirm(&mut app).unwrap(); + assert_eq!(query(&mut app)["axisMax"], 50.0); + } + + /// The limits the fields keep to are the ones `graph.setType` keeps to. + #[test] + fn the_fields_keep_to_the_command_s_limits() { + let mut app = app_with_graph("column"); + let long = "x".repeat(AFFIX_CHARS + 6); + app.run(COMMAND, json!({"tickMarks": 99, "columnWidth": 5000, "clusterWidth": 0, "prefix": long, "axisMin": 0, "axisMax": 1, "ticks": 500})) + .unwrap(); + let q = query(&mut app); + assert_eq!(q["tickMarks"].as_f64(), Some(*TICK_MARKS.end())); + assert_eq!((q["columnWidth"].as_f64(), q["clusterWidth"].as_f64()), (Some(*WIDTHS.end()), Some(*WIDTHS.start()))); + assert_eq!(q["prefix"].as_str().map(str::len), Some(AFFIX_CHARS)); + assert_eq!(q["ticks"].as_f64(), Some(*DIVISIONS.end())); + } +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 28944173dd..a335c9f424 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1359,6 +1359,40 @@ font-technology Variable Променливи Graph Диаграма Graph Data Данни за диаграма Graph Type Тип диаграма + Graph Options Опции на диаграмата + Left Axis Лява ос + Right Axis Дясна ос + Category Axis Ос на категориите + Separate Scales Отделни скали + Add Drop Shadow Добавяне на падаща сянка + Add Legend Across Top Добавяне на легенда отгоре + First Row in Front Първият ред отпред + First Column in Front Първата колона отпред + Legend Легенда + Column Width Ширина на колоната + Bar Width Ширина на стълба + Cluster Width Ширина на групата + Mark Data Points Маркиране на точките от данни + Connect Data Points Свързване на точките от данни + Edge-to-Edge Lines Линии от край до край + Draw Filled Lines Чертане на запълнени линии + Tick Values Стойности на отметките + Override Calculated Values Замяна на изчислените стойности + Divisions Деления + Tick Marks Отметки + Tick Marks per Division Отметки на деление + Draw Tick Marks Between Labels Чертане на отметки между етикетите + Add Labels Добавяне на етикети + Maximum must be more than Minimum. Максимумът трябва да е по-голям от минимума. + Column Graph Колонна диаграма + Stacked Column Graph Колонна диаграма с натрупване + Bar Graph Стълбчеста диаграма + Stacked Bar Graph Стълбчеста диаграма с натрупване + Line Graph Линейна диаграма + Area Graph Площна диаграма + Scatter Graph Диаграма на разсейване + Pie Graph Кръгова диаграма + Radar Graph Радарна диаграма Graph Design Дизайн на диаграма Graph Marker Маркер на диаграма New Design Нов дизайн diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 01b90e5148..dbe13b1475 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -3546,6 +3546,40 @@ theme Appearance Izgled Create Graph Kreiraj grafikon Graph Data Podaci grafikona Graph Type Vrsta grafikona + Graph Options Opcije grafikona + Left Axis Lijeva osa + Right Axis Desna osa + Category Axis Osa kategorija + Separate Scales Odvojene skale + Add Drop Shadow Dodaj sjenu + Add Legend Across Top Dodaj legendu na vrh + First Row in Front Prvi red naprijed + First Column in Front Prva kolona naprijed + Legend Legenda + Column Width Širina stupca + Bar Width Širina trake + Cluster Width Širina grupe + Mark Data Points Označi tačke podataka + Connect Data Points Poveži tačke podataka + Edge-to-Edge Lines Linije od ivice do ivice + Draw Filled Lines Crtaj ispunjene linije + Tick Values Vrijednosti crtica + Override Calculated Values Zamijeni izračunate vrijednosti + Divisions Podioci + Tick Marks Crtice + Tick Marks per Division Crtica po podioku + Draw Tick Marks Between Labels Crtaj crtice između oznaka + Add Labels Dodaj oznake + Maximum must be more than Minimum. Maksimum mora biti veći od minimuma. + Column Graph Stupčasti grafikon + Stacked Column Graph Naslagani stupčasti grafikon + Bar Graph Trakasti grafikon + Stacked Bar Graph Naslagani trakasti grafikon + Line Graph Linijski grafikon + Area Graph Površinski grafikon + Scatter Graph Raspršeni grafikon + Pie Graph Tortni grafikon + Radar Graph Radarski grafikon Vector Halftone Vektorski polutonovi spot spot Text Tekst diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 4ddd1d5605..b388182f3e 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -1891,6 +1891,40 @@ digits Malayalam Malaiàlam Graph Design Disseny del gràfic Graph Marker Marcador del gràfic Graph Type Tipus de gràfic + Graph Options Opcions del gràfic + Left Axis Eix esquerre + Right Axis Eix dret + Category Axis Eix de categories + Separate Scales Escales separades + Add Drop Shadow Afegeix una ombra paral·lela + Add Legend Across Top Afegeix la llegenda a la part superior + First Row in Front Primera fila al davant + First Column in Front Primera columna al davant + Legend Llegenda + Column Width Amplada de columna + Bar Width Amplada de barra + Cluster Width Amplada del grup + Mark Data Points Marca els punts de dades + Connect Data Points Connecta els punts de dades + Edge-to-Edge Lines Línies de vora a vora + Draw Filled Lines Dibuixa línies emplenades + Tick Values Valors de les marques + Override Calculated Values Substitueix els valors calculats + Divisions Divisions + Tick Marks Marques de graduació + Tick Marks per Division Marques per divisió + Draw Tick Marks Between Labels Dibuixa les marques entre les etiquetes + Add Labels Afegeix etiquetes + Maximum must be more than Minimum. El màxim ha de ser més gran que el mínim. + Column Graph Gràfic de columnes + Stacked Column Graph Gràfic de columnes apilades + Bar Graph Gràfic de barres + Stacked Bar Graph Gràfic de barres apilades + Line Graph Gràfic de línies + Area Graph Gràfic d’àrea + Scatter Graph Gràfic de dispersió + Pie Graph Gràfic de sectors + Radar Graph Gràfic de radar Graphic Gràfic Graphic Pen Ploma gràfica Graphic Pen… Ploma gràfica… diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 211982f086..96f8e70e0d 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3600,6 +3600,40 @@ theme Appearance Motiv Create Graph Vytvořit graf Graph Data Data grafu Graph Type Typ grafu + Graph Options Možnosti grafu + Left Axis Levá osa + Right Axis Pravá osa + Category Axis Osa kategorií + Separate Scales Samostatná měřítka + Add Drop Shadow Přidat vržený stín + Add Legend Across Top Přidat legendu nahoru + First Row in Front První řádek vpředu + First Column in Front První sloupec vpředu + Legend Legenda + Column Width Šířka sloupce + Bar Width Šířka pruhu + Cluster Width Šířka skupiny + Mark Data Points Označit datové body + Connect Data Points Spojit datové body + Edge-to-Edge Lines Čáry od okraje k okraji + Draw Filled Lines Kreslit vyplněné čáry + Tick Values Hodnoty značek + Override Calculated Values Přepsat vypočítané hodnoty + Divisions Dílky + Tick Marks Značky + Tick Marks per Division Značek na dílek + Draw Tick Marks Between Labels Kreslit značky mezi popisky + Add Labels Přidat popisky + Maximum must be more than Minimum. Maximum musí být větší než minimum. + Column Graph Sloupcový graf + Stacked Column Graph Skládaný sloupcový graf + Bar Graph Pruhový graf + Stacked Bar Graph Skládaný pruhový graf + Line Graph Spojnicový graf + Area Graph Plošný graf + Scatter Graph Bodový graf + Pie Graph Výsečový graf + Radar Graph Paprskový graf Vector Halftone Vektorový polotón spot přímá Text Text diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index b3de0ae89a..428f8fbbff 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -3331,6 +3331,40 @@ theme Appearance Udseende Create Graph Opret graf Graph Data Grafdata Graph Type Graftype + Graph Options Grafindstillinger + Left Axis Venstre akse + Right Axis Højre akse + Category Axis Kategoriakse + Separate Scales Separate skalaer + Add Drop Shadow Tilføj slagskygge + Add Legend Across Top Tilføj forklaring øverst + First Row in Front Første række forrest + First Column in Front Første kolonne forrest + Legend Forklaring + Column Width Søjlebredde + Bar Width Bjælkebredde + Cluster Width Gruppebredde + Mark Data Points Markér datapunkter + Connect Data Points Forbind datapunkter + Edge-to-Edge Lines Linjer fra kant til kant + Draw Filled Lines Tegn udfyldte linjer + Tick Values Afmærkningsværdier + Override Calculated Values Tilsidesæt beregnede værdier + Divisions Inddelinger + Tick Marks Afmærkninger + Tick Marks per Division Afmærkninger pr. inddeling + Draw Tick Marks Between Labels Tegn afmærkninger mellem etiketter + Add Labels Tilføj etiketter + Maximum must be more than Minimum. Maksimum skal være større end minimum. + Column Graph Søjlediagram + Stacked Column Graph Stablet søjlediagram + Bar Graph Bjælkediagram + Stacked Bar Graph Stablet bjælkediagram + Line Graph Linjediagram + Area Graph Områdediagram + Scatter Graph Spredningsdiagram + Pie Graph Cirkeldiagram + Radar Graph Radardiagram Vector Halftone Vektorraster spot spot Text Tekst diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index d0610144ef..489bf32d94 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1404,6 +1404,40 @@ file-filter Templates Vorlagen Graph Diagramm Graph Data Diagrammdaten Graph Type Diagrammtyp + Graph Options Diagrammoptionen + Left Axis Linke Achse + Right Axis Rechte Achse + Category Axis Kategorieachse + Separate Scales Getrennte Skalen + Add Drop Shadow Schlagschatten hinzufügen + Add Legend Across Top Legende oben hinzufügen + First Row in Front Erste Zeile vorne + First Column in Front Erste Spalte vorne + Legend Legende + Column Width Säulenbreite + Bar Width Balkenbreite + Cluster Width Gruppenbreite + Mark Data Points Datenpunkte markieren + Connect Data Points Datenpunkte verbinden + Edge-to-Edge Lines Linien von Kante zu Kante + Draw Filled Lines Gefüllte Linien zeichnen + Tick Values Teilstrichwerte + Override Calculated Values Berechnete Werte überschreiben + Divisions Teilungen + Tick Marks Teilstriche + Tick Marks per Division Teilstriche pro Teilung + Draw Tick Marks Between Labels Teilstriche zwischen Beschriftungen zeichnen + Add Labels Beschriftungen hinzufügen + Maximum must be more than Minimum. Das Maximum muss größer als das Minimum sein. + Column Graph Säulendiagramm + Stacked Column Graph Gestapeltes Säulendiagramm + Bar Graph Balkendiagramm + Stacked Bar Graph Gestapeltes Balkendiagramm + Line Graph Liniendiagramm + Area Graph Flächendiagramm + Scatter Graph Streudiagramm + Pie Graph Kreisdiagramm + Radar Graph Netzdiagramm Graph Design Diagrammdesign Graph Marker Diagrammmarkierung New Design Neues Design diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index d03f8c6958..1f67f30bb3 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1334,6 +1334,40 @@ font-technology Variable Μεταβλητές Graph Γράφημα Graph Data Δεδομένα γραφήματος Graph Type Τύπος γραφήματος + Graph Options Επιλογές γραφήματος + Left Axis Αριστερός άξονας + Right Axis Δεξιός άξονας + Category Axis Άξονας κατηγοριών + Separate Scales Ξεχωριστές κλίμακες + Add Drop Shadow Προσθήκη εξωτερικής σκιάς + Add Legend Across Top Προσθήκη υπομνήματος στο επάνω μέρος + First Row in Front Πρώτη γραμμή μπροστά + First Column in Front Πρώτη στήλη μπροστά + Legend Υπόμνημα + Column Width Πλάτος στήλης + Bar Width Πλάτος ράβδου + Cluster Width Πλάτος ομάδας + Mark Data Points Σήμανση σημείων δεδομένων + Connect Data Points Σύνδεση σημείων δεδομένων + Edge-to-Edge Lines Γραμμές από άκρη σε άκρη + Draw Filled Lines Σχεδίαση γεμισμένων γραμμών + Tick Values Τιμές υποδιαιρέσεων + Override Calculated Values Αντικατάσταση υπολογισμένων τιμών + Divisions Διαιρέσεις + Tick Marks Σημάδια υποδιαίρεσης + Tick Marks per Division Σημάδια ανά διαίρεση + Draw Tick Marks Between Labels Σχεδίαση σημαδιών ανάμεσα στις ετικέτες + Add Labels Προσθήκη ετικετών + Maximum must be more than Minimum. Το μέγιστο πρέπει να είναι μεγαλύτερο από το ελάχιστο. + Column Graph Γράφημα στηλών + Stacked Column Graph Γράφημα στοιβαγμένων στηλών + Bar Graph Ραβδόγραμμα + Stacked Bar Graph Στοιβαγμένο ραβδόγραμμα + Line Graph Γραμμικό γράφημα + Area Graph Γράφημα περιοχής + Scatter Graph Διάγραμμα διασποράς + Pie Graph Κυκλικό γράφημα + Radar Graph Αραχνοειδές γράφημα Graph Design Σχεδίαση γραφήματος Graph Marker Δείκτης γραφήματος New Design Νέο σχέδιο diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 3e8bd23752..b50fa85739 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1413,6 +1413,40 @@ font-technology Variable Variables Graph Gráfico Graph Data Datos del gráfico Graph Type Tipo de gráfico + Graph Options Opciones de gráfico + Left Axis Eje izquierdo + Right Axis Eje derecho + Category Axis Eje de categorías + Separate Scales Escalas separadas + Add Drop Shadow Añadir sombra paralela + Add Legend Across Top Añadir leyenda en la parte superior + First Row in Front Primera fila delante + First Column in Front Primera columna delante + Legend Leyenda + Column Width Anchura de columna + Bar Width Anchura de barra + Cluster Width Anchura de grupo + Mark Data Points Marcar puntos de datos + Connect Data Points Conectar puntos de datos + Edge-to-Edge Lines Líneas de borde a borde + Draw Filled Lines Dibujar líneas rellenas + Tick Values Valores de marcas + Override Calculated Values Sustituir valores calculados + Divisions Divisiones + Tick Marks Marcas de graduación + Tick Marks per Division Marcas por división + Draw Tick Marks Between Labels Dibujar marcas entre etiquetas + Add Labels Añadir etiquetas + Maximum must be more than Minimum. El máximo debe ser mayor que el mínimo. + Column Graph Gráfico de columnas + Stacked Column Graph Gráfico de columnas apiladas + Bar Graph Gráfico de barras + Stacked Bar Graph Gráfico de barras apiladas + Line Graph Gráfico de líneas + Area Graph Gráfico de áreas + Scatter Graph Gráfico de dispersión + Pie Graph Gráfico circular + Radar Graph Gráfico de radar Graph Design Diseño de gráfico Graph Marker Marcador de gráfico New Design Nuevo diseño diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index d408220f47..d6cf919251 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -3538,6 +3538,40 @@ theme Appearance Ulkoasu Create Graph Luo kaavio Graph Data Kaavion tiedot Graph Type Kaavion tyyppi + Graph Options Kaavion asetukset + Left Axis Vasen akseli + Right Axis Oikea akseli + Category Axis Luokka-akseli + Separate Scales Erilliset asteikot + Add Drop Shadow Lisää pudotusvarjo + Add Legend Across Top Lisää selite ylös + First Row in Front Ensimmäinen rivi edessä + First Column in Front Ensimmäinen sarake edessä + Legend Selite + Column Width Pylvään leveys + Bar Width Palkin leveys + Cluster Width Ryhmän leveys + Mark Data Points Merkitse arvopisteet + Connect Data Points Yhdistä arvopisteet + Edge-to-Edge Lines Viivat reunasta reunaan + Draw Filled Lines Piirrä täytetyt viivat + Tick Values Asteikon arvot + Override Calculated Values Ohita lasketut arvot + Divisions Jaot + Tick Marks Asteikkomerkit + Tick Marks per Division Asteikkomerkkejä jakoa kohden + Draw Tick Marks Between Labels Piirrä asteikkomerkit nimien väliin + Add Labels Lisää nimet + Maximum must be more than Minimum. Suurimman on oltava pienintä suurempi. + Column Graph Pylväskaavio + Stacked Column Graph Pinottu pylväskaavio + Bar Graph Palkkikaavio + Stacked Bar Graph Pinottu palkkikaavio + Line Graph Viivakaavio + Area Graph Aluekaavio + Scatter Graph Hajontakaavio + Pie Graph Ympyräkaavio + Radar Graph Säteittäinen kaavio Vector Halftone Vektorirasteri spot spotti Text Teksti diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 318bbbeb44..58239c33f0 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1407,6 +1407,40 @@ font-technology Variable Variables Graph Graphe Graph Data Données du graphe Graph Type Type de graphe + Graph Options Options du graphe + Left Axis Axe gauche + Right Axis Axe droit + Category Axis Axe des catégories + Separate Scales Échelles distinctes + Add Drop Shadow Ajouter une ombre portée + Add Legend Across Top Ajouter la légende en haut + First Row in Front Première ligne au premier plan + First Column in Front Première colonne au premier plan + Legend Légende + Column Width Largeur des colonnes + Bar Width Largeur des barres + Cluster Width Largeur des groupes + Mark Data Points Marquer les points de données + Connect Data Points Relier les points de données + Edge-to-Edge Lines Lignes bord à bord + Draw Filled Lines Tracer des lignes pleines + Tick Values Valeurs des graduations + Override Calculated Values Remplacer les valeurs calculées + Divisions Divisions + Tick Marks Graduations + Tick Marks per Division Graduations par division + Draw Tick Marks Between Labels Tracer les graduations entre les étiquettes + Add Labels Ajouter des étiquettes + Maximum must be more than Minimum. Le maximum doit être supérieur au minimum. + Column Graph Graphe à colonnes + Stacked Column Graph Graphe à colonnes empilées + Bar Graph Graphe à barres + Stacked Bar Graph Graphe à barres empilées + Line Graph Graphe linéaire + Area Graph Graphe de zones + Scatter Graph Graphe de dispersion + Pie Graph Graphe à secteurs + Radar Graph Graphe radar Graph Design Conception de graphe Graph Marker Marqueur de graphe New Design Nouvelle conception diff --git a/crates/ui-egui/src/i18n/gu.tsv b/crates/ui-egui/src/i18n/gu.tsv index 716d851655..6489e8b8ce 100644 --- a/crates/ui-egui/src/i18n/gu.tsv +++ b/crates/ui-egui/src/i18n/gu.tsv @@ -1533,6 +1533,40 @@ Graph Design ગ્રાફ ડિઝાઇન Graph Marker ગ્રાફ માર્કર Graph Type ગ્રાફ પ્રકાર + Graph Options ગ્રાફ વિકલ્પો + Left Axis ડાબી અક્ષ + Right Axis જમણી અક્ષ + Category Axis શ્રેણી અક્ષ + Separate Scales અલગ સ્કેલ + Add Drop Shadow ડ્રોપ શેડો ઉમેરો + Add Legend Across Top ઉપર લેજન્ડ ઉમેરો + First Row in Front પ્રથમ પંક્તિ આગળ + First Column in Front પ્રથમ કૉલમ આગળ + Legend લેજન્ડ + Column Width કૉલમ પહોળાઈ + Bar Width બાર પહોળાઈ + Cluster Width જૂથ પહોળાઈ + Mark Data Points ડેટા પોઇન્ટ ચિહ્નિત કરો + Connect Data Points ડેટા પોઇન્ટ જોડો + Edge-to-Edge Lines કિનારીથી કિનારી સુધી રેખાઓ + Draw Filled Lines ભરેલી રેખાઓ દોરો + Tick Values ટિક મૂલ્યો + Override Calculated Values ગણતરી કરેલાં મૂલ્યો બદલો + Divisions વિભાગો + Tick Marks ટિક ચિહ્નો + Tick Marks per Division વિભાગ દીઠ ટિક ચિહ્નો + Draw Tick Marks Between Labels લેબલ વચ્ચે ટિક ચિહ્નો દોરો + Add Labels લેબલ ઉમેરો + Maximum must be more than Minimum. મહત્તમ ન્યૂનતમ કરતાં વધુ હોવું જોઈએ. + Column Graph કૉલમ ગ્રાફ + Stacked Column Graph સ્ટેક્ડ કૉલમ ગ્રાફ + Bar Graph બાર ગ્રાફ + Stacked Bar Graph સ્ટેક્ડ બાર ગ્રાફ + Line Graph રેખા ગ્રાફ + Area Graph ક્ષેત્ર ગ્રાફ + Scatter Graph સ્કેટર ગ્રાફ + Pie Graph પાઇ ગ્રાફ + Radar Graph રડાર ગ્રાફ Graphic Pen… ગ્રાફિક પેન… Graphic Style ગ્રાફિક સ્ટાઇલ Graphic Style Libraries ગ્રાફિક સ્ટાઇલ લાઇબ્રેરીઓ diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 78c0a9a7eb..d3685c8b9a 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -2101,6 +2101,40 @@ antiAlias Sharp शार्प Graph Design ग्राफ़ डिजाइन Graph Marker ग्राफ़ मार्कर Graph Type ग्राफ़ टाइप + Graph Options ग्राफ़ विकल्प + Left Axis बायाँ एक्सिस + Right Axis दायाँ एक्सिस + Category Axis श्रेणी एक्सिस + Separate Scales अलग स्केल + Add Drop Shadow ड्रॉप शैडो जोड़ें + Add Legend Across Top ऊपर लेजेंड जोड़ें + First Row in Front पहली पंक्ति आगे + First Column in Front पहला कॉलम आगे + Legend लेजेंड + Column Width कॉलम चौड़ाई + Bar Width बार चौड़ाई + Cluster Width समूह चौड़ाई + Mark Data Points डेटा पॉइंट चिह्नित करें + Connect Data Points डेटा पॉइंट जोड़ें + Edge-to-Edge Lines किनारे से किनारे तक रेखाएँ + Draw Filled Lines भरी हुई रेखाएँ बनाएँ + Tick Values टिक मान + Override Calculated Values परिकलित मानों को बदलें + Divisions विभाजन + Tick Marks टिक चिह्न + Tick Marks per Division प्रति विभाजन टिक चिह्न + Draw Tick Marks Between Labels लेबलों के बीच टिक चिह्न बनाएँ + Add Labels लेबल जोड़ें + Maximum must be more than Minimum. अधिकतम, न्यूनतम से अधिक होना चाहिए। + Column Graph कॉलम ग्राफ़ + Stacked Column Graph स्टैक्ड कॉलम ग्राफ़ + Bar Graph बार ग्राफ़ + Stacked Bar Graph स्टैक्ड बार ग्राफ़ + Line Graph लाइन ग्राफ़ + Area Graph एरिया ग्राफ़ + Scatter Graph स्कैटर ग्राफ़ + Pie Graph पाई ग्राफ़ + Radar Graph रडार ग्राफ़ Graphic ग्राफिक Graphic Pen ग्राफ़िक पेन Graphic Pen… ग्राफ़िक पेन… diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 7f2f03afb2..e656bfb5a3 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1759,6 +1759,40 @@ file-filter Templates Sablonok Graph Diagram Graph Data Diagramadatok Graph Type Diagramtípus + Graph Options Diagrambeállítások + Left Axis Bal tengely + Right Axis Jobb tengely + Category Axis Kategóriatengely + Separate Scales Külön skálák + Add Drop Shadow Vetett árnyék hozzáadása + Add Legend Across Top Jelmagyarázat felül + First Row in Front Első sor elöl + First Column in Front Első oszlop elöl + Legend Jelmagyarázat + Column Width Oszlopszélesség + Bar Width Sávszélesség + Cluster Width Csoportszélesség + Mark Data Points Adatpontok jelölése + Connect Data Points Adatpontok összekötése + Edge-to-Edge Lines Széltől szélig érő vonalak + Draw Filled Lines Kitöltött vonalak rajzolása + Tick Values Osztásértékek + Override Calculated Values Számított értékek felülírása + Divisions Osztások + Tick Marks Osztásjelek + Tick Marks per Division Osztásjelek osztásonként + Draw Tick Marks Between Labels Osztásjelek a címkék között + Add Labels Címkék hozzáadása + Maximum must be more than Minimum. A maximumnak nagyobbnak kell lennie a minimumnál. + Column Graph Oszlopdiagram + Stacked Column Graph Halmozott oszlopdiagram + Bar Graph Sávdiagram + Stacked Bar Graph Halmozott sávdiagram + Line Graph Vonaldiagram + Area Graph Területdiagram + Scatter Graph Pontdiagram + Pie Graph Kördiagram + Radar Graph Sugárdiagram Select Segments Szakaszok kijelölése Artistic Művészi Film Grain… Filmszemcse… diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index c7c6b956db..897825f1d9 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -3331,6 +3331,40 @@ theme Appearance Tampilan Create Graph Buat grafik Graph Data Data grafik Graph Type Jenis grafik + Graph Options Opsi grafik + Left Axis Sumbu kiri + Right Axis Sumbu kanan + Category Axis Sumbu kategori + Separate Scales Skala terpisah + Add Drop Shadow Tambahkan bayangan jatuh + Add Legend Across Top Tambahkan legenda di atas + First Row in Front Baris pertama di depan + First Column in Front Kolom pertama di depan + Legend Legenda + Column Width Lebar kolom + Bar Width Lebar batang + Cluster Width Lebar kelompok + Mark Data Points Tandai titik data + Connect Data Points Hubungkan titik data + Edge-to-Edge Lines Garis dari tepi ke tepi + Draw Filled Lines Gambar garis terisi + Tick Values Nilai tanda skala + Override Calculated Values Timpa nilai terhitung + Divisions Pembagian + Tick Marks Tanda skala + Tick Marks per Division Tanda skala per pembagian + Draw Tick Marks Between Labels Gambar tanda skala di antara label + Add Labels Tambahkan label + Maximum must be more than Minimum. Maksimum harus lebih besar dari minimum. + Column Graph Grafik kolom + Stacked Column Graph Grafik kolom bertumpuk + Bar Graph Grafik batang + Stacked Bar Graph Grafik batang bertumpuk + Line Graph Grafik garis + Area Graph Grafik area + Scatter Graph Grafik sebar + Pie Graph Grafik pai + Radar Graph Grafik radar Vector Halftone Halftone vektor spot spot Text Teks diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index f832a5baf3..83a454af1b 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1434,6 +1434,40 @@ file-filter Templates Modelli Graph Grafico Graph Data Dati grafico Graph Type Tipo di grafico + Graph Options Opzioni grafico + Left Axis Asse sinistro + Right Axis Asse destro + Category Axis Asse delle categorie + Separate Scales Scale separate + Add Drop Shadow Aggiungi ombra esterna + Add Legend Across Top Aggiungi legenda in alto + First Row in Front Prima riga in primo piano + First Column in Front Prima colonna in primo piano + Legend Legenda + Column Width Larghezza colonne + Bar Width Larghezza barre + Cluster Width Larghezza gruppi + Mark Data Points Contrassegna punti dati + Connect Data Points Collega punti dati + Edge-to-Edge Lines Linee da bordo a bordo + Draw Filled Lines Disegna linee piene + Tick Values Valori delle tacche + Override Calculated Values Sostituisci valori calcolati + Divisions Divisioni + Tick Marks Tacche + Tick Marks per Division Tacche per divisione + Draw Tick Marks Between Labels Disegna tacche tra le etichette + Add Labels Aggiungi etichette + Maximum must be more than Minimum. Il massimo deve essere maggiore del minimo. + Column Graph Grafico a colonne + Stacked Column Graph Grafico a colonne in pila + Bar Graph Grafico a barre + Stacked Bar Graph Grafico a barre in pila + Line Graph Grafico a linee + Area Graph Grafico ad area + Scatter Graph Grafico a dispersione + Pie Graph Grafico a torta + Radar Graph Grafico radar Graph Design Design del grafico Graph Marker Indicatore del grafico New Design Nuovo design diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 3abb388f49..aa79ddaf31 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3836,6 +3836,40 @@ file-filter Templates テンプレート Gradient (. or Shift+.) グラデーション (. または Shift+.) Graph Data グラフデータ Graph Type グラフの種類 + Graph Options グラフオプション + Left Axis 左軸 + Right Axis 右軸 + Category Axis 項目軸 + Separate Scales 目盛りを別にする + Add Drop Shadow ドロップシャドウを追加 + Add Legend Across Top 凡例を上に表示 + First Row in Front 最初の行を前面に + First Column in Front 最初の列を前面に + Legend 凡例 + Column Width 棒の幅 + Bar Width 横棒の幅 + Cluster Width グループの幅 + Mark Data Points データポイントにマーカーを表示 + Connect Data Points データポイントを線でつなぐ + Edge-to-Edge Lines 端から端までの線 + Draw Filled Lines 線を塗りつぶして描く + Tick Values 目盛りの値 + Override Calculated Values 計算値を上書き + Divisions 分割数 + Tick Marks 目盛り + Tick Marks per Division 分割ごとの目盛り + Draw Tick Marks Between Labels ラベルの間に目盛りを描く + Add Labels ラベルを追加 + Maximum must be more than Minimum. 最大値は最小値より大きくしてください。 + Column Graph 棒グラフ + Stacked Column Graph 積み上げ棒グラフ + Bar Graph 横向き棒グラフ + Stacked Bar Graph 横向き積み上げ棒グラフ + Line Graph 折れ線グラフ + Area Graph 面グラフ + Scatter Graph 散布図 + Pie Graph 円グラフ + Radar Graph レーダーチャート Graph Design グラフのデザイン Graph Marker グラフのマーカー New Design 新規デザイン diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 55f726dc38..8d68d5db9f 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -3177,6 +3177,40 @@ theme Appearance იერსახე Create Graph გრაფიკის შექმნა Graph Data გრაფიკის მონაცემები Graph Type გრაფიკის ტიპი + Graph Options გრაფიკის პარამეტრები + Left Axis მარცხენა ღერძი + Right Axis მარჯვენა ღერძი + Category Axis კატეგორიების ღერძი + Separate Scales ცალკე სკალები + Add Drop Shadow ჩრდილის დამატება + Add Legend Across Top ლეგენდის დამატება ზემოთ + First Row in Front პირველი მწკრივი წინ + First Column in Front პირველი სვეტი წინ + Legend ლეგენდა + Column Width სვეტის სიგანე + Bar Width ზოლის სიგანე + Cluster Width ჯგუფის სიგანე + Mark Data Points მონაცემთა წერტილების მონიშვნა + Connect Data Points მონაცემთა წერტილების შეერთება + Edge-to-Edge Lines ხაზები კიდიდან კიდემდე + Draw Filled Lines შევსებული ხაზების დახატვა + Tick Values დანაყოფების მნიშვნელობები + Override Calculated Values გამოთვლილი მნიშვნელობების შეცვლა + Divisions დანაყოფები + Tick Marks დანაყოფის ნიშნები + Tick Marks per Division ნიშნები თითო დანაყოფზე + Draw Tick Marks Between Labels ნიშნების დახატვა წარწერებს შორის + Add Labels წარწერების დამატება + Maximum must be more than Minimum. მაქსიმუმი მინიმუმზე მეტი უნდა იყოს. + Column Graph სვეტოვანი გრაფიკი + Stacked Column Graph დაწყობილი სვეტოვანი გრაფიკი + Bar Graph ზოლოვანი გრაფიკი + Stacked Bar Graph დაწყობილი ზოლოვანი გრაფიკი + Line Graph ხაზოვანი გრაფიკი + Area Graph არეების გრაფიკი + Scatter Graph გაფანტული გრაფიკი + Pie Graph წრიული გრაფიკი + Radar Graph რადარული გრაფიკი Vector Halftone ვექტორული ნახევარტონი spot სპოტ Text ტექსტი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index abe2b4bc28..6a10e0bc69 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -3824,6 +3824,40 @@ file-filter Templates 템플릿 Gradient (. or Shift+.) 그레이디언트 (. 또는 Shift+.) Graph Data 그래프 데이터 Graph Type 그래프 유형 + Graph Options 그래프 옵션 + Left Axis 왼쪽 축 + Right Axis 오른쪽 축 + Category Axis 범주 축 + Separate Scales 별도 눈금 + Add Drop Shadow 드롭 섀도 추가 + Add Legend Across Top 위쪽에 범례 추가 + First Row in Front 첫 행을 앞에 + First Column in Front 첫 열을 앞에 + Legend 범례 + Column Width 세로 막대 폭 + Bar Width 가로 막대 폭 + Cluster Width 묶음 폭 + Mark Data Points 데이터 요소 표시 + Connect Data Points 데이터 요소 연결 + Edge-to-Edge Lines 가장자리 간 선 + Draw Filled Lines 채워진 선 그리기 + Tick Values 눈금 값 + Override Calculated Values 계산된 값 재정의 + Divisions 구간 + Tick Marks 눈금 표시 + Tick Marks per Division 구간당 눈금 표시 + Draw Tick Marks Between Labels 레이블 사이에 눈금 표시 그리기 + Add Labels 레이블 추가 + Maximum must be more than Minimum. 최대값은 최소값보다 커야 합니다. + Column Graph 세로 막대 그래프 + Stacked Column Graph 누적 세로 막대 그래프 + Bar Graph 가로 막대 그래프 + Stacked Bar Graph 누적 가로 막대 그래프 + Line Graph 선 그래프 + Area Graph 영역 그래프 + Scatter Graph 분산형 그래프 + Pie Graph 원형 그래프 + Radar Graph 방사형 그래프 Graph Design 그래프 디자인 Graph Marker 그래프 마커 New Design 새 디자인 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index ed281382a4..468b22ec1b 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -3849,6 +3849,40 @@ theme Appearance Vormgeving Create Graph Diagram maken Graph Data Diagramgegevens Graph Type Diagramtype + Graph Options Diagramopties + Left Axis Linkeras + Right Axis Rechteras + Category Axis Categorie-as + Separate Scales Aparte schalen + Add Drop Shadow Slagschaduw toevoegen + Add Legend Across Top Legenda bovenaan toevoegen + First Row in Front Eerste rij vooraan + First Column in Front Eerste kolom vooraan + Legend Legenda + Column Width Kolombreedte + Bar Width Staafbreedte + Cluster Width Groepbreedte + Mark Data Points Gegevenspunten markeren + Connect Data Points Gegevenspunten verbinden + Edge-to-Edge Lines Lijnen van rand tot rand + Draw Filled Lines Gevulde lijnen tekenen + Tick Values Maatstreepwaarden + Override Calculated Values Berekende waarden negeren + Divisions Verdelingen + Tick Marks Maatstreepjes + Tick Marks per Division Maatstreepjes per verdeling + Draw Tick Marks Between Labels Maatstreepjes tussen labels tekenen + Add Labels Labels toevoegen + Maximum must be more than Minimum. Het maximum moet groter zijn dan het minimum. + Column Graph Kolomdiagram + Stacked Column Graph Gestapeld kolomdiagram + Bar Graph Staafdiagram + Stacked Bar Graph Gestapeld staafdiagram + Line Graph Lijndiagram + Area Graph Vlakdiagram + Scatter Graph Spreidingsdiagram + Pie Graph Cirkeldiagram + Radar Graph Radardiagram Vector Halftone Vectorhalftoon spot spot Text Tekst diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 08e96e6a35..421dc8329f 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3479,6 +3479,40 @@ theme Appearance Wygląd Create Graph Utwórz wykres Graph Data Dane wykresu Graph Type Typ wykresu + Graph Options Opcje wykresu + Left Axis Lewa oś + Right Axis Prawa oś + Category Axis Oś kategorii + Separate Scales Oddzielne skale + Add Drop Shadow Dodaj cień + Add Legend Across Top Dodaj legendę u góry + First Row in Front Pierwszy wiersz z przodu + First Column in Front Pierwsza kolumna z przodu + Legend Legenda + Column Width Szerokość kolumny + Bar Width Szerokość słupka + Cluster Width Szerokość grupy + Mark Data Points Oznacz punkty danych + Connect Data Points Połącz punkty danych + Edge-to-Edge Lines Linie od krawędzi do krawędzi + Draw Filled Lines Rysuj wypełnione linie + Tick Values Wartości podziałki + Override Calculated Values Zastąp obliczone wartości + Divisions Podziały + Tick Marks Znaczniki podziałki + Tick Marks per Division Znaczniki na podział + Draw Tick Marks Between Labels Rysuj znaczniki między etykietami + Add Labels Dodaj etykiety + Maximum must be more than Minimum. Maksimum musi być większe niż minimum. + Column Graph Wykres kolumnowy + Stacked Column Graph Skumulowany wykres kolumnowy + Bar Graph Wykres słupkowy + Stacked Bar Graph Skumulowany wykres słupkowy + Line Graph Wykres liniowy + Area Graph Wykres warstwowy + Scatter Graph Wykres punktowy + Pie Graph Wykres kołowy + Radar Graph Wykres radarowy Vector Halftone Półton wektorowy spot dodatkowy Text Tekst diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 79c4d9dc8e..9b537c9a19 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4943,6 +4943,40 @@ axis Both Ambos Gradient (. or Shift+.) Gradiente (. ou Shift+.) Graph Data Dados do Gráfico Graph Type Tipo de Gráfico + Graph Options Opções do Gráfico + Left Axis Eixo Esquerdo + Right Axis Eixo Direito + Category Axis Eixo de Categorias + Separate Scales Escalas Separadas + Add Drop Shadow Adicionar Sombra Projetada + Add Legend Across Top Adicionar Legenda na Parte Superior + First Row in Front Primeira Linha na Frente + First Column in Front Primeira Coluna na Frente + Legend Legenda + Column Width Largura da Coluna + Bar Width Largura da Barra + Cluster Width Largura do Grupo + Mark Data Points Marcar Pontos de Dados + Connect Data Points Conectar Pontos de Dados + Edge-to-Edge Lines Linhas de Borda a Borda + Draw Filled Lines Desenhar Linhas Preenchidas + Tick Values Valores das Marcas + Override Calculated Values Substituir Valores Calculados + Divisions Divisões + Tick Marks Marcas de Escala + Tick Marks per Division Marcas por Divisão + Draw Tick Marks Between Labels Desenhar Marcas entre Rótulos + Add Labels Adicionar Rótulos + Maximum must be more than Minimum. O máximo deve ser maior que o mínimo. + Column Graph Gráfico de Colunas + Stacked Column Graph Gráfico de Colunas Empilhadas + Bar Graph Gráfico de Barras + Stacked Bar Graph Gráfico de Barras Empilhadas + Line Graph Gráfico de Linhas + Area Graph Gráfico de Áreas + Scatter Graph Gráfico de Dispersão + Pie Graph Gráfico de Pizza + Radar Graph Gráfico de Radar New Design Novo Design Delete Design Excluir Design Paste Design Colar Design diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 9f608618e7..69c377b12d 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1423,6 +1423,40 @@ file-filter Templates Шаблоны Graph Диаграмма Graph Data Данные диаграммы Graph Type Тип диаграммы + Graph Options Параметры диаграммы + Left Axis Левая ось + Right Axis Правая ось + Category Axis Ось категорий + Separate Scales Раздельные шкалы + Add Drop Shadow Добавить тень + Add Legend Across Top Добавить легенду сверху + First Row in Front Первая строка спереди + First Column in Front Первый столбец спереди + Legend Легенда + Column Width Ширина столбца + Bar Width Ширина полосы + Cluster Width Ширина группы + Mark Data Points Отмечать точки данных + Connect Data Points Соединять точки данных + Edge-to-Edge Lines Линии от края до края + Draw Filled Lines Рисовать заполненные линии + Tick Values Значения делений + Override Calculated Values Заменить вычисленные значения + Divisions Деления + Tick Marks Метки делений + Tick Marks per Division Меток на деление + Draw Tick Marks Between Labels Рисовать метки между подписями + Add Labels Добавить подписи + Maximum must be more than Minimum. Максимум должен быть больше минимума. + Column Graph Столбчатая диаграмма + Stacked Column Graph Столбчатая диаграмма с накоплением + Bar Graph Линейчатая диаграмма + Stacked Bar Graph Линейчатая диаграмма с накоплением + Line Graph График + Area Graph Диаграмма с областями + Scatter Graph Точечная диаграмма + Pie Graph Круговая диаграмма + Radar Graph Лепестковая диаграмма Graph Design Дизайн диаграммы Graph Marker Маркер диаграммы New Design Новый дизайн diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 6156ca23d5..92837a92b6 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1372,6 +1372,40 @@ font-technology Variable Variabla Graph Diagram Graph Data Diagramdata Graph Type Diagramtyp + Graph Options Diagramalternativ + Left Axis Vänster axel + Right Axis Höger axel + Category Axis Kategoriaxel + Separate Scales Separata skalor + Add Drop Shadow Lägg till slagskugga + Add Legend Across Top Lägg till förklaring överst + First Row in Front Första raden längst fram + First Column in Front Första kolumnen längst fram + Legend Förklaring + Column Width Stapelbredd + Bar Width Liggande stapelbredd + Cluster Width Gruppbredd + Mark Data Points Markera datapunkter + Connect Data Points Förbind datapunkter + Edge-to-Edge Lines Linjer från kant till kant + Draw Filled Lines Rita fyllda linjer + Tick Values Skalstrecksvärden + Override Calculated Values Åsidosätt beräknade värden + Divisions Indelningar + Tick Marks Skalstreck + Tick Marks per Division Skalstreck per indelning + Draw Tick Marks Between Labels Rita skalstreck mellan etiketter + Add Labels Lägg till etiketter + Maximum must be more than Minimum. Maximum måste vara större än minimum. + Column Graph Stapeldiagram + Stacked Column Graph Staplat stapeldiagram + Bar Graph Liggande stapeldiagram + Stacked Bar Graph Staplat liggande stapeldiagram + Line Graph Linjediagram + Area Graph Ytdiagram + Scatter Graph Punktdiagram + Pie Graph Cirkeldiagram + Radar Graph Polärdiagram Graph Design Diagramdesign Graph Marker Diagrammarkör New Design Ny design diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 7120d44707..a3104e828f 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1346,6 +1346,40 @@ font-technology Variable Değişken Graph Grafik Graph Data Grafik Verileri Graph Type Grafik Türü + Graph Options Grafik Seçenekleri + Left Axis Sol Eksen + Right Axis Sağ Eksen + Category Axis Kategori Ekseni + Separate Scales Ayrı Ölçekler + Add Drop Shadow Alt Gölge Ekle + Add Legend Across Top Göstergeyi Üste Ekle + First Row in Front İlk Satır Önde + First Column in Front İlk Sütun Önde + Legend Gösterge + Column Width Sütun Genişliği + Bar Width Çubuk Genişliği + Cluster Width Küme Genişliği + Mark Data Points Veri Noktalarını İşaretle + Connect Data Points Veri Noktalarını Bağla + Edge-to-Edge Lines Kenardan Kenara Çizgiler + Draw Filled Lines Dolgulu Çizgiler Çiz + Tick Values Onay İşareti Değerleri + Override Calculated Values Hesaplanan Değerleri Geçersiz Kıl + Divisions Bölümler + Tick Marks Ölçek İşaretleri + Tick Marks per Division Bölüm Başına Ölçek İşareti + Draw Tick Marks Between Labels Etiketler Arasına Ölçek İşaretleri Çiz + Add Labels Etiket Ekle + Maximum must be more than Minimum. En çok, en azdan büyük olmalıdır. + Column Graph Sütun Grafik + Stacked Column Graph Yığılmış Sütun Grafik + Bar Graph Çubuk Grafik + Stacked Bar Graph Yığılmış Çubuk Grafik + Line Graph Çizgi Grafik + Area Graph Alan Grafik + Scatter Graph Dağılım Grafik + Pie Graph Pasta Grafik + Radar Graph Radar Grafik Graph Design Grafik Tasarımı Graph Marker Grafik İşaretleyici New Design Yeni Tasarım diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 84f296b0a6..521149139c 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1428,6 +1428,40 @@ file-filter Templates Шаблони Graph Діаграма Graph Data Дані діаграми Graph Type Тип діаграми + Graph Options Параметри діаграми + Left Axis Ліва вісь + Right Axis Права вісь + Category Axis Вісь категорій + Separate Scales Окремі шкали + Add Drop Shadow Додати тінь + Add Legend Across Top Додати легенду вгорі + First Row in Front Перший рядок попереду + First Column in Front Перший стовпець попереду + Legend Легенда + Column Width Ширина стовпця + Bar Width Ширина смуги + Cluster Width Ширина групи + Mark Data Points Позначати точки даних + Connect Data Points З'єднувати точки даних + Edge-to-Edge Lines Лінії від краю до краю + Draw Filled Lines Малювати заповнені лінії + Tick Values Значення поділок + Override Calculated Values Замінити обчислені значення + Divisions Поділки + Tick Marks Позначки поділок + Tick Marks per Division Позначок на поділку + Draw Tick Marks Between Labels Малювати позначки між підписами + Add Labels Додати підписи + Maximum must be more than Minimum. Максимум має бути більшим за мінімум. + Column Graph Стовпчикова діаграма + Stacked Column Graph Стовпчикова діаграма з накопиченням + Bar Graph Лінійчата діаграма + Stacked Bar Graph Лінійчата діаграма з накопиченням + Line Graph Графік + Area Graph Діаграма з областями + Scatter Graph Точкова діаграма + Pie Graph Кругова діаграма + Radar Graph Пелюсткова діаграма Graph Design Дизайн діаграми Graph Marker Маркер діаграми New Design Новий дизайн diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index e8b9e5758f..39409effc1 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1679,6 +1679,40 @@ file-filter Templates Mẫu Chop Design Cắt thiết kế Scale Design Co giãn thiết kế Graph Type Loại biểu đồ + Graph Options Tùy chọn biểu đồ + Left Axis Trục trái + Right Axis Trục phải + Category Axis Trục danh mục + Separate Scales Thang đo riêng + Add Drop Shadow Thêm đổ bóng + Add Legend Across Top Thêm chú giải ở trên cùng + First Row in Front Hàng đầu tiên ở trước + First Column in Front Cột đầu tiên ở trước + Legend Chú giải + Column Width Độ rộng cột + Bar Width Độ rộng thanh + Cluster Width Độ rộng nhóm + Mark Data Points Đánh dấu điểm dữ liệu + Connect Data Points Nối các điểm dữ liệu + Edge-to-Edge Lines Đường từ mép đến mép + Draw Filled Lines Vẽ đường tô đầy + Tick Values Giá trị vạch chia + Override Calculated Values Ghi đè giá trị đã tính + Divisions Số khoảng chia + Tick Marks Vạch chia + Tick Marks per Division Vạch chia mỗi khoảng + Draw Tick Marks Between Labels Vẽ vạch chia giữa các nhãn + Add Labels Thêm nhãn + Maximum must be more than Minimum. Giá trị tối đa phải lớn hơn giá trị tối thiểu. + Column Graph Biểu đồ cột + Stacked Column Graph Biểu đồ cột chồng + Bar Graph Biểu đồ thanh + Stacked Bar Graph Biểu đồ thanh chồng + Line Graph Biểu đồ đường + Area Graph Biểu đồ vùng + Scatter Graph Biểu đồ phân tán + Pie Graph Biểu đồ tròn + Radar Graph Biểu đồ radar Graphic Đồ họa Graphic Style Kiểu đồ họa Graphic Style Libraries Thư viện kiểu đồ họa diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 719968086f..29f2b424eb 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1364,6 +1364,40 @@ file-filter Templates 模板 Graph 图表 Graph Data 图表数据 Graph Type 图表类型 + Graph Options 图表选项 + Left Axis 左轴 + Right Axis 右轴 + Category Axis 类别轴 + Separate Scales 分开刻度 + Add Drop Shadow 添加投影 + Add Legend Across Top 在顶部添加图例 + First Row in Front 第一行在前 + First Column in Front 第一列在前 + Legend 图例 + Column Width 列宽 + Bar Width 条形宽度 + Cluster Width 簇宽度 + Mark Data Points 标记数据点 + Connect Data Points 连接数据点 + Edge-to-Edge Lines 边到边线条 + Draw Filled Lines 绘制填充线 + Tick Values 刻度值 + Override Calculated Values 覆盖计算值 + Divisions 刻度数 + Tick Marks 刻度线 + Tick Marks per Division 每个刻度的刻度线数 + Draw Tick Marks Between Labels 在标签之间绘制刻度线 + Add Labels 添加标签 + Maximum must be more than Minimum. 最大值必须大于最小值。 + Column Graph 柱形图 + Stacked Column Graph 堆积柱形图 + Bar Graph 条形图 + Stacked Bar Graph 堆积条形图 + Line Graph 折线图 + Area Graph 面积图 + Scatter Graph 散点图 + Pie Graph 饼图 + Radar Graph 雷达图 Graph Design 图表设计 Graph Marker 图表标记 New Design 新建设计 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 121a8b8c02..018e2ff788 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1399,6 +1399,40 @@ file-filter Templates 範本 Graph 圖表 Graph Data 圖表資料 Graph Type 圖表類型 + Graph Options 圖表選項 + Left Axis 左軸 + Right Axis 右軸 + Category Axis 類別軸 + Separate Scales 分開刻度 + Add Drop Shadow 加入投影 + Add Legend Across Top 在頂端加入圖例 + First Row in Front 第一列在前 + First Column in Front 第一欄在前 + Legend 圖例 + Column Width 直條寬度 + Bar Width 橫條寬度 + Cluster Width 群組寬度 + Mark Data Points 標示資料點 + Connect Data Points 連接資料點 + Edge-to-Edge Lines 邊到邊線條 + Draw Filled Lines 繪製填色線條 + Tick Values 刻度值 + Override Calculated Values 覆寫計算值 + Divisions 刻度數 + Tick Marks 刻度記號 + Tick Marks per Division 每個刻度的記號數 + Draw Tick Marks Between Labels 在標籤之間繪製刻度記號 + Add Labels 加入標籤 + Maximum must be more than Minimum. 最大值必須大於最小值。 + Column Graph 直條圖 + Stacked Column Graph 堆疊直條圖 + Bar Graph 橫條圖 + Stacked Bar Graph 堆疊橫條圖 + Line Graph 折線圖 + Area Graph 區域圖 + Scatter Graph 散佈圖 + Pie Graph 圓形圖 + Radar Graph 雷達圖 Graph Design 圖表設計 Graph Marker 圖表標記 New Design 新增設計 diff --git a/crates/ui-egui/src/menus/invoke.rs b/crates/ui-egui/src/menus/invoke.rs index 3aab5e1e44..3f89c73020 100644 --- a/crates/ui-egui/src/menus/invoke.rs +++ b/crates/ui-egui/src/menus/invoke.rs @@ -131,9 +131,7 @@ pub(crate) fn graph_dialog(app: &mut VectorcraftApp, id: &str) -> Result Date: Sat, 14 Nov 2026 10:48:08 -0800 Subject: [PATCH 06/28] Graphs: a value axis side without its own scale shows the scale drawn on it With Separate Scales off, or with no series on a side, the axis page shows the tick values of the scale the graph draws on that side instead of the bounds kept for it, and its Override Calculated Values is dimmed. --- crates/ui-egui/src/dialogs/graph_type.rs | 41 ++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/dialogs/graph_type.rs b/crates/ui-egui/src/dialogs/graph_type.rs index 14bc371e74..7563a1f42c 100644 --- a/crates/ui-egui/src/dialogs/graph_type.rs +++ b/crates/ui-egui/src/dialogs/graph_type.rs @@ -656,9 +656,22 @@ fn value_page(ui: &mut egui::Ui, d: &mut Dialog, right: bool) { // Without Separate Scales the right axis shows the left axis' tick values, and with it a side // without series shows the other side's. let own_scale = scale_used(d, right); - check(ui, d, scale.overridden, tl!("Override Calculated Values"), own_scale); + // A side without a scale of its own shows the tick values of the scale drawn on it. + let drawn = if own_scale { + scale + } else if right { + &LEFT + } else { + &RIGHT + }; + check(ui, d, drawn.overridden, tl!("Override Calculated Values"), own_scale); ui.add_enabled_ui(own_scale && d.bool(scale.overridden), |ui| { - let ((lo, hi), count) = (bounds(d, scale), divisions(d, scale)); + let ((lo, hi), count) = if own_scale || d.bool(drawn.overridden) { + (bounds(d, drawn), divisions(d, drawn)) + } else { + let (lo, hi, count) = calculated(d, drawn); + ((lo, hi), count) + }; for (key, label, shown) in [(scale.min, tl!("Minimum:"), lo), (scale.max, tl!("Maximum:"), hi)] { widgets::label_row(ui, label, LABEL_W, |ui| { if let Some(v) = widgets::plain_field(ui, ("graph-type", key), shown, "", 4, NUMBER_W) { @@ -1230,6 +1243,30 @@ mod tests { } } + /// A value axis side without a scale of its own shows the tick values of the scale drawn on it, + /// not bounds kept for it. + #[test] + fn a_side_without_its_own_scale_shows_the_scale_drawn_on_it() { + let mut app = app_with_graph("column"); + app.run(COMMAND, json!({"valueAxis": "both", "rightAxisMin": 100, "rightAxisMax": 200})).unwrap(); + open(&mut app).unwrap(); + assert!(app.ui.dialog.as_ref().unwrap().bool("rightOverrideValues")); + set(&mut app, PAGE, json!("rightAxis")); + let mut h = harness(app); + let shown = |h: &Harness, name: &str| { + let node = h.get_by_role_and_label(Role::TextInput, name); + node.accesskit_node().value().as_deref().and_then(|v| v.trim().parse::().ok()) + }; + assert_eq!((shown(&h, "Minimum:"), shown(&h, "Maximum:")), (Some(0.0), Some(4.0)), "the left scale, calculated"); + let overridden = h.get_by_role_and_label(Role::CheckBox, "Override Calculated Values"); + assert!(overridden.accesskit_node().is_disabled()); + // With Separate Scales and a series on the right axis, the side shows its own bounds. + set(h.state_mut(), "separateScales", json!(true)); + set(h.state_mut(), RIGHT_SERIES, json!([1])); + h.run_steps(3); + assert_eq!((shown(&h, "Minimum:"), shown(&h, "Maximum:")), (Some(100.0), Some(200.0))); + } + /// OK changes the graph and the series the dialog opened on, even when the selection changed /// while it was open. #[test] From 5ab24e23bf4c85da69f27347e7f2db62d4f56ef4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 10:56:48 -0800 Subject: [PATCH 07/28] Graphs: an axis value that is not a number reaches graph.setType from Graph Type Override Calculated Values fills only the axis values that are missing, so a value that is not a number is sent as it is and the command refuses it instead of the dialog putting a calculated value in its place. The right axis page without a scale of its own is described in docs/mcp.md. --- crates/ui-egui/src/dialogs/graph_type.rs | 21 +++++++++++++++++++-- docs/mcp.md | 6 ++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/dialogs/graph_type.rs b/crates/ui-egui/src/dialogs/graph_type.rs index 7563a1f42c..107b98357f 100644 --- a/crates/ui-egui/src/dialogs/graph_type.rs +++ b/crates/ui-egui/src/dialogs/graph_type.rs @@ -412,12 +412,14 @@ fn settle(d: &mut Dialog) { continue; } let ((lo, hi), count) = (bounds(d, axis), divisions(d, axis)); + // A value that is not a number stays, and `graph.setType` refuses it. + let missing = |d: &Dialog, key: &str| d.fields.get(key).is_none_or(Value::is_null); for (key, value) in [(axis.min, json!(lo)), (axis.max, json!(hi))] { - if number(d, key).is_none() { + if missing(d, key) { d.fields.insert(key.into(), value); } } - if number(d, axis.ticks).is_none_or(|n| n < 1.0) { + if missing(d, axis.ticks) || number(d, axis.ticks).is_some_and(|n| n < 1.0) { d.fields.insert(axis.ticks.into(), json!(count)); } } @@ -1148,6 +1150,21 @@ mod tests { assert_eq!(problem(app.ui.dialog.as_ref().unwrap()), None); set(&mut app, RIGHT_SERIES, json!([1])); assert!(problem(app.ui.dialog.as_ref().unwrap()).is_some()); + // A value that is not a number is not replaced by a calculated one: the command refuses it. + open(&mut app).unwrap(); + set(&mut app, "axisMax", json!("ten")); + settle(app.ui.dialog.as_mut().unwrap()); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["axisMax"], "ten"); + let before = steps(&app); + assert!(crate::dialogs::confirm(&mut app).is_err()); + assert_eq!((steps(&app), query(&mut app)["axisMax"].as_f64()), (before, Some(20.0))); + open(&mut app).unwrap(); + for (key, value) in [("rightOverrideValues", json!(true)), ("rightAxisMin", json!(5)), ("rightAxisMax", json!(1))] { + set(&mut app, key, value); + } + set(&mut app, "valueAxis", json!("both")); + set(&mut app, "separateScales", json!(true)); + set(&mut app, RIGHT_SERIES, json!([1])); // A pie graph has no value axis to refuse. set(&mut app, "type", json!("pie")); assert_eq!(problem(app.ui.dialog.as_ref().unwrap()), None); diff --git a/docs/mcp.md b/docs/mcp.md index f241010b04..f9c901f2da 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -10144,8 +10144,10 @@ the whole graph, as the command applies them; `type` or `valueAxis` is `null` wh OK then leaves each series as it is. OK runs `graph.setType` with the fields: `legend` for every type but pie, and the three pie options for a pie graph. When the selection changed while the dialog was open, it also sends `id`, and `seriesIndexes` while series are picked and a `type` or `valueAxis` goes to them, so it changes the graph and the series -the dialog opened on. With Separate Scales, a value axis side that no series is on draws the other side's scale, and -its Override Calculated Values is dimmed. With nothing changed it closes without a step of history. An overridden value +the dialog opened on. A value axis side without a scale of its own (the right side without Separate Scales, or with +Separate Scales a side that no series is on) shows the tick values of the scale drawn on it, and its Override Calculated +Values is dimmed. A value of `axisMin`, `axisMax` or `ticks` (or of the right axis' three) that is not a number is sent +as it is, and `graph.setType` refuses it. With nothing changed it closes without a step of history. An overridden value axis whose maximum is not more than its minimum is the error "Maximum must be more than Minimum." and the dialog stays open. The types the dialog dims for picked series (those that cannot go in the graph), the options it dims for a type and the pages it lists are VectorCraft's own rules. From af64492f1c660cc8c1ab935878f19b87a9ed13e2 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Sun, 15 Nov 2026 01:41:41 +0530 Subject: [PATCH 08/28] Graphic Styles: built-in Neon & Glow graphic style library --- ROADMAP.md | 3 +- crates/doc/src/style_libs.rs | 71 +++++++++++++++++++++++++++++ crates/engine/src/tests/stylelib.rs | 54 ++++++++++++++++++++++ docs/gaps.md | 14 +++--- docs/target-app-parity.md | 7 +-- 5 files changed, 139 insertions(+), 10 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index dabed3eb94..177e71c446 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: Raster effects 85% → 88%, gallery thumbnails and in-dialog preview in; headline numbers unchanged; Symbols area ~87% → ~88%: dynamic instance parts take opacity, blend, gradient and pattern on the canvas; G5 docs sync for Overlap and Pattern auto corners; previously minor: Tools stays beside the canvas with left-side panels; Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (G5: built-in Neon & Glow graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); G5 estimate decremented (4–14 → 3–13 h); previously: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: Raster effects 85% → 88%, gallery thumbnails and in-dialog preview in; headline numbers unchanged; Symbols area ~87% → ~88%: dynamic instance parts take opacity, blend, gradient and pattern on the canvas; G5 docs sync for Overlap and Pattern auto corners; previously minor: Tools stays beside the canvas with left-side panels; Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON @@ -223,6 +223,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | Date | Change | Summary | |---|---|---| +| 2026-11-15 | minor | Graphic Styles: built-in Neon & Glow graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); G5 estimate decremented (4–14 → 3–13 h). | | 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. | | 2026-11-10 | minor | Tidy-up, no content change: the unheaded change bullets at the top of the file and a revision row inside the headline-numbers table move to this table under their commit dates. Scores and estimates unchanged. | | 2026-11-10 | minor | Raster effects: Effect Gallery thumbnails and an in-dialog preview (an original drawing through each filter). Features-table Raster effects 85% → 88%, hours 1–2 → none. Headline numbers unchanged, so the progress log is unchanged. | diff --git a/crates/doc/src/style_libs.rs b/crates/doc/src/style_libs.rs index 1952060663..34faf1f9c4 100644 --- a/crates/doc/src/style_libs.rs +++ b/crates/doc/src/style_libs.rs @@ -162,6 +162,7 @@ pub const STYLE_LIBRARIES: &[BuiltinStyleLibrary] = &[ BuiltinStyleLibrary { id: "blends-transparency", name: "Blends and Transparency", make: blends_transparency }, BuiltinStyleLibrary { id: "game-hud", name: "Game HUD", make: game_hud }, BuiltinStyleLibrary { id: "status-effects", name: "Status Effects", make: status_effects }, + BuiltinStyleLibrary { id: "neon-glow", name: "Neon & Glow", make: neon_glow }, ]; /// Built-in library `id`, made once. @@ -619,6 +620,44 @@ fn status_effects() -> Vec { ] } +/// Neon & Glow: original multi-layer procedural styles with neon tubes, laser beams, and glowing halos. +fn neon_glow() -> Vec { + vec![ + style( + "Cyan Neon", + vec![S(StrokeLayer { opacity: 0.7, ..round("#00e5ff", 6.0) }), S(round("#ffffff", 1.5))], + vec![fx("stylize.dropShadow", json!({"mode": "screen", "opacity": 85.0, "x": 0.0, "y": 0.0, "blur": 8.0, "color": "#00e5ff"}))], + ), + GraphicStyle { + blend: BlendMode::Screen, + ..style( + "Magenta Laser", + vec![S(StrokeLayer { blend: BlendMode::Screen, ..round("#ff007f", 5.0) }), S(round("#ffffff", 1.5))], + vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 90.0, "blur": 8.0, "color": "#ff007f"}))], + ) + }, + style( + "Amber Tube", + vec![S(StrokeLayer { opacity: 0.8, ..round("#ff8c00", 6.0) }), S(round("#ffea00", 1.5))], + vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 85.0, "blur": 8.0, "color": "#ff8c00"}))], + ), + style( + "Electric Lime", + vec![S(StrokeLayer { opacity: 0.75, ..round("#39ff14", 6.0) }), S(round("#ffffff", 1.5))], + vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 90.0, "blur": 8.0, "color": "#39ff14"}))], + ), + style( + "Pulse Violet", + vec![S(StrokeLayer { opacity: 0.75, ..round("#7b2cbf", 5.0) }), S(round("#e0aaff", 1.5))], + vec![ + fx("stylize.innerGlow", json!({"mode": "screen", "opacity": 75.0, "blur": 6.0, "color": "#c77dff", "source": "edge"})), + fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 85.0, "blur": 8.0, "color": "#7b2cbf"})), + ], + ), + style("Cobalt Phosphor", vec![S(round("#0047ab", 6.0)), S(round("#ffffff", 1.5))], vec![fx("blur.gaussian", json!({"radius": 2.0}))]), + ] +} + #[cfg(test)] mod tests { use super::*; @@ -750,4 +789,36 @@ mod tests { let Paint::Gradient(gp) = g.appearance.fill_paint() else { panic!("a gradient") }; assert!(gp.swatch.is_none() && gp.gradient.stops.iter().all(|s| s.swatch.is_none() && s.tint == 1.0)); } + + #[test] + fn neon_and_glow_styles_are_defined_with_multi_strokes_and_effects() { + let lib = builtin_style_library("neon-glow").expect("neon-glow library exists"); + assert_eq!(lib.name, "Neon & Glow"); + assert_eq!(lib.len(), 6); + let names: Vec<&str> = lib.styles.iter().map(|g| g.name.as_str()).collect(); + assert_eq!(names, ["Cyan Neon", "Magenta Laser", "Amber Tube", "Electric Lime", "Pulse Violet", "Cobalt Phosphor"]); + for g in &lib.styles { + let strokes = g.appearance.items.iter().filter(|i| matches!(i, AppearanceItem::Stroke(_))).count(); + assert!(strokes >= 2, "{} has {} strokes, expected >= 2", g.name, strokes); + assert!(!g.appearance.effects.is_empty(), "{} has effects", g.name); + } + let cyan = lib.style("Cyan Neon").unwrap(); + assert_eq!(cyan.appearance.effects[0].id, "stylize.dropShadow"); + let magenta = lib.style("Magenta Laser").unwrap(); + assert_eq!(magenta.blend, BlendMode::Screen); + let pulse = lib.style("Pulse Violet").unwrap(); + let pulse_fx: Vec<&str> = pulse.appearance.effects.iter().map(|e| e.id.as_str()).collect(); + assert_eq!(pulse_fx, ["stylize.innerGlow", "stylize.outerGlow"]); + let cobalt = lib.style("Cobalt Phosphor").unwrap(); + assert_eq!(cobalt.appearance.effects[0].id, "blur.gaussian"); + } + + #[test] + fn neon_glow_round_trips_through_vcstyles() { + let lib = builtin_style_library("neon-glow").unwrap(); + let text = write(&lib); + assert!(sniff(&text)); + let read_back = read(&text, "Fallback").unwrap(); + assert_eq!(*lib, read_back); + } } diff --git a/crates/engine/src/tests/stylelib.rs b/crates/engine/src/tests/stylelib.rs index fd8f2c7e24..e8623900fd 100644 --- a/crates/engine/src/tests/stylelib.rs +++ b/crates/engine/src/tests/stylelib.rs @@ -45,6 +45,7 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects() assert!(libs.iter().any(|l| l["id"] == "game-hud" && l["name"] == "Game HUD"), "{libs:?}"); assert!(libs.iter().any(|l| l["id"] == "status-effects" && l["name"] == "Status Effects"), "{libs:?}"); assert!(libs.iter().any(|l| l["id"] == "textures" && l["name"] == "Textures" && l["count"] == 8), "{libs:?}"); + assert!(libs.iter().any(|l| l["id"] == "neon-glow" && l["name"] == "Neon & Glow" && l["count"] == 6), "{libs:?}"); for l in libs { let id = l["id"].as_str().unwrap(); let g = run(&mut s, "graphicStyle.library", json!({"library": id})); @@ -69,6 +70,9 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects() let texture_names: Vec<&str> = textures["styles"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect(); let expected = ["Fine Grain", "Canvas Weave", "Burlap Sack", "Cracked Glaze", "Mosaic Floor", "Quilted Squares", "Leaded Glass", "Damp Paper"]; assert_eq!(texture_names, expected); + let neon = run(&mut s, "graphicStyle.library", json!({"library": "neon-glow"})); + let neon_names: Vec<&str> = neon["styles"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect(); + assert_eq!(neon_names, ["Cyan Neon", "Magenta Laser", "Amber Tube", "Electric Lime", "Pulse Violet", "Cobalt Phosphor"]); let id = rect(&mut s, 10.0, 20.0, 40.0, 30.0); let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "game-hud", "name": "Mana Glow", "apply": true, "ids": [id.0]})); assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec!["Mana Glow"], Some("Mana Glow"))); @@ -419,3 +423,53 @@ fn every_builtin_style_paints_visibly_on_a_rectangle() { } } } + +#[test] +fn neon_and_glow_library_adds_and_applies_multi_stroke_styles() { + let mut s = session(); + let list = run(&mut s, "graphicStyle.libraries", json!({})); + let libs = list["libraries"].as_array().unwrap(); + let at = libs.iter().position(|l| l["id"] == "neon-glow").expect("neon-glow listed"); + assert_eq!(libs[at]["name"], "Neon & Glow"); + assert_eq!(libs[at]["count"], 6); + + let (_, lib) = cmd::stylelib::library(&s, "neon-glow").unwrap(); + assert_eq!(lib.len(), 6); + + let want = [ + ("Cyan Neon", 2, "stylize.dropShadow"), + ("Magenta Laser", 2, "stylize.outerGlow"), + ("Amber Tube", 2, "stylize.outerGlow"), + ("Electric Lime", 2, "stylize.outerGlow"), + ("Pulse Violet", 2, "stylize.innerGlow"), + ("Cobalt Phosphor", 2, "blur.gaussian"), + ]; + + for (i, (name, stroke_count, first_effect)) in want.iter().enumerate() { + let id = rect(&mut s, 10.0 + 30.0 * i as f64, 15.0, 25.0, 25.0); + let before_undo = undo_len(&s); + let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "neon-glow", "name": name, "apply": true, "ids": [id.0]})); + assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec![*name], Some(*name))); + assert_eq!(undo_len(&s), before_undo + 1, "{name}"); + + let node = doc(&s).node(id).unwrap(); + let strokes = node.appearance.items.iter().filter(|it| matches!(it, vectorcraft_doc::AppearanceItem::Stroke(_))).count(); + assert_eq!(strokes, *stroke_count, "{name}"); + assert_eq!(node.appearance.effects[0].id, *first_effect, "{name}"); + } + + // Adding existing style dedupes and creates no new undo step. + let before = (undo_len(&s), doc(&s).graphic_styles.len()); + let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "neon-glow", "name": "Cyan Neon"})); + assert_eq!((names(&r["added"]), names(&r["existing"])), (vec![], vec!["Cyan Neon"])); + assert_eq!((undo_len(&s), doc(&s).graphic_styles.len()), before); + + // Round-trip through serialize / open + let b64 = run(&mut s, "document.serialize", json!({}))["dataBase64"].as_str().unwrap().to_string(); + run(&mut s, "document.open", json!({"name": "neon.vectorcraft", "dataBase64": b64})); + for (name, _, _) in want { + let d = doc(&s); + let st = d.graphic_style(name).unwrap(); + assert!(st.same_look(lib.style(name).unwrap()), "{name}"); + } +} diff --git a/docs/gaps.md b/docs/gaps.md index 36dd83cd3c..7bf6fd2cfc 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (G5: built-in Weather Icons symbol library (Sun, Moon, Cloud, Rain, Thunder, Snow); estimate 5–15 → 4–14 h; previously: 3D: the seams of white-light Extrude & Bevel, Inflate and Rotate, and the face seams of the 3D (Classic) effects, have round joins instead of miters; previously: PDF import: preserve Type 3 font text runs as named groups , or as live text in TextAs::Text mode if ToUnicode exists on non-colour fonts (TextAs::Outlines produces grouped outlines), #6740; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: oversized editing artboard import; Glyphs panel: Alternates for Current Selection applies an OpenType feature value (`salt=2`) to the selected letter alone, so an Arabic word stays joined, and Entire Font lists the unencoded glyphs, #5514; #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: G5: built-in Energy Beams brush library; estimate 5–15 → 4–14 h; previously minor: G5: built-in Skill Icons symbol library; estimate 6–16 → 5–15 h; previously minor: ruler double-click adds a guide at the pointer, #5239; G5: built-in Status Effects graphic style library; estimate 7–17 → 6–16 h; MCP docs catch-up for UI Frames and Map Markers; previously minor: G5: built-in Inventory Icons symbol library; estimate 8–18 → 7–17 h; previously minor: G9: `.ai` save names a variable font instance's design-space setting, #6388; previously minor: G5: built-in Game Trails brush library; estimate 9–19 → 8–18 h; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (G5: built-in Neon & Glow graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); estimate 4–14 → 3–13 h; previously: G5: built-in Weather Icons symbol library (Sun, Moon, Cloud, Rain, Thunder, Snow); estimate 5–15 → 4–14 h; previously: 3D: the seams of white-light Extrude & Bevel, Inflate and Rotate, and the face seams of the 3D (Classic) effects, have round joins instead of miters; previously: PDF import: preserve Type 3 font text runs as named groups , or as live text in TextAs::Text mode if ToUnicode exists on non-colour fonts (TextAs::Outlines produces grouped outlines), #6740; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: oversized editing artboard import; Glyphs panel: Alternates for Current Selection applies an OpenType feature value (`salt=2`) to the selected letter alone, so an Arabic word stays joined, and Entire Font lists the unencoded glyphs, #5514; #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: G5: built-in Energy Beams brush library; estimate 5–15 → 4–14 h; previously minor: G5: built-in Skill Icons symbol library; estimate 6–16 → 5–15 h; previously minor: ruler double-click adds a guide at the pointer, #5239; G5: built-in Status Effects graphic style library; estimate 7–17 → 6–16 h; MCP docs catch-up for UI Frames and Map Markers; previously minor: G5: built-in Inventory Icons symbol library; estimate 8–18 → 7–17 h; previously minor: G9: `.ai` save names a variable font instance's design-space setting, #6388; previously minor: G5: built-in Game Trails brush library; estimate 9–19 → 8–18 h; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -34,6 +34,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-15 | minor | G5: Neon & Glow joins Shadows and Glows, Outlines and Rules, Hand-Drawn, Textures, Painted and Sketched, Gradient Finishes, Shape Effects, Blends and Transparency, Game HUD and Status Effects as an eleventh built-in graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor; original procedural styles generated in code); estimate decremented (4–14 → 3–13 h). | | 2026-11-14 | minor | G12: Symbols panel context menu: right-click on a symbol in thumbnail or list view displays a context menu (Place Symbol Instance, Edit Symbol, Duplicate Symbol, Delete Symbol, Break Link to Symbol, Select All Instances), selecting the symbol first if unselected; scores and estimates unchanged. | | 2026-11-14 | minor | G5: Weather Icons joins Basic Shapes, UI Frames, Map Markers, Inventory Icons and Skill Icons as a sixth built-in symbol library (Sun, Moon, Cloud, Rain, Thunder, Snow; original procedural vector symbols generated in code, no default 9-slice); estimate decremented (5–15 → 4–14 h). | | 2026-11-14 | minor | G12: Layers panel row context menu (Options…, Duplicate, Delete, Lock Others/Show All/Hide Others/Unlock All, Locate Object, Collect for Export) with selection handling and undo tracking. Scores and estimates unchanged. | @@ -95,7 +96,7 @@ stage ([ROADMAP.md](../ROADMAP.md)). | 8 | [G2 Photoshop-style raster effects and the Effect Gallery](#g2-photoshop-style-raster-effects-and-the-effect-gallery) | 0 | no | [effects-parity.md](effects-parity.md) | | 9 | [G13 Illustrator 2026 (30.x) additions](#g13-illustrator-2026-30x-additions) | 13–23 | no | [target-app-parity.md](target-app-parity.md) | | 10 | [G14 Pen hardware beyond Windows pressure](#g14-pen-hardware-beyond-windows-pressure) | 4–10 | Wayland pressure | [hardware-parity.md](hardware-parity.md) | -| 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 4–14 | no | [target-app-parity.md](target-app-parity.md) | +| 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 3–13 | no | [target-app-parity.md](target-app-parity.md) | | 12 | [G3 3D and Materials](#g3-3d-and-materials) | 25–45 | no | [effects-parity.md](effects-parity.md) | | 13 | [G7 Views and windows](#g7-views-and-windows) | 15–25 | no | [ui-parity.md](ui-parity.md) | | 14 | [G6 Automation and scripting](#g6-automation-and-scripting) | 4–8 | no | [target-app-parity.md](target-app-parity.md) | @@ -671,17 +672,18 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi Direct Selection of a path, compound path or type inside a dynamic instance also takes opacity and blend (`transparency.set`) and a gradient or pattern fill or stroke (`paint.setFill` / `paint.setStroke`, Eyedropper), for that instance only; - graphic style libraries (Window › Graphic Style Libraries; ten original libraries generated in code, including + graphic style libraries (Window › Graphic Style Libraries; eleven original libraries generated in code, including Game HUD with Health Fill, Mana Glow, Damage Flash, Quest Outline, Coin Shine and Boss Alert, Status Effects - with Poison Tint, Freeze Edge, Burn Aura, Stun Pulse, Stealth Fade and Shield Buff, Painted and Sketched with + with Poison Tint, Freeze Edge, Burn Aura, Stun Pulse, Stealth Fade and Shield Buff, Neon & Glow with Cyan Neon, + Magenta Laser, Amber Tube, Electric Lime, Pulse Violet and Cobalt Phosphor, Painted and Sketched with Washed Blue, Bristle Ochre, Pastel Heather, Charcoal Shade, Ink Contour, Wet Ink and Fine Pen, and Textures with Fine Grain, Canvas Weave, Burlap Sack, Cracked Glaze, Mosaic Floor, Quilted Squares, Leaded Glass and Damp Paper, painted with the texture effects; User Defined; Other Library…; `graphicStyle.libraries`, `graphicStyle.addFromLibrary`). -- **Missing:** more built-in brush libraries beyond the eight shipped (and further graphic-style libraries beyond the ten shipped, and further symbol libraries beyond Basic +- **Missing:** more built-in brush libraries beyond the eight shipped (and further graphic-style libraries beyond the eleven shipped, and further symbol libraries beyond Basic Shapes, UI Frames, Map Markers, Inventory Icons, Skill Icons and Weather Icons; original, generated in code, never Adobe's); dynamic symbols' `.ai` read and write; Global Edit of rotated copies as matches. -- **Estimate:** 4–14 h. +- **Estimate:** 3–13 h. ## G3 3D and Materials diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c6375f3aa3..043e962d56 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (Pencil continuation, straight-line and closing cursors; scores and estimates unchanged; previously: G5: Weather Icons joins built-in symbol libraries (Sun, Moon, Cloud, Rain, Thunder, Snow); scores and estimates unchanged; previously: a closed Pencil stroke closes with a straight segment; previously: Snap to Point with Smart Guides off draws no Smart Guides labels or lines, #7250; previously: 3D: the seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins, so white-light 3D output is expected to differ from the 2026-10-17 comparison at facet corners (not measured: the scenes were not exported again); previously: `.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; Character panel: the Set Font Height Reference dropdown shows an icon before each reference, #6304; scores and estimates unchanged; previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter, so a top object saved with its sides' curves in pieces draws as its `.ai` drawn art, #7097; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously minor: Object › Rasterize with the Grayscale or Bitmap colour model makes a grayscale image, #6969; scores and estimates unchanged; previously minor: `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, miter tips and moved strokes included, after its glyphs grown by half its strokes' weight (#6824; #6617: Swatches panel menu: Show Recent Colors; scores unchanged; previously minor: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Type tool: kerning at a caret at the start of a line moves the character after it, #5111; scores and estimates unchanged; previously minor: Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (G5: Neon & Glow joins built-in graphic style libraries (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); scores and estimates unchanged; previously: Pencil continuation, straight-line and closing cursors; scores and estimates unchanged; previously: G5: Weather Icons joins built-in symbol libraries (Sun, Moon, Cloud, Rain, Thunder, Snow); scores and estimates unchanged; previously: a closed Pencil stroke closes with a straight segment; previously: Snap to Point with Smart Guides off draws no Smart Guides labels or lines, #7250; previously: 3D: the seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins, so white-light 3D output is expected to differ from the 2026-10-17 comparison at facet corners (not measured: the scenes were not exported again); previously: `.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; Character panel: the Set Font Height Reference dropdown shows an icon before each reference, #6304; scores and estimates unchanged; previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter, so a top object saved with its sides' curves in pieces draws as its `.ai` drawn art, #7097; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously minor: Object › Rasterize with the Grayscale or Bitmap colour model makes a grayscale image, #6969; scores and estimates unchanged; previously minor: `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, miter tips and moved strokes included, after its glyphs grown by half its strokes' weight (#6824; #6617: Swatches panel menu: Show Recent Colors; scores unchanged; previously minor: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Type tool: kerning at a caret at the start of a line moves the character after it, #5111; scores and estimates unchanged; previously minor: Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865). > @@ -58,6 +58,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-15 | minor | G5: Neon & Glow joins Shadows and Glows, Outlines and Rules, Hand-Drawn, Textures, Painted and Sketched, Gradient Finishes, Shape Effects, Blends and Transparency, Game HUD and Status Effects as an eleventh built-in graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor; original procedural styles generated in code); area score and estimate unchanged. | | 2026-11-14 | minor | G5: Weather Icons joins Basic Shapes, UI Frames, Map Markers, Inventory Icons and Skill Icons as a sixth built-in symbol library (Sun, Moon, Cloud, Rain, Thunder, Snow; original procedural vector symbols generated in code, no default 9-slice); area score and estimate unchanged. | | 2026-11-14 | minor | Pencil and Paintbrush: a closed stroke (Alt at the release, or ending within Close Paths When Ends Are Within of its start) is fitted as the open stroke would be and closed by a straight segment back to its start, instead of curving on past both ends (`pencil_alt_closes_a_stroke_straight_back_to_its_start` in `crates/engine/src/tests/draw2.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Pencil cursors: the continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears another selected path's open end it connects to; the straight-line cursor is a short level line; a continued path, a straight line too, shows the closing cursor and closes within Close Paths When Ends Are Within of its other end, and with Alt at the release (`a_continued_stroke_shows_where_it_connects_or_closes` in `crates/tools/src/draw2/gesture/tests.rs`, `pencil_extend_closes_as_closed_says` in `crates/engine/src/tests/draw2.rs`). Scores and estimates unchanged. | @@ -559,7 +560,7 @@ since 2026-10-01. | **Interaction fidelity:** modifiers, cursors, small behaviours | ~30–40% | The dedicated pass hasn't started, and there has been no side-by-side session with Illustrator yet. A power user notices this first | | **Look & feel** | ~75–80% | Measured against Illustrator 2026 screenshots (2026-10-02); the panels added since haven't been re-measured | | **File interop** | ~85% | SVG/SVGZ, PDF and PDF-compatible `.ai` (and legacy version 7 `.ai` out, keeping layers, groups and type as text), EPS, DXF, EMF/WMF, raster formats, PSD export and PSD/PSB placement (merged image), Place and Links, Package, Print, clipboard flavours. Affinity documents (Affinity 1–3) open natively as editable art, checked against 51 pinned public files (33 current `.af`), with selected saved-thumbnail comparisons and every-board native/SVG/PDF/PSD checks for current files; Affinity export is missing and effects, adjustments and brushes don't import. Affinity isn't an Illustrator feature, so this doesn't raise the parity score. The editing copy inside an Illustrator EPS or `.ai` is read for its structure: layers, groups, names, hidden objects and type, colours, gradients, transparency, images and every artboard (see Out of scope for what it doesn't read yet); DWG has no open spec | -| **Bundled content:** brush, symbol, style and swatch libraries | ~46% | Ours are original and generated in code, and far fewer than Illustrator ships; symbol libraries have six (Basic Shapes, UI Frames with default 9-slice, Map Markers, Inventory Icons, Skill Icons, Weather Icons), graphic style libraries eight (including Game HUD and Status Effects) and brush libraries nine (Art Ribbons, Ink and Wash, Calligraphic Nibs, Bristle Brushes, Pattern Borders, Scatter Shapes, Game Trails, Energy Beams, Dry Media) beside User Defined and Other Library… | +| **Bundled content:** brush, symbol, style and swatch libraries | ~46% | Ours are original and generated in code, and far fewer than Illustrator ships; symbol libraries have six (Basic Shapes, UI Frames with default 9-slice, Map Markers, Inventory Icons, Skill Icons, Weather Icons), graphic style libraries eleven (including Game HUD, Status Effects and Neon & Glow) and brush libraries nine (Art Ribbons, Ink and Wash, Calligraphic Nibs, Bristle Brushes, Pattern Borders, Scatter Shapes, Game Trails, Energy Beams, Dry Media) beside User Defined and Other Library… | | **Performance** | unverified | Multithreaded, off-thread rendering and caches are in place. The last budget run (2026-10-01, loaded machine) measured 290 ms for a 50k-path fit against a 16 ms budget. Re-run `vectorcraft-cli perf` on an idle machine | | **Robustness** | good, new | ~2,840 tests, property tests, no panics in shipped code (lints, the `guard` safety net, import fuzzing), Data Recovery. Missing: a corpus of real-world files, Windows/Linux/browser QA | | **Platforms and 1.0 polish** | ~60% | The macOS app and the web build work, and Finder opens every readable format with the macOS app (#295, #354); on macOS the menus are in the system menu bar, laid out as a Mac app's, with the in-window menus as a fallback (`--in-window-menus`; verified on Windows and by cross-compilation, not yet by hand on a Mac); hybrid-graphics laptops render on the power-saving GPU by default on Windows and macOS (#306), and Windows starts on Direct3D 12, falling back to OpenGL, and loads no Vulkan driver unless `WGPU_BACKEND` asks for it (#545: Vulkan flickered black on an Intel GPU, DX12 and OpenGL didn't; #806: a faulty Vulkan driver crashed the app before its window appeared; verified by unit tests and the reporter's runs with `WGPU_BACKEND`), and Linux on the GPU the desktop runs on, since a Wayland compositor may refuse frames from the other one; a window that fails on its GPU while starting up starts again on the next one instead of crashing (#502; the dual-GPU Wayland case is verified by unit tests and cross-compilation, not on such hardware); web drops land in the document they were dropped on (#359); desktop drops carry no pointer position (winit 0.30), so a dropped document opens as a tab of its own and a picture or text is placed in the middle of the view, while the web places anything dropped on the canvas and opens what is dropped off it, as Illustrator does (#472); a web page that loses its WebGPU device or WebGL context restarts its graphics with the same documents (#369); on Linux/Wayland the clipboard reads the compositor's own (data-control) and copied image files paste, but files can't be dropped on the window under native Wayland until eframe moves to winit 0.31 (#398; the Wayland side is verified by compilation only), so Preferences › Performance › Linux Display Server › X11 (XWayland) opens the window through XWayland, where drops work (#1151; verified by unit tests and cross-compilation, not on a Wayland desktop); on Linux the Paste menu checks the system clipboard on a background thread, so a clipboard owner that never answers no longer freezes the window, while Windows and macOS keep their quick in-line check (#510; the X11 hang verified by the contributor, here by headless tests); on KDE Plasma 6.3+ under Wayland, pens and pen displays reach the app only under XWayland (`WAYLAND_DISPLAY=`) or with KWin's `KWIN_WAYLAND_EMULATE_TABLET=1`, since winit 0.30 has no Wayland tablet input (#491, not verified on hardware); Windows/Linux packaging and accessibility are pending | @@ -1624,7 +1625,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur On the canvas, the renderer no longer draws the picture of the art below back as an image each time it copies the backdrop of a non-isolated group that blends: it keeps that picture and draws the art that follows over it. At 2880 × 1800, 50 translucent groups with Multiply members take 65 ms a frame instead of 299 ms on 8 threads (`vectorcraft-cli bench`), and the frames have the same pixels. Type inside a group, layer, symbol instance, blend or repeat keeps its opacity, blend mode, opacity mask, Opacity & Mask Define Knockout Shape, own effects, perspective and visibility when the effects on the container outline it. Hidden type with a geometry effect of its own is left out of exports. PDF export writes a knockout group as one PDF knockout group (`/K true`, with `/I true` when the group isolates; ISO 32000-1, 11.4.6.3), so a knockout group of any size opens again in VectorCraft as a knockout group, and the form nesting of the file does not grow with the number of elements. When an element's opacity or mask defines its knockout shape, or it paints translucently, each element is drawn through one soft mask of the overlapping elements above it instead; past 65,536 objects in a group's masks (or 2,048 elements) the group is a PDF knockout group, with a warning that its translucent elements knock out fully. SVG export draws each element of a knockout group through one mask of the overlapping elements above it, and the masks refer to one copy of each element's shape, so the elements stay siblings and the file nests no deeper with more elements; past 65,536 shapes in a group's masks (or 2,048 elements) the group is written as a plain group, with a warning. Evidence: `pdf/src/tests/blend.rs`, `svg/tests/it/blend_export.rs`, `pdf/tests/gradient_forms.rs` (a gradient in a knockout group reads back in place). - - **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; ten generated style libraries (including Game HUD, Status Effects, Painted and Sketched, whose seven styles each repaint a solid fill with one watercolor, dry brush, pastel, charcoal or ink effect, and Textures) save and load as `.vcstyles`. + - **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; eleven generated style libraries (including Game HUD, Status Effects, Neon & Glow, Painted and Sketched, whose seven styles each repaint a solid fill with one watercolor, dry brush, pastel, charcoal or ink effect, and Textures) save and load as `.vcstyles`. The built-in Textures graphic style library (`graphicStyle.addFromLibrary {library: "textures"}`) has eight styles that paint their fills and strokes with the texture effects: Fine Grain (Grain), Canvas Weave and Burlap Sack (Texturizer), Cracked Glaze (Craquelure), Mosaic Floor (Mosaic Tiles), Quilted Squares (Patchwork), Leaded Glass (Stained Glass) and Damp Paper (Water Paper). The help page names new graphic styles for textures ([What's new](https://helpx.adobe.com/illustrator/desktop/new-features/whats-new.html)) without naming them, so the styles, their names and their settings are VectorCraft's own (`tests::stylelib::textures_styles_apply_their_texture_effects_and_keep_their_params_through_save_and_open`). In the Graphic Styles panel, Shift-click selects a range of styles and Ctrl/Cmd-click adds or removes one. Applying a style to several objects, and Redefine Graphic Style, give the objects one shared copy of the style's appearance instead of a copy each; an object that is edited later gets its own copy, and an object whose placed gradients are fitted to its own bounds gets its own. The undo history's memory estimate counts a shared appearance once. Saved files are unchanged (`crates/engine/src/tests/styles.rs`). From ffe63354bc7e062e2b5f24dc66838ffed7b14d7e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 14:52:42 -0800 Subject: [PATCH 09/28] i18n: Danish rows for the composite font errors Danish joined the complete-message languages on main, so its catalog gets the 42 composite font error rows the other complete catalogs carry. --- crates/ui-egui/src/i18n/da.tsv | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index ca27bdd2f9..f71a172f0e 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -2164,6 +2164,48 @@ digits Malayalam Malayalam Composite Fonts Sammensatte skrifttyper Export Composite Fonts Eksportér sammensatte skrifttyper Import Composite Fonts Importér sammensatte skrifttyper +@msg each font must be {name, parts} hver skrifttype skal være {name, parts} +@msg unknown key `{k}`; a font takes name and parts ukendt nøgle `{k}`; en skrifttype tager name og parts +@msg each font needs a name hver skrifttype skal have et navn +@msg `{raw}` can't name a composite font (1 to {COMPOSITE_NAME_MAX} characters, no control characters) `{raw}` kan ikke være navnet på en sammensat skrifttype (1 til {COMPOSITE_NAME_MAX} tegn, ingen kontroltegn) +@msg `{name}`: parts must be an object `{name}`: parts skal være et objekt +@msg `{name}`: unknown class `{k}`; the classes are kanji, kana, punctuation, symbols, roman and numerals `{name}`: ukendt klasse `{k}`; klasserne er kanji, kana, punctuation, symbols, roman og numerals +@msg `{name}`: {_1} must be an object `{name}`: {_1} skal være et objekt +@msg `{name}`: unknown key `{k}` in {_1} `{name}`: ukendt nøgle `{k}` i {_1} +@msg `{name}`: give the kanji part `{name}`: angiv delen kanji +@msg a document keeps at most {COMPOSITE_FONTS_MAX} composite fonts et dokument har højst {COMPOSITE_FONTS_MAX} sammensatte skrifttyper +@msg two composite fonts are named `{_1}` to sammensatte skrifttyper hedder `{_1}` +@msg `{_1}` is the name of an installed font `{_1}` er navnet på en installeret skrifttype +@msg `{_1}`: {_2} is set in `{font}`, a composite font `{_1}`: {_2} er sat med `{font}`, en sammensat skrifttype +@msg give fonts: [{name, parts}…] angiv fonts: [{name, parts}…] +@msg path is empty path er tom +@msg fonts must be a list of composite fonts fonts skal være en liste med sammensatte skrifttyper +@msg names must be strings names skal være strenge +@msg no composite font named `{n}` ingen sammensat skrifttype hedder `{n}` +@msg names must be a list of names names skal være en liste med navne +@msg no composite fonts to export ingen sammensatte skrifttyper at eksportere +@msg not a composite fonts file: {e} ikke en fil med sammensatte skrifttyper: {e} +@msg not a composite fonts file: no compositeFonts list ikke en fil med sammensatte skrifttyper: ingen liste compositeFonts +@msg the file holds more than {COMPOSITE_FONTS_MAX} composite fonts filen indeholder mere end {COMPOSITE_FONTS_MAX} sammensatte skrifttyper +@msg the file is larger than 4 MB filen er større end 4 MB +@msg dataBase64 is not base64 dataBase64 er ikke base64 +@msg give one of path, data or dataBase64 angiv én af path, data eller dataBase64 +@msg the file holds no composite fonts filen indeholder ingen sammensatte skrifttyper +@msg the file holds two composite fonts named `{_1}` filen indeholder to sammensatte skrifttyper, der hedder `{_1}` +@msg `{_1}` can't name a composite font `{_1}` kan ikke være navnet på en sammensat skrifttype +@msg {k}: give a font {k}: angiv en skrifttype +@msg `{name}`: {k}: give a font `{name}`: {k}: angiv en skrifttype +@msg {k}: size must be {_1} to {_2}% {k}: size skal være {_1} til {_2}% +@msg `{name}`: {k}: size must be {_1} to {_2}% `{name}`: {k}: size skal være {_1} til {_2}% +@msg {k}: baseline must be {_1} to {_2}% {k}: baseline skal være {_1} til {_2}% +@msg `{name}`: {k}: baseline must be {_1} to {_2}% `{name}`: {k}: baseline skal være {_1} til {_2}% +@msg {k}: scale must be {_1} to {_2}% {k}: skaleringen skal være {_1} til {_2}% +@msg `{name}`: {k}: scale must be {_1} to {_2}% `{name}`: {k}: skaleringen skal være {_1} til {_2}% +@msg {k}: only kana, roman and numerals can be scaled {k}: kun kana, roman og numerals kan skaleres +@msg `{name}`: {k}: only kana, roman and numerals can be scaled `{name}`: {k}: kun kana, roman og numerals kan skaleres +@msg {k}: only kana can be scaled at the center {k}: kun kana kan skaleres fra midten +@msg `{name}`: {k}: only kana can be scaled at the center `{name}`: {k}: kun kana kan skaleres fra midten +@msg no composite font ingen sammensat skrifttype Delete Font Slet skrifttype Kanji Kanji Kana Kana From 2c5da43728f540ccf108b52fcccc8967ce851e66 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 17:49:15 -0800 Subject: [PATCH 10/28] Status bar: a background save or failed job keeps a newer status line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A save written in the background reported "Saved " when it finished, and a background job that failed reported "