79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Find UI strings still written in title case.
|
||
|
|
|
||
|
|
M3's style guide: "All text, including titles, headings, labels, menu items, navigation
|
||
|
|
components, app bars, and buttons should use sentence-style capitalization." Product names
|
||
|
|
and branded terms keep their capitals.
|
||
|
|
|
||
|
|
Two things this gets right that the first, grep-based version did not:
|
||
|
|
|
||
|
|
- It allows lowercase articles inside a title-cased phrase, so "Invite a Friend" is
|
||
|
|
caught. Requiring every word after the first to be capitalised missed four strings.
|
||
|
|
- It scans the whole file rather than one line at a time, so a `Text(` whose literal is
|
||
|
|
on the next line is caught. That missed one more.
|
||
|
|
|
||
|
|
Sample data is excluded by name rather than by pattern, because "Steve Biko" and "To Kill
|
||
|
|
a Mockingbird" are title case for the correct reason: they are a person and a book.
|
||
|
|
|
||
|
|
Usage: m3-title-case.py [--list]
|
||
|
|
"""
|
||
|
|
import os, re, sys
|
||
|
|
|
||
|
|
UI = 'composeApp/src/commonMain/kotlin/press/mantra/compose/ui'
|
||
|
|
|
||
|
|
SMALL = {'a', 'an', 'the', 'to', 'of', 'for', 'and', 'or', 'via',
|
||
|
|
'in', 'on', 'at', 'with', 'from', 'by'}
|
||
|
|
|
||
|
|
# People, book titles and other proper nouns used as preview and test fixtures.
|
||
|
|
SAMPLE = {
|
||
|
|
'Steve Biko', 'John Doe', 'Frank Talk', 'Alan Turing',
|
||
|
|
'To Kill a Mockingbird', 'Man With A Plan', 'Woman Of Few Words',
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def offenders():
|
||
|
|
found = []
|
||
|
|
for root, _, files in os.walk(UI):
|
||
|
|
for f in sorted(files):
|
||
|
|
if not f.endswith('.kt'):
|
||
|
|
continue
|
||
|
|
path = os.path.join(root, f)
|
||
|
|
text = open(path, encoding='utf-8').read()
|
||
|
|
for m in re.finditer(r'"([^"$\\]{4,90})"', text):
|
||
|
|
phrase = m.group(1)
|
||
|
|
if phrase in SAMPLE:
|
||
|
|
continue
|
||
|
|
line_start = text.rfind('\n', 0, m.start()) + 1
|
||
|
|
line = text[line_start:text.find('\n', m.start())]
|
||
|
|
if line.lstrip().startswith(('//', '*', '/*')):
|
||
|
|
continue
|
||
|
|
# A log line is not UI copy. `logger.d("Queried Sync")` is written for
|
||
|
|
# whoever is reading logcat, and sentence-casing it would be cargo cult.
|
||
|
|
if re.search(r'\blogger\s*\.\s*[dewiv]\s*\(', line):
|
||
|
|
continue
|
||
|
|
words = phrase.split()
|
||
|
|
if len(words) < 2 or not words[0][:1].isupper():
|
||
|
|
continue
|
||
|
|
later = [w for w in words[1:] if w.lower() not in SMALL]
|
||
|
|
if not later:
|
||
|
|
continue
|
||
|
|
# Title case: every significant word capitalised, and at least one of
|
||
|
|
# them an ordinary capitalised word rather than an acronym like NIP.
|
||
|
|
if all(w[:1].isupper() for w in later) and \
|
||
|
|
any(w[:1].isupper() and w[1:].islower() for w in later):
|
||
|
|
found.append((path, text[:m.start()].count('\n') + 1, phrase))
|
||
|
|
return found
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
found = offenders()
|
||
|
|
print(f' {"Title Case in UI strings":42s} {len(found):6d}')
|
||
|
|
if '--list' in sys.argv:
|
||
|
|
for path, line, phrase in found:
|
||
|
|
print(f' {path.replace(UI + "/", "")}:{line} {phrase!r}')
|
||
|
|
return 1 if found else 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
sys.exit(main())
|