mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-11 04:50:23 -06:00
Naming: use Diagram and Decorative Text in the feature catalog (#3622)
* Naming: use Diagram and Decorative Text in the feature catalog * Naming: align the Chinese decorative text term * test(naming): guard neutral display labels * Naming: Diagram no longer repeats the chart word in de, uk and ro German Schema and Ukrainian Схема match craft-i18n#211; Romanian Schemă avoids GridCraft's own chart word Diagramă. A catalog test checks every shipped language translates both labels and that Diagram differs from Chart/Charts. The display-name guard now handles escaped char literals and any #[cfg(test)] item, asserts it scanned catalog.rs and a minimum of files and literals, and tests word boundaries. The catalog test asserts each ID appears once. Docs carry timestamps and revision history. --------- Co-authored-by: FredApps <FredApps@users.noreply.github.com>
This commit is contained in:
@@ -118,7 +118,7 @@ pub const CATALOG: &[(&str, &str, &str, &str)] = &[
|
||||
("insert.picture", "Insert", "Illustrations", "Pictures"),
|
||||
("insert.shape", "Insert", "Illustrations", "Shapes"),
|
||||
("insert.icons", "Insert", "Illustrations", "Icons"),
|
||||
("insert.smartArt", "Insert", "Illustrations", "SmartArt"),
|
||||
("insert.smartArt", "Insert", "Illustrations", "Diagram"),
|
||||
("insert.screenshot", "Insert", "Illustrations", "Screenshot"),
|
||||
("insert.recommendedCharts", "Insert", "Charts", "Recommended Charts"),
|
||||
("insert.chart", "Insert", "Charts", "Charts"),
|
||||
@@ -131,7 +131,7 @@ pub const CATALOG: &[(&str, &str, &str, &str)] = &[
|
||||
("review.newComment", "Insert", "Comments", "Comment"),
|
||||
("insert.textBox", "Insert", "Text", "Text Box"),
|
||||
("pageLayout.headerFooter", "Insert", "Text", "Header & Footer"),
|
||||
("insert.wordArt", "Insert", "Text", "WordArt"),
|
||||
("insert.wordArt", "Insert", "Text", "Decorative Text"),
|
||||
("insert.signatureLine", "Insert", "Text", "Signature Line"),
|
||||
("insert.object", "Insert", "Text", "Object"),
|
||||
("insert.equation", "Insert", "Symbols", "Equation"),
|
||||
@@ -340,3 +340,15 @@ pub fn parity() -> (usize, usize) {
|
||||
let done = ids.iter().filter(|id| crate::find_command(id).is_some()).count();
|
||||
(done, ids.len())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod feature_name_tests {
|
||||
#[test]
|
||||
fn own_feature_labels_keep_command_ids() {
|
||||
for (id, expected) in [("insert.smartArt", "Diagram"), ("insert.wordArt", "Decorative Text")] {
|
||||
let entries: Vec<_> = super::CATALOG.iter().filter(|entry| entry.0 == id).collect();
|
||||
assert_eq!(entries.len(), 1, "{id} appears once");
|
||||
assert_eq!(entries[0].3, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,12 @@ impl Language {
|
||||
/// A label that needs two translations (Home's text "Orientation" vs Page Layout's print
|
||||
/// "Orientation") is keyed `context|label`: English shows only the part after the `|`.
|
||||
pub fn tr(self, text: &str) -> &str {
|
||||
let translated = match self {
|
||||
self.translated(text).unwrap_or_else(|| text.rsplit_once('|').map_or(text, |(_, label)| label))
|
||||
}
|
||||
|
||||
/// The catalog's own translation of `text`, or `None` when it falls back to English.
|
||||
fn translated(self, text: &str) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::En => None,
|
||||
Self::Es => es::translate(text),
|
||||
Self::El => el::translate(text),
|
||||
@@ -214,8 +219,7 @@ impl Language {
|
||||
Self::Vi => vi::translate(text),
|
||||
Self::Nl => nl::translate(text),
|
||||
Self::Ro => ro::translate(text),
|
||||
};
|
||||
translated.unwrap_or_else(|| text.rsplit_once('|').map_or(text, |(_, label)| label))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tr_error(self, message: &str) -> String {
|
||||
@@ -321,6 +325,23 @@ fn name_sort_key(name: &str) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every shipped catalog names GridCraft's own Diagram and Decorative Text features, and
|
||||
/// Diagram never repeats that catalog's chart word (both sit on the Insert tab).
|
||||
#[test]
|
||||
fn every_catalog_has_distinct_diagram_and_decorative_text() {
|
||||
for language in Language::ALL.into_iter().filter(|l| *l != Language::En) {
|
||||
let code = language.code();
|
||||
let diagram = language.translated("Diagram");
|
||||
assert!(diagram.is_some(), "{code}: Diagram");
|
||||
assert!(language.translated("Decorative Text").is_some(), "{code}: Decorative Text");
|
||||
for chart in ["Chart", "Charts"] {
|
||||
if let Some(chart_text) = language.translated(chart) {
|
||||
assert_ne!(diagram, Some(chart_text), "{code}: Diagram equals {chart}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Help tables are sorted and unique: lookups binary-search them.
|
||||
#[test]
|
||||
fn function_help_tables_are_sorted_and_unique() {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/// Returns the Czech translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Diagram",
|
||||
"Decorative Text" => "Ozdobný text",
|
||||
"Paste" => "Vložit",
|
||||
"Paste Values" => "Vložit hodnoty",
|
||||
"Paste Formulas" => "Vložit vzorce",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
/// Returns the German translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Schema",
|
||||
"Decorative Text" => "Dekorativer Text",
|
||||
"Paste" => "Einfügen",
|
||||
"Paste Values" => "Werte einfügen",
|
||||
"Paste Formulas" => "Formeln einfügen",
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
/// Returns the Greek translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Διάγραμμα",
|
||||
"Decorative Text" => "Διακοσμητικό κείμενο",
|
||||
"Paste" => "Επικόλληση",
|
||||
"Paste Values" => "Επικόλληση τιμών",
|
||||
"Paste Formulas" => "Επικόλληση τύπων",
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
/// Returns the Spanish translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Diagrama",
|
||||
"Decorative Text" => "Texto decorativo",
|
||||
"Paste" => "Pegar",
|
||||
"Paste Values" => "Pegar valores",
|
||||
"Paste Formulas" => "Pegar fórmulas",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/// Returns the French translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Diagramme",
|
||||
"Decorative Text" => "Texte décoratif",
|
||||
"Different First Page" => "Première page différente",
|
||||
"Different Odd & Even Pages" => "Pages paires et impaires différentes",
|
||||
"Scale with Document" => "Mettre à l'échelle avec le document",
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
/// Returns the Italian translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Diagramma",
|
||||
"Decorative Text" => "Testo decorativo",
|
||||
"Paste" => "Incolla",
|
||||
"Paste Values" => "Incolla valori",
|
||||
"Paste Formulas" => "Incolla formule",
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
/// English label → Japanese, the shared reference set.
|
||||
pub(super) const CORE: &[(&str, &str)] = &[
|
||||
("Diagram", "図解"),
|
||||
("Decorative Text", "装飾文字"),
|
||||
("Paste", "貼り付け"),
|
||||
("Paste Values", "値の貼り付け"),
|
||||
("Paste Formulas", "数式の貼り付け"),
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/// Returns the Korean translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "다이어그램",
|
||||
"Decorative Text" => "장식 텍스트",
|
||||
"Paste" => "붙여넣기",
|
||||
"Paste Values" => "값 붙여넣기",
|
||||
"Paste Formulas" => "수식 붙여넣기",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/// Returns the Dutch translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Diagram",
|
||||
"Decorative Text" => "Decoratieve tekst",
|
||||
"Paste" => "Plakken",
|
||||
"Paste Values" => "Waarden plakken",
|
||||
"Paste Formulas" => "Formules plakken",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/// Returns the Polish translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Diagram",
|
||||
"Decorative Text" => "Tekst ozdobny",
|
||||
"Paste" => "Wklej",
|
||||
"Paste Values" => "Wklej wartości",
|
||||
"Paste Formulas" => "Wklej formuły",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
/// Returns the Portuguese translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Diagrama",
|
||||
"Decorative Text" => "Texto decorativo",
|
||||
"Paste" => "Colar",
|
||||
"Paste Values" => "Colar Valores",
|
||||
"Paste Formulas" => "Colar Fórmulas",
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
/// Returns the Romanian translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Schemă",
|
||||
"Decorative Text" => "Text decorativ",
|
||||
"Paste" => "Lipire",
|
||||
"Paste Values" => "Lipire valori",
|
||||
"Paste Formulas" => "Lipire formule",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/// Returns the Russian translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Схема",
|
||||
"Decorative Text" => "Декоративный текст",
|
||||
"Paste" => "Вставить",
|
||||
"Paste Values" => "Вставить значения",
|
||||
"Paste Formulas" => "Вставить формулы",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/// Returns the Serbian translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Dijagram",
|
||||
"Decorative Text" => "Ukrasni tekst",
|
||||
"Paste" => "Nalepi",
|
||||
"Paste Values" => "Nalepi vrednosti",
|
||||
"Paste Formulas" => "Nalepi formule",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
/// Returns the Turkish translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Diyagram",
|
||||
"Decorative Text" => "Dekoratif Metin",
|
||||
"Paste" => "Yapıştır",
|
||||
"Paste Values" => "Değerleri Yapıştır",
|
||||
"Paste Formulas" => "Formülleri Yapıştır",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/// Returns the Ukrainian translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Схема",
|
||||
"Decorative Text" => "Декоративний текст",
|
||||
"Paste" => "Вставити",
|
||||
"Paste Values" => "Вставити значення",
|
||||
"Paste Formulas" => "Вставити формули",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
/// Returns the Vietnamese translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "Sơ đồ",
|
||||
"Decorative Text" => "Chữ trang trí",
|
||||
"Paste" => "Dán",
|
||||
"Paste Values" => "Dán giá trị",
|
||||
"Paste Formulas" => "Dán công thức",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/// Returns the Chinese translation for `text`, or `None` so callers fall back to English.
|
||||
pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
Some(match text {
|
||||
"Diagram" => "图示",
|
||||
"Decorative Text" => "装饰文字",
|
||||
"Paste" => "粘贴",
|
||||
"Paste Values" => "粘贴值",
|
||||
"Paste Formulas" => "粘贴公式",
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Guard the production display surfaces without banning compatibility identifiers or notices.
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const BANNED: &[&str] = &["SmartArt", "WordArt"];
|
||||
const ALLOWED: &[&str] = &[];
|
||||
const ROOTS: &[&str] = &["src", "../engine/src/catalog.rs"];
|
||||
|
||||
fn is_word(c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_'
|
||||
}
|
||||
|
||||
fn banned_display(text: &str) -> bool {
|
||||
if ALLOWED.contains(&text) {
|
||||
return false;
|
||||
}
|
||||
BANNED.iter().any(|banned| {
|
||||
text.match_indices(banned)
|
||||
.any(|(at, _)| !text[..at].chars().next_back().is_some_and(is_word) && !text[at + banned.len()..].chars().next().is_some_and(is_word))
|
||||
})
|
||||
}
|
||||
|
||||
// Read ordinary and raw Rust string literals, ignoring comments. No compiler dependency is
|
||||
// needed, so this guard can also run by itself when a full application build is unavailable.
|
||||
#[allow(clippy::cognitive_complexity)] // This small test-only lexer keeps raw-string/comment state together.
|
||||
fn literals(source: &str) -> Vec<String> {
|
||||
let chars: Vec<char> = source.chars().collect();
|
||||
let mut out = Vec::new();
|
||||
let mut at = 0;
|
||||
let mut test_module = false;
|
||||
let mut test_depth = 0_usize;
|
||||
while at < chars.len() {
|
||||
if chars.get(at..at + 12) == Some(&['#', '[', 'c', 'f', 'g', '(', 't', 'e', 's', 't', ')', ']']) {
|
||||
at += 12;
|
||||
// Any test-only item (module, fn, use, const...) ends at its closing brace or at a
|
||||
// `;` outside braces, so its literals are skipped and later production is scanned.
|
||||
test_module = true;
|
||||
} else if chars.get(at..at + 2) == Some(&['/', '/']) {
|
||||
while at < chars.len() && chars[at] != '\n' {
|
||||
at += 1;
|
||||
}
|
||||
} else if chars.get(at..at + 2) == Some(&['/', '*']) {
|
||||
at += 2;
|
||||
let mut depth = 1;
|
||||
while at < chars.len() && depth > 0 {
|
||||
if chars.get(at..at + 2) == Some(&['/', '*']) {
|
||||
depth += 1;
|
||||
at += 2;
|
||||
} else if chars.get(at..at + 2) == Some(&['*', '/']) {
|
||||
depth -= 1;
|
||||
at += 2;
|
||||
} else {
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
} else if chars[at] == 'r' && matches!(chars.get(at + 1), Some('"' | '#')) {
|
||||
let start = at;
|
||||
at += 1;
|
||||
while chars.get(at) == Some(&'#') {
|
||||
at += 1;
|
||||
}
|
||||
let hashes = at - start - 1;
|
||||
if chars.get(at) != Some(&'"') {
|
||||
continue;
|
||||
}
|
||||
at += 1;
|
||||
let begin = at;
|
||||
while at < chars.len() {
|
||||
if chars[at] == '"' && (1..=hashes).all(|n| chars.get(at + n) == Some(&'#')) {
|
||||
break;
|
||||
}
|
||||
at += 1;
|
||||
}
|
||||
if !test_module {
|
||||
out.push(chars[begin..at].iter().collect());
|
||||
}
|
||||
at += 1 + hashes;
|
||||
} else if chars[at] == '"' {
|
||||
at += 1;
|
||||
let mut text = String::new();
|
||||
while at < chars.len() {
|
||||
let c = chars[at];
|
||||
at += 1;
|
||||
if c == '"' {
|
||||
break;
|
||||
}
|
||||
if c == '\\' && at < chars.len() {
|
||||
text.push(chars[at]);
|
||||
at += 1;
|
||||
} else {
|
||||
text.push(c);
|
||||
}
|
||||
}
|
||||
if !test_module {
|
||||
out.push(text);
|
||||
}
|
||||
} else if chars[at] == '\'' && chars.get(at + 1) == Some(&'\\') {
|
||||
// An escaped character literal such as '\'', '\"' or '\u{22}': skip to its closing quote.
|
||||
at += 3;
|
||||
while at < chars.len() && chars[at] != '\'' && chars[at] != '\n' {
|
||||
at += 1;
|
||||
}
|
||||
at += 1;
|
||||
} else if chars[at] == '\'' && chars.get(at + 2) == Some(&'\'') {
|
||||
at += 3; // A character literal such as '"', not the start of a string.
|
||||
} else {
|
||||
if test_module {
|
||||
match chars[at] {
|
||||
'{' => test_depth += 1,
|
||||
'}' => {
|
||||
test_depth = test_depth.saturating_sub(1);
|
||||
if test_depth == 0 {
|
||||
test_module = false;
|
||||
}
|
||||
}
|
||||
';' if test_depth == 0 => test_module = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Scan {
|
||||
files: usize,
|
||||
literals: usize,
|
||||
catalog: bool,
|
||||
failures: Vec<String>,
|
||||
}
|
||||
|
||||
fn check_tree(path: &Path, scan: &mut Scan) {
|
||||
if path.is_dir() {
|
||||
for entry in std::fs::read_dir(path).expect("source directory") {
|
||||
let path = entry.expect("source entry").path();
|
||||
if path.file_name().is_some_and(|name| name == "tests") {
|
||||
continue;
|
||||
}
|
||||
check_tree(&path, scan);
|
||||
}
|
||||
} else if path.extension().is_some_and(|ext| ext == "rs") {
|
||||
let name = path.file_name().expect("source filename").to_string_lossy();
|
||||
if name == "tests.rs" || name.ends_with("_tests.rs") {
|
||||
return;
|
||||
}
|
||||
let source = std::fs::read_to_string(path).expect("UTF-8 Rust source");
|
||||
scan.files += 1;
|
||||
scan.catalog |= name == "catalog.rs";
|
||||
// Skip only inline test modules, not later production following test-only imports.
|
||||
for text in literals(&source) {
|
||||
scan.literals += 1;
|
||||
if banned_display(&text) {
|
||||
scan.failures.push(format!("{}: {text:?}", path.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_display_names_stay_neutral() {
|
||||
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let mut scan = Scan::default();
|
||||
for root in ROOTS {
|
||||
let root = base.join(root);
|
||||
assert!(root.exists(), "missing display source: {}", root.display());
|
||||
check_tree(&root, &mut scan);
|
||||
}
|
||||
// A scan that silently finds nothing would pass vacuously.
|
||||
assert!(scan.catalog, "engine catalog.rs was not scanned");
|
||||
assert!(scan.files >= 50, "only {} source files scanned", scan.files);
|
||||
assert!(scan.literals >= 5_000, "only {} literals scanned", scan.literals);
|
||||
assert!(scan.failures.is_empty(), "Banned display names:\n{}", scan.failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_distinguishes_labels_from_identifiers_and_comments() {
|
||||
for name in BANNED {
|
||||
assert!(banned_display(name), "{name}");
|
||||
}
|
||||
for name in ALLOWED {
|
||||
assert!(!banned_display(name), "{name}");
|
||||
}
|
||||
assert!(!banned_display("filter.cameraRaw"));
|
||||
assert!(!banned_display("tool.guidedUpright"));
|
||||
assert!(!banned_display("insert.smartArt"));
|
||||
for identifier in ["mySmartArt", "SmartArt2", "_SmartArt_", "WordArtStyle"] {
|
||||
assert!(!banned_display(identifier), "{identifier}");
|
||||
}
|
||||
for label in ["Use SmartArt here", "WordArt…", "(SmartArt)"] {
|
||||
assert!(banned_display(label), "{label}");
|
||||
}
|
||||
assert_eq!(literals("// \"ignored\"\n/* \"ignored\" */ \"label\""), ["label"]);
|
||||
assert_eq!(literals("r#\"raw label\"#"), ["raw label"]);
|
||||
assert_eq!(literals("r##\"a \"# b\"## \"next\""), ["a \"# b", "next"]);
|
||||
assert_eq!(literals("\"say \\\"hi\\\"\" \"next\""), ["say \"hi\"", "next"]);
|
||||
assert_eq!(literals("/* outer /* \"inner\" */ \"still\" */ \"label\""), ["label"]);
|
||||
assert_eq!(literals("let q = '\\''; let d = '\\\"'; let u = '\\u{22}'; \"label\""), ["label"]);
|
||||
assert_eq!(literals("let d = '\"'; \"label\""), ["label"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_scans_production_after_test_only_items() {
|
||||
let source = r###"#[cfg(test)] mod tests { let _ = "ignored"; }
|
||||
#[cfg(test)] use fixtures::Helper;
|
||||
fn production() { let _ = r#"live label"#; }
|
||||
"###;
|
||||
assert_eq!(literals(source), ["live label"]);
|
||||
assert_eq!(literals("#[cfg(test)] mod tests; fn live() { let _ = \"live label\"; }"), ["live label"]);
|
||||
let items = r#"#[cfg(test)] pub mod a { "x"; }
|
||||
#[cfg(test)] fn helper() { "y"; }
|
||||
#[cfg(test)] const C: &str = "z";
|
||||
fn live() { "live"; }"#;
|
||||
assert_eq!(literals(items), ["live"]);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Feature naming
|
||||
|
||||
> **Last reviewed:** 2026-11-10 · **Last updated:** 2026-11-10 · **Change:** minor (Diagram words aligned with craft-i18n#211)
|
||||
|
||||
ArtCraft apps use descriptive names for their own features. Product names used to describe compatibility, file formats, attribution or external software remain accurate.
|
||||
|
||||
## Naming map
|
||||
|
||||
SmartArt → Diagram; WordArt → Decorative Text. These are catalog labels for pending features, not a claim that the features are implemented.
|
||||
|
||||
## Compatibility boundary
|
||||
|
||||
The insert.smartArt and insert.wordArt command IDs and pending keytips remain unchanged. OOXML data, imported names and interoperability references are unchanged. No assets, attribution, dependencies or rendering behavior change.
|
||||
|
||||
## Localization and verification
|
||||
|
||||
Each shipped non-English interface catalog includes the new labels. Diagram uses a word distinct from Chart in every catalog: German (`Schema`), Ukrainian (`Схема`) and Romanian (`Schemă`) avoid their chart word, matching the shared wording in [craft-i18n#211](https://github.com/storytold/craft-i18n/pull/211) for German and Ukrainian. Regression tests cover the labels, every shipped catalog and stable command identities. The translations are AI-assisted and not yet reviewed by native speakers.
|
||||
|
||||
## Regression coverage
|
||||
|
||||
The standard-library-only `display_names` integration tests scan production Rust string literals for the retired display names and preserve explicit compatibility exceptions. They also test identifier, comment and raw-string handling. These focused checks do not replace application or visual testing.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-10 | minor | Diagram no longer repeats the chart word: German `Schema`, Ukrainian `Схема` (as craft-i18n#211), Romanian `Schemă`; a test covers every shipped catalog |
|
||||
| 2026-11-08 | minor | Created: SmartArt → Diagram and WordArt → Decorative Text as GridCraft's own catalog labels |
|
||||
@@ -1,6 +1,6 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (CJK fallback baseline alignment); previously minor (Arabic bidi display, editing and cell reading direction; missing translations are listed by `cargo xtask i18n-report` and never fail a check); previously minor (Arabic font fallback; missing translations are listed by `cargo xtask i18n-report` and never fail a check); previously minor (number formats honour the `B1`/`B2` calendar prefixes, checked against Excel; earlier: missing translations are listed by `cargo xtask i18n-report` and never fail a check; Arabic text sorts by its letters, not its vowel marks); previously minor (AutoFit estimates Arabic text width from measured glyph widths); previously minor (missing translations are listed by `cargo xtask i18n-report` and never fail a check; Arabic text sorts by its letters, not its vowel marks) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (Diagram and Decorative Text labels, Diagram distinct from Chart); previously minor (CJK fallback baseline alignment); previously minor (Arabic bidi display, editing and cell reading direction; missing translations are listed by `cargo xtask i18n-report` and never fail a check); previously minor (Arabic font fallback; missing translations are listed by `cargo xtask i18n-report` and never fail a check); previously minor (number formats honour the `B1`/`B2` calendar prefixes, checked against Excel; earlier: missing translations are listed by `cargo xtask i18n-report` and never fail a check; Arabic text sorts by its letters, not its vowel marks); previously minor (AutoFit estimates Arabic text width from measured glyph widths); previously minor (missing translations are listed by `cargo xtask i18n-report` and never fail a check; Arabic text sorts by its letters, not its vowel marks) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Per-language status against Excel, which ships its UI in dozens of languages (Windows language
|
||||
packs cover 100+), with localized function names, argument separators, number and date formats,
|
||||
@@ -133,6 +133,7 @@ language (none so far) and of localized function names.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-10 | minor | Diagram and Decorative Text labels in all 18 non-English catalogs; Diagram never repeats the catalog's chart word (German `Schema`, Ukrainian `Схема`, Romanian `Schemă`), see [feature-names.md](feature-names.md) |
|
||||
| 2026-10-31 | minor | System and embedded CJK text fallbacks share the Latin baseline in cells, editors and window titles; other scripts, symbols and emoji keep their alignment |
|
||||
| 2026-10-26 | minor | Shared bidi layout supports Arabic clusters, marks, visual cursor navigation and directional accessibility; cell Context/LTR/RTL is stored separately from sheet RTL and round-trips through XLSX |
|
||||
| 2026-10-26 | minor | Arabic text renders in cells and the interface: a system Arabic face (Tahoma, Arial or Segoe UI on Windows, Geeza Pro on macOS, Noto Sans or Naskh Arabic on Linux) joins the script fallbacks, fontconfig's Arabic choice, when it has Arabic letters, leads the Arabic fallbacks and draws Arabic wherever the UI or cell font has none, such as the default cell fonts (part of #229; a UI font with Arabic of its own, such as DejaVu Sans in the Flatpak, still draws interface Arabic, left for a follow-up), and craft-fonts' Noto Sans Arabic is a required face allowed on the web. Workbook fonts without Arabic keep every script fallback. Shaping and bidirectional layout remain separate work |
|
||||
|
||||
@@ -165,11 +165,11 @@ Implemented:
|
||||
|
||||
Missing:
|
||||
|
||||
- `insert.smartArt` SmartArt (Illustrations)
|
||||
- `insert.smartArt` Diagram (Illustrations)
|
||||
- `insert.screenshot` Screenshot (Illustrations)
|
||||
- `insert.map` Maps (Charts)
|
||||
- `insert.timeline` Timeline (Filters)
|
||||
- `insert.wordArt` WordArt (Text)
|
||||
- `insert.wordArt` Decorative Text (Text)
|
||||
- `insert.signatureLine` Signature Line (Text)
|
||||
- `insert.object` Object (Text)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user