mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-25 11:26:46 -06:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c00ae3492 | ||
|
|
c1dd60ffd6 | ||
|
|
bec28d6b87 | ||
|
|
d3e6111cf1 | ||
|
|
dd1f0531ac |
@@ -21,3 +21,41 @@ jobs:
|
||||
nix:
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/nix.yml@main
|
||||
|
||||
git-sources:
|
||||
name: Git dependencies come from storytold
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Git dependencies come from storytold
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
|
||||
# Source replacement, patches and path overrides in cargo config never show up
|
||||
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
|
||||
# Cargo still reads the legacy .cargo/config.
|
||||
python3 - <<'PY'
|
||||
import os, sys, tomllib
|
||||
for path in (".cargo/config", ".cargo/config.toml"):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
config = tomllib.load(open(path, "rb"))
|
||||
replaced = [name for name, source in config.get("source", {}).items()
|
||||
if isinstance(source, dict) and "replace-with" in source]
|
||||
if replaced or "patch" in config or "paths" in config:
|
||||
print(f"{path} replaces, patches or overrides a source")
|
||||
sys.exit(1)
|
||||
PY
|
||||
# Fails when Cargo.lock is out of date with the manifests.
|
||||
cargo metadata --locked --format-version 1 > /dev/null
|
||||
python3 - <<'PY'
|
||||
import sys, tomllib
|
||||
lock = tomllib.load(open("Cargo.lock", "rb"))
|
||||
bad = sorted({p["source"] for p in lock.get("package", [])
|
||||
if p.get("source", "").startswith("git+")
|
||||
and not p["source"].startswith("git+https://github.com/storytold/")})
|
||||
if bad:
|
||||
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
@@ -11,14 +11,57 @@ on:
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
git-sources:
|
||||
name: Git dependencies come from storytold
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Git dependencies come from storytold
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
|
||||
# Source replacement, patches and path overrides in cargo config never show up
|
||||
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
|
||||
# Cargo still reads the legacy .cargo/config.
|
||||
python3 - <<'PY'
|
||||
import os, sys, tomllib
|
||||
for path in (".cargo/config", ".cargo/config.toml"):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
config = tomllib.load(open(path, "rb"))
|
||||
replaced = [name for name, source in config.get("source", {}).items()
|
||||
if isinstance(source, dict) and "replace-with" in source]
|
||||
if replaced or "patch" in config or "paths" in config:
|
||||
print(f"{path} replaces, patches or overrides a source")
|
||||
sys.exit(1)
|
||||
PY
|
||||
# Fails when Cargo.lock is out of date with the manifests.
|
||||
cargo metadata --locked --format-version 1 > /dev/null
|
||||
python3 - <<'PY'
|
||||
import sys, tomllib
|
||||
lock = tomllib.load(open("Cargo.lock", "rb"))
|
||||
bad = sorted({p["source"] for p in lock.get("package", [])
|
||||
if p.get("source", "").startswith("git+")
|
||||
and not p["source"].startswith("git+https://github.com/storytold/")})
|
||||
if bad:
|
||||
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
release:
|
||||
needs: git-sources
|
||||
permissions:
|
||||
# The reusable release workflow tags and uploads the draft release.
|
||||
contents: write
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/release.yml@main
|
||||
with:
|
||||
|
||||
@@ -11,7 +11,8 @@ permissions:
|
||||
|
||||
concurrency:
|
||||
group: sonarcloud-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# A push to main is the run that publishes coverage; superseded runs there would never finish.
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
sonarcloud:
|
||||
|
||||
@@ -18,8 +18,7 @@ pub use craft_docpath::Refused;
|
||||
/// on other platforms.
|
||||
pub fn resolve(stored: &str, document: Option<&Path>) -> Result<PathBuf, Refused> {
|
||||
let document = document.filter(|d| !d.as_os_str().is_empty()).map(|d| std::path::absolute(d).unwrap_or_else(|_| d.to_path_buf()));
|
||||
let path = craft_docpath::resolve(stored.trim(), document.as_deref())?;
|
||||
if path.is_absolute() { Ok(path) } else { Err(Refused::Invalid) }
|
||||
craft_docpath::resolve_absolute(stored.trim(), document.as_deref())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+204
-895
File diff suppressed because it is too large
Load Diff
@@ -391,20 +391,4 @@ mod tests {
|
||||
assert_eq!(parse_hex(bad), None, "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dialog_labels_have_ukrainian_translations() {
|
||||
let sources = [include_str!("dialog.rs")];
|
||||
let mut missing: Vec<String> = dialog::TABS.iter().filter(|(_, l)| crate::i18n::tr("uk", l) == *l).map(|(_, l)| l.to_string()).collect();
|
||||
for source in sources {
|
||||
for rest in source.split("crate::tl!(").skip(1) {
|
||||
let Some(text) = rest.strip_prefix('"').and_then(|r| r.split('"').next()) else { continue };
|
||||
// File format names read the same in every language.
|
||||
if crate::i18n::tr("uk", text) == text && !["DXF", "DWG"].contains(&text) {
|
||||
missing.push(text.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(missing.is_empty(), "missing translations: {missing:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,37 +169,6 @@ fn a_selected_block_reference_offers_the_block_editor() {
|
||||
assert_eq!(app.session.state().unwrap().block_editor(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_menu_label_has_a_ukrainian_translation() {
|
||||
let mut app = app();
|
||||
app.cmdline("LINE 0,0 10,0");
|
||||
app.cmdline("");
|
||||
app.run("pline", json!({ "vertices": [[0, 0], [10, 0]] })).unwrap();
|
||||
select_all(&mut app);
|
||||
let mut all = flatten(&context_menu::entries(&app));
|
||||
let line = app.session.doc().unwrap().model.handles()[0];
|
||||
app.session.set_selection(app.session.doc().unwrap().model.handles().into_iter().filter(|h| *h != line).collect());
|
||||
all.extend(flatten(&context_menu::entries(&app)));
|
||||
all.extend(flatten(&context_menu::snap_entries(&app)));
|
||||
type_line(&mut app, "LINE");
|
||||
type_line(&mut app, "0,0");
|
||||
all.extend(flatten(&context_menu::command_entries(&app).unwrap()));
|
||||
let mut missing = Vec::new();
|
||||
for (label, id, _) in &all {
|
||||
let segments: Vec<&str> = label.split('/').collect();
|
||||
// The item itself is user text when it types a value, or a command name (Recent Commands).
|
||||
let user_text = (id.starts_with(context_menu::TYPE) && !id.contains('\'')) || segments.first() == Some(&"Recent Commands");
|
||||
let n = segments.len() - usize::from(user_text);
|
||||
for s in &segments[..n] {
|
||||
// Point filters (.X) and "Repeat LINE" (a template) aren't catalog keys.
|
||||
if !s.starts_with('.') && !s.starts_with("Repeat ") && cadcraft_ui_egui::i18n::tr("uk", s) == *s {
|
||||
missing.push(s.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(missing.is_empty(), "untranslated menu labels: {missing:?}");
|
||||
}
|
||||
|
||||
/// The text painted in one frame at `time` (seconds).
|
||||
fn painted(app: &mut CadApp, ctx: &egui::Context, events: Vec<egui::Event>, time: f64) -> String {
|
||||
fn collect(shape: &egui::Shape, text: &mut String) {
|
||||
|
||||
@@ -136,7 +136,7 @@ fn layer_and_properties_palette_menus() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_label_has_a_ukrainian_translation_and_every_id_a_command() {
|
||||
fn every_id_is_a_command() {
|
||||
let mut app = app();
|
||||
app.cmdline("LINE 0,0 10,0");
|
||||
app.cmdline("");
|
||||
@@ -146,16 +146,7 @@ fn every_label_has_a_ukrainian_translation_and_every_id_a_command() {
|
||||
all.extend(flatten(&sm::layout_tab_entries(&app, "Layout1")));
|
||||
all.extend(flatten(&sm::layer_entries(&app, "0")));
|
||||
all.extend(flatten(&sm::properties_entries(&app)));
|
||||
let mut missing = Vec::new();
|
||||
for (label, id, _) in &all {
|
||||
let segments: Vec<&str> = label.split('/').collect();
|
||||
// Command names (Recent Commands) and line counts aren't translated.
|
||||
let n = segments.len() - usize::from(segments.len() > 1);
|
||||
for s in &segments[..n] {
|
||||
if cadcraft_ui_egui::i18n::tr("uk", s) == *s {
|
||||
missing.push(s.to_string());
|
||||
}
|
||||
}
|
||||
let command = id.strip_prefix("run:").and_then(|r| r.split_once(':')).map_or(id.as_str(), |(c, _)| c);
|
||||
let special = ["drawing:", "layout:", "cmdline:"].iter().any(|p| id.starts_with(p));
|
||||
assert!(
|
||||
@@ -163,5 +154,4 @@ fn every_label_has_a_ukrainian_translation_and_every_id_a_command() {
|
||||
"{label}: {id} isn't a command"
|
||||
);
|
||||
}
|
||||
assert!(missing.is_empty(), "untranslated menu labels: {missing:?}");
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ selection. Resetting palettes keeps the language choice.
|
||||
The catalogs cover command and menu labels, tool groups and tooltips, the Start page,
|
||||
file and model-space tabs, status controls, object snaps, drafting prompts, property labels,
|
||||
dialogs, and the About window's controls. The command reference can be searched by English
|
||||
command ID, English label, or translated label. Ukrainian has enforced coverage;
|
||||
missing entries in the other catalogs are reported by tests and fall back to English.
|
||||
command ID, English label, or translated label. A missing entry falls back to English and
|
||||
never fails a build; `cargo xtask i18n` (a step of `cargo xtask ci`) lists each language's.
|
||||
|
||||
Czech has 1,202 catalog rows, covering the reference catalog and every marked UI
|
||||
literal, including Options. Four `menu` entries distinguish menu headings from object/command
|
||||
@@ -139,14 +139,14 @@ The implementation follows the display-time catalog pattern used by
|
||||
- Windows retain explicit IDs based on their English source, and tool groups retain their
|
||||
source names as keys, so switching languages preserves their state.
|
||||
- Add a translation attribution row in `ATTRIBUTION.md`, update the language lists, and add
|
||||
locale, plural, font-glyph and live-switch tests. Add the catalog to `REPORTED` in
|
||||
`i18n/tests.rs`: only Ukrainian, the reference translation, is in `REQUIRED`, so adding a
|
||||
command needs a row in one language, not in all of them.
|
||||
the language's locale tags and plural rule. The rendering, font-glyph and live-switch tests
|
||||
run for every catalog; no test checks one language's wording, and adding a command needs
|
||||
only its English label.
|
||||
- Catalogs are embedded and parsed once. Missing or malformed rows cannot crash the app;
|
||||
tests reject invalid rows, duplicate keys, changed placeholders, and mismatched ellipses.
|
||||
- `cargo test -p cadcraft-ui-egui i18n` checks menu/command/tool/snap coverage, every marked
|
||||
UI literal, locale resolution, preferences, integer plurals, bundled glyphs and actual
|
||||
egui renders with language switching. `cargo xtask assets` checks asset attribution;
|
||||
- `cargo test -p cadcraft-ui-egui i18n` checks the catalogs, locale resolution, preferences,
|
||||
integer plurals, bundled glyphs and actual egui renders with language switching.
|
||||
`cargo xtask i18n` prints the strings each language shows in English. `cargo xtask assets` checks asset attribution;
|
||||
`cargo xtask ci` runs the workspace quality gates.
|
||||
|
||||
Translations are original work under `MIT OR Apache-2.0`, written from the meaning of
|
||||
|
||||
+16
-1
@@ -21,7 +21,8 @@ commands:
|
||||
layers enforce the crate dependency layering (plan/architecture.md §3)
|
||||
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates
|
||||
test cargo nextest run (cargo test without nextest) plus the doctests
|
||||
ci fmt --check, clippy -D warnings, test, assets, layers, wasm (stops at first failure)
|
||||
i18n print the strings each interface language shows in English (never fails)
|
||||
ci fmt --check, clippy -D warnings, test, i18n, assets, layers, wasm (stops at first failure)
|
||||
corpus [--download]
|
||||
show where test corpora live; --download fetches PngSuite into corpus/pngsuite
|
||||
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
|
||||
@@ -38,6 +39,7 @@ fn main() -> ExitCode {
|
||||
Some("layers") => cmd_layers(),
|
||||
Some("wasm") => cmd_wasm(),
|
||||
Some("test") => cmd_test(),
|
||||
Some("i18n") => cmd_i18n(),
|
||||
Some("ci") => cmd_ci(),
|
||||
Some("corpus") => cmd_corpus(rest.contains(&"--download")),
|
||||
Some("stats") => stats::run(&root(), rest.contains(&"--exact")),
|
||||
@@ -167,6 +169,18 @@ fn cmd_test() -> Result<(), String> {
|
||||
run(c, "cargo test --workspace --doc")
|
||||
}
|
||||
|
||||
/// The translation gap report (`cadcraft-ui-egui`'s ignored `i18n::tests::translation_gaps`).
|
||||
/// A missing translation shows in English and never fails a build, so neither does this step.
|
||||
fn cmd_i18n() -> Result<(), String> {
|
||||
let mut c = cargo();
|
||||
// `--workspace` builds the same test binaries as the test step; `-p` alone would rebuild them.
|
||||
c.args(["test", "--workspace", "--lib", "-q", "--", "--ignored", "--exact", "i18n::tests::translation_gaps", "--nocapture"]);
|
||||
if let Err(e) = run(c, "cargo test --workspace --lib -q -- --ignored --exact i18n::tests::translation_gaps --nocapture") {
|
||||
eprintln!("note: the translation gap report did not run: {e}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_ci() -> Result<(), String> {
|
||||
type Step = (&'static str, Box<dyn Fn() -> Result<(), String>>);
|
||||
let steps: Vec<Step> = vec![
|
||||
@@ -187,6 +201,7 @@ fn cmd_ci() -> Result<(), String> {
|
||||
}),
|
||||
),
|
||||
("test", Box::new(cmd_test)),
|
||||
("i18n", Box::new(cmd_i18n)),
|
||||
("assets", Box::new(|| assets::run(&root()))),
|
||||
("layers", Box::new(cmd_layers)),
|
||||
("wasm", Box::new(cmd_wasm)),
|
||||
|
||||
Reference in New Issue
Block a user