Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d1a3927
Refactor `RulesFor` to use `strum_macros::Display` and remove custom …
moritz-gross Aug 11, 2026
468054a
missing ! before condition (#657)
moritz-gross Aug 11, 2026
1c87cc6
simplify TTS using strum (#667)
moritz-gross Aug 11, 2026
b230d91
Exclude `definitions.yaml` from audits and update related tests and d…
moritz-gross Aug 11, 2026
4096a29
Remove unused `error` field and error handling logic from `SpeechRule…
moritz-gross Aug 12, 2026
ab01a5d
Remove `error` field and related checks from `PreferenceManager` (#669)
moritz-gross Aug 12, 2026
bfb6b07
Refactor `strum` dependency to use `derive` feature and replace `stru…
moritz-gross Aug 12, 2026
ca0e9dd
Recognize lg as an ASCIIMath function (#672)
luantaraschi Aug 12, 2026
42ec87e
regex XML_COMMENT is buggy, and xml comments are ignored by parser an…
moritz-gross Aug 12, 2026
d0c900b
update translation of traditioanl chinese based on english version da…
hjy1210 Aug 12, 2026
9c2a725
fix sqrt rule that emitted no speech for 'square root' if not terse. …
NSoiffer Aug 12, 2026
843e554
Merge branch 'main' of https://github.com/daisy/MathCAT
NSoiffer Aug 12, 2026
2730e7c
Add initial translation for the navigation rules
NSoiffer Aug 12, 2026
ec36e05
add `CommandOffset` instead of using `string-length($Prefix)+1` so th…
NSoiffer Aug 12, 2026
206d728
add some missing s for properties
NSoiffer Aug 13, 2026
9c1bdf6
Added a rule to the nb and sv branches to avoid saying "in" when zoom…
NSoiffer Aug 13, 2026
b8d09a9
Ru - Improve wording of Russian docs (#661)
Kostenkov-2021 Aug 11, 2026
f747bd6
Merge branch 'ru' of https://github.com/Kostenkov-2021/MathCAT into ru
Kostenkov-2021 Aug 13, 2026
ddb740c
ru: handle lone log/ln; suppress nav announcements
Kostenkov-2021 Aug 13, 2026
7724816
Russian braille: bracket/arrow mappings & tests
Kostenkov-2021 Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ sxd-document-no-unsafe = "0.4.2"
sxd-xpath-no-unsafe = "0.5.1"
yaml-rust = "0.4"
# yaml-rust = { version = "0.11", package = "yaml-rust2" }
strum = "0.28"
strum_macros = "0.28"
strum = { version = "0.28", features = ["derive"] }
anyhow = "1.0"
regex = "1.12.3"
dirs = "6.0"
Expand Down
4 changes: 4 additions & 0 deletions PythonScripts/audit_translations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ The tool automatically adjusts its matching logic based on the file type:
* Matches rules based on character/range keys.
* *Examples:* `unicode.yaml`, `unicode-full.yaml` (keys like `a-z`, `!`, `0-9`).

`definitions.yaml` is intentionally excluded from audits *for now*. It does not have the same semantics
as normal rules, so the tool ignores it during automatic file discovery and when it is passed to
`--file`.

---

### ⚙️ Usage & Commands
Expand Down
21 changes: 17 additions & 4 deletions PythonScripts/audit_translations/auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ def get_rules_dir(rules_dir: str | None = None) -> Path:
return package_dir.parent.parent / "Rules" / "Languages"


def is_definitions_file(file_path: str | Path) -> bool:
"""Return if the file name is definitions.yaml, which is not yet supported."""
return Path(file_path).name == "definitions.yaml"


def get_yaml_files(lang_dir: Path, region_dir: Path | None = None) -> list[Path]:
"""Get all YAML files to audit for a language, including region overrides."""
files: set[Path] = set()
Expand All @@ -39,12 +44,13 @@ def collect_from(directory: Path, root: Path) -> None:
if not directory.exists():
return
for f in directory.glob("*.yaml"):
if f.name != "prefs.yaml": # Skip prefs.yaml as it's not translated
if f.name != "prefs.yaml" and not is_definitions_file(f):
files.add(f.relative_to(root))
shared_dir = directory / "SharedRules"
if shared_dir.exists():
for f in shared_dir.glob("*.yaml"):
files.add(f.relative_to(root))
if not is_definitions_file(f):
files.add(f.relative_to(root))

collect_from(lang_dir, lang_dir)
if region_dir:
Expand Down Expand Up @@ -144,7 +150,11 @@ def audit_language(
verbose: bool = False,
source_language: str = "en",
) -> int:
"""Audit translations for a specific language. Returns total issue count."""
"""Audit translations for a specific language and return the total issue count.

``specific_file`` is the relative file path supplied by the CLI's ``--file``
option. When set, the audit is limited to that file.
"""
rules_dir_path = get_rules_dir(rules_dir)

source_base_language, source_region = split_language_into_base_and_region(source_language)
Expand All @@ -168,7 +178,10 @@ def audit_language(
raise AuditError(f"Target region directory not found: {translated_region_dir}")

# Get list of files to audit
files = [specific_file] if specific_file else get_yaml_files(source_dir, source_region_dir)
if specific_file:
files = [] if is_definitions_file(Path(specific_file)) else [specific_file]
else:
files = get_yaml_files(source_dir, source_region_dir)

print_audit_header(language, len(files), source_language)

Expand Down
28 changes: 28 additions & 0 deletions PythonScripts/audit_translations/tests/test_auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,34 @@ def test_get_yaml_files_includes_region(tmp_path) -> None:
assert set(files) == {Path("base.yaml"), Path("SharedRules/shared.yaml"), Path("unicode.yaml")}


def test_get_yaml_files_ignores_definitions(tmp_path) -> None:
"""Definitions files are excluded from automatic audit discovery."""
lang_dir = tmp_path / "lang"
shared_dir = lang_dir / "SharedRules"
shared_dir.mkdir(parents=True)
(lang_dir / "rules.yaml").write_text("---", encoding="utf-8")
(lang_dir / "definitions.yaml").write_text("---", encoding="utf-8")
(shared_dir / "definitions.yaml").write_text("---", encoding="utf-8")

assert get_yaml_files(lang_dir) == [Path("rules.yaml")]


def test_audit_language_ignores_explicit_definitions_file(tmp_path, fixed_console_width) -> None:
"""Passing definitions.yaml through --file produces an empty audit."""
rules_dir = tmp_path / "Rules" / "Languages"
(rules_dir / "en").mkdir(parents=True)
(rules_dir / "de").mkdir(parents=True)

with console.capture() as capture:
total_issues = audit_language("de", specific_file="definitions.yaml", rules_dir=str(rules_dir))
output = strip_ansi(capture.get())

assert total_issues == 0
assert "Files to check: 0" in output
assert "Files checked" in output
assert "definitions.yaml" not in output


def test_list_languages_includes_region_codes(tmp_path) -> None:
"""
Ensures list_languages reports region variants.
Expand Down
4 changes: 2 additions & 2 deletions Rules/Braille/ASCIIMath/definitions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"lambda", "Lambda", "and", "or", "if",
"lim", "Lim",
"sin", "cos", "tan", "arcsin", "arccos", "arctan", "sinh", "cosh", "tanh", "cot", "coth", "sech", "csch", "sec", "csc",
"log", "ln", "abs", "norm", "floor", "ceil",
"log", "lg", "ln", "abs", "norm", "floor", "ceil",
"Sin", "Cos", "Tan", "Arcsin", "Arccos", "Arctan", "Sinh", "Cosh", "Tanh", "Cot", "Sec", "Csc",
"Log", "Ln", "det", "exp", "dim", "mod", "gcd", "lcm", "lub", "glb", "min", "max",
}
Expand All @@ -30,4 +30,4 @@

- UnderAccents: {
"¯": "ul", "⏟": "ubrace",
}
}
26 changes: 26 additions & 0 deletions Rules/Braille/Russian/Russian_Rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1555,6 +1555,32 @@
replace:
- x: "DefinitionValue(., 'Braille', 'RussianFunctionBraille')"

-
name: contextual-square-bracket-recognition-point
tag: mo
match:
- "parent::*[IsBracketed(., '[', ']')] and"
- "((.='[' and not(preceding-sibling::*) and following-sibling::*[1][self::m:mi or self::m:mtext]) or"
- " (.=']' and not(following-sibling::*) and preceding-sibling::*[1][self::m:mi or self::m:mtext]))"
replace:
- T: "⠠"
- x: "text()"

-
name: contextual-angle-bracket-recognition-point
tag: mo
match:
- "parent::*[IsBracketed(., '⟨', '⟩')] and"
- "((.='⟨' and not(preceding-sibling::*)) or (.='⟩' and not(following-sibling::*))) and"
- "(parent::*/*[2][self::m:mi or self::m:mtext] or"
- " parent::*/*[last()-1][self::m:mi or self::m:mtext])"
replace:
- T: "⠈"
- test:
- if: ".='⟨'"
then: [T: "⠪"]
else: [T: "⠕"]

-
name: div-operator-function
tag: mo
Expand Down
6 changes: 6 additions & 0 deletions Rules/Braille/Russian/unicode.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@
- "≢": [t: "⠀⠰⠾"]
- "≤": [t: "⠀⠪⠶"]
- "≥": [t: "⠀⠕⠶"]
- "≶": [T: "⠀⠪⠕⠀"]
- "≷": [T: "⠀⠕⠪⠀"]
- "≪": [t: "⠀⠪⠪⠀"]
- "≫": [t: "⠀⠕⠕⠀"]
- "≺": [t: "⠀⠒⠪⠀"]
Expand All @@ -238,6 +240,10 @@
- "⋰": [t: "⠠⠲⠲⠲"]
- "⋱": [t: "⠠⠲⠲⠲"]
- "△": [t: "⠸⠙"]
- "⟨": [T: "⠪"]
- "⟩": [T: "⠕"]
- "↑": [T: "⠰⠌"]
- "↓": [T: "⠘⠡"]
- "→": [t: "⠀⠒⠕"]
- "⟶": [t: "⠀⠒⠕"]
- "←": [t: "⠀⠦⠶"]
Expand Down
8 changes: 4 additions & 4 deletions Rules/Intent/general.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@
then:
- intent:
name: "logarithm-with-base"
attrs: "data-intent-property='concat(data-intent-property, \":prefix\")'"
attrs: "data-intent-property='concat(data-intent-property, \":prefix:\")'"
children: [x: "*[2]"] # grab the base
else:
- intent:
Expand Down Expand Up @@ -770,7 +770,7 @@
contains(@data-intent-property, ':piecewise:') or
contains(@data-intent-property, ':matrix:') or
contains(@data-intent-property, ':determinant:') or
contains(@data-intent-property, 'system-of-equations:') or
contains(@data-intent-property, ':system-of-equations:') or
contains(@data-intent-property, ':lines:') )
]"
replace:
Expand All @@ -787,7 +787,7 @@
contains(@data-intent-property, ':array:') or
contains(@data-intent-property, ':matrix:') or
contains(@data-intent-property, ':piecewise:') or
contains(@data-intent-property, 'system-of-equations:') or
contains(@data-intent-property, ':system-of-equations:') or
contains(@data-intent-property, ':lines:') )
]"
replace:
Expand Down Expand Up @@ -898,7 +898,7 @@
name: mtd-to-mrow
tag: mtd
match: "($TableProperty = 'piecewise' or $TableProperty = 'system-of-equations' or $TableProperty = 'lines') and
not(contains(@data-intent-property, ':equation-label'))"
not(contains(@data-intent-property, ':equation-label:'))"
replace:
- test:
if: "@data-intent-property"
Expand Down
37 changes: 24 additions & 13 deletions Rules/Languages/en/navigate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,38 +60,40 @@
- pause: "medium"
else:
- test:
# CommandOffset is 1 + length of the English NavCommand stem (Zoom/Move/Read/Describe),
# not the spoken Prefix length — so translations can use a different Prefix word.
- if: "starts-with($NavCommand, 'Zoom')"
then: [set_variables: [Prefix: "'zoom'"]] # phrase('zoom' in to see more details)
then: [set_variables: [Prefix: "'zoom'", CommandOffset: "5"]] # phrase('zoom' in to see more details)
- else_if: "starts-with($NavCommand, 'Move')"
then: [set_variables: [Prefix: "'move'"]] # phrase('move' to next entry in table)
then: [set_variables: [Prefix: "'move'", CommandOffset: "5"]] # phrase('move' to next entry in table)
- else_if: "starts-with($NavCommand, 'Read')"
then: [set_variables: [Prefix: "'read'"]] # phrase('read' to next entry in table)
then: [set_variables: [Prefix: "'read'", CommandOffset: "5"]] # phrase('read' to next entry in table)
- else_if: "starts-with($NavCommand, 'Describe')"
then: [set_variables: [Prefix: "'describe'"]] # phrase('describe' to next entry in table)
then: [set_variables: [Prefix: "'describe'", CommandOffset: "9"]] # phrase('describe' to next entry in table)
- test:
if: "$Prefix != ''"
then:
- x: "$Prefix"
- test:
- if: "substring($NavCommand, string-length($Prefix)+1) = 'In'"
- if: "substring($NavCommand, $CommandOffset) = 'In'"
then: [t: "in"] # phrase(zoom 'in' to see more details)
- else_if: "substring($NavCommand, string-length($Prefix)+1) = 'InAll'"
- else_if: "substring($NavCommand, $CommandOffset) = 'InAll'"
# HACK: '\uF8FE' is used internally for the concatenation char by 'ct' -- this gets "ed" concatenated to "zoom"
then: [t: "\uF8FEed in all of the way"] # phrase(zoom 'out all of the way' to see more details)
- else_if: "substring($NavCommand, string-length($Prefix)+1) = 'Out'"
- else_if: "substring($NavCommand, $CommandOffset) = 'Out'"
then: [t: "out"] # phrase(zoom 'out' to see more details)
- else_if: "substring($NavCommand, string-length($Prefix)+1) = 'OutAll'"
- else_if: "substring($NavCommand, $CommandOffset) = 'OutAll'"
# HACK: '\uF8FE' is used internally for the concatenation char by 'ct' -- this gets "ed" concatenated to "zoom"
then: [t: "\uF8FEed out all of the way"] # phrase(zoom 'out all of the way' to see more details)
- else_if: "substring($NavCommand, string-length($Prefix)+1) = 'Next'"
- else_if: "substring($NavCommand, $CommandOffset) = 'Next'"
then: [t: "right"] # phrase(move to the 'right')
- else_if: "substring($NavCommand, string-length($Prefix)+1) = 'Previous'"
- else_if: "substring($NavCommand, $CommandOffset) = 'Previous'"
then: [t: "left"] # phrase(move to the 'left')
- else_if: "substring($NavCommand, string-length($Prefix)+1) = 'Current'"
- else_if: "substring($NavCommand, $CommandOffset) = 'Current'"
then: [t: "current"] # phrase(who is the 'current' president)
- else_if: "substring($NavCommand, string-length($Prefix)+1) = 'LineStart'"
- else_if: "substring($NavCommand, $CommandOffset) = 'LineStart'"
then: [t: "to start of line"] # phrase(move 'to start of line')
- else_if: "substring($NavCommand, string-length($Prefix)+1) = 'LineEnd'"
- else_if: "substring($NavCommand, $CommandOffset) = 'LineEnd'"
then: [t: "to end of line"] # phrase(move 'to end of line')
- pause: "medium"
- set_variables: [MatchCounter: "$MatchCounter + 1"]
Expand All @@ -103,6 +105,15 @@
(name(.)='mrow' or name(.) = 'mtr' or name(.) = 'mlabeledtr' or @data-from-mathml = 'mtable')"
replace: []

# Prefix/silent intents with no NavigationParts entry (e.g. unary minus, x-hat)
# should not announce "in"/"out of".
- name: into-or-out-of-prefix-or-silent-without-parts
tag: "*"
match: "$Move2D != '' and
(contains(@data-intent-property, ':prefix:') or contains(@data-intent-property, ':silent:')) and
GetNavigationPartName(name(.), count($Child2D/preceding-sibling::*)) = ''"
replace: []

- name: into-or-out-of-mtr
tag: [mtr, mlabeledtr]
match: "$Move2D = 'in'"
Expand Down
Loading
Loading