forked from pre-commit/pre-commit-hooks
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheck_fluent.py
More file actions
137 lines (109 loc) · 4.32 KB
/
check_fluent.py
File metadata and controls
137 lines (109 loc) · 4.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
from __future__ import annotations
import argparse
from collections.abc import Sequence
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check.')
args = parser.parse_args(argv)
retval = 0
for filename in args.filenames:
try:
with open(filename, encoding='UTF-8') as f:
content = f.read()
if not _validate_fluent_syntax(content, filename):
retval = 1
except (OSError, UnicodeDecodeError) as exc:
print(f"{filename}: Failed to read file ({exc})")
retval = 1
return retval
def _validate_fluent_syntax(content: str, filename: str) -> bool:
"""Validate Fluent FTL file syntax."""
lines = content.splitlines()
errors = []
# Track current message context
current_message = None
has_default_variant = False
in_select_expression = False
for line_num, line in enumerate(lines, 1):
# Skip empty lines and comments
if not line.strip() or line.strip().startswith('#'):
continue
# Check for message definitions (identifier = value)
if (
'=' in line and
not line.startswith(' ') and
not line.startswith('\t')
):
current_message = line.split('=')[0].strip()
in_select_expression = False
has_default_variant = False
# Validate message identifier
if not _is_valid_identifier(current_message):
errors.append(
f"Line {line_num}: Invalid message identifier "
f'"{current_message}"',
)
# Check for select expressions (contains -> or other select syntax)
if '{' in line and '$' in line and '->' in line:
in_select_expression = True
# Handle indented content (attributes, variants, multiline values)
elif line.startswith(' ') or line.startswith('\t'):
if current_message is None:
errors.append(
f"Line {line_num}: Indented content without "
f"message context",
)
continue
stripped = line.strip()
# Check for attribute definitions
if stripped.startswith('.') and '=' in stripped:
# Remove leading dot
attr_name = stripped.split('=')[0].strip()[1:]
if not _is_valid_identifier(attr_name):
errors.append(
f"Line {line_num}: Invalid attribute identifier "
f'"{attr_name}"',
)
# Check for variants in select expressions
elif stripped.startswith('*') or (
stripped.startswith('[') and stripped.endswith(']')
):
if not in_select_expression:
errors.append(
f"Line {line_num}: Variant definition outside "
f"select expression",
)
elif stripped.startswith('*'):
has_default_variant = True
else:
# Non-* variants don't set has_default_variant
pass
# Check for unterminated select expressions
if in_select_expression and current_message:
if '}' in line:
in_select_expression = False
if not has_default_variant:
errors.append(
f"Line {line_num}: Select expression missing "
f"default variant (marked with *)",
)
# Report errors
if errors:
for error in errors:
print(f"{filename}: {error}")
return False
return True
def _is_valid_identifier(identifier: str) -> bool:
"""Check if identifier follows Fluent naming conventions."""
if not identifier:
return False
# Must start with letter
if not identifier[0].isalpha():
return False
# Can contain letters, numbers, underscores, and hyphens
for char in identifier:
if not (char.isalnum() or char in '_-'):
return False
return True
if __name__ == '__main__':
raise SystemExit(main())