Lines B, D and H of docs/curated-to-mantra.md, pulled from the Curated fork this
morning, go again this afternoon: the group's nostr profile (kind 0), its four relay
lists (NIP-65, NIP-17, NIP-50, NIP-51), its posts (kind 1), the curated schemas it
publishes (31889), the suggestions it reads (31888) and the entries it accepts
(31890). Eighty-two files, twelve screens, the `nostr/curated/` package, the readings
(`GroupNostrProfile`, `GroupRelayList`, `GroupPost`, `GroupCuratedSchema`,
`GroupCuratedEntry`, `CuratedSuggestion`), the `applyInnerEvent` arms for all eight
kinds, the `ChatRepository` and `NostrRepository` reads that fed them, 226 strings,
and every test that came with them. A translation collective has no list to curate
and no reason to describe itself under a key no member controls, and five rows
saying so on every group's screen were five rows about somebody else's product.
**The pasted-event proposal goes with them, because it was theirs.**
`GroupEventProposal.ACCEPTED_KINDS` was exactly kinds 0, 1, the four relay-list
kinds and 31889 -- "the kinds the group's screen has a place for", in its own words --
so with the five rows gone it would have refused every paste. Keeping it as a
generic "sign any event" was considered and rejected: the screen's whole argument
was that a member goes to the row to see the event landed, and there is no row. The
`ProposedEvent` summaries for the same kinds go too; the one test of its pre-existing
fallback ("Event of kind N") is kept, as the only line of `ProposedEventTest` that
was about code this repository had before the pull.
**Broadcast stays, and nothing opens it.** `BroadcastGroupSignedEventScreen`, its
route, view model, state and `BroadcastButton` are kept, on the decision that a way
to send a group-signed event to relays is worth having against the day something
wires a button in -- the button's only call sites were the five removed screens.
Two things had to change for it to compile against a tree with no relay lists:
`defaultRelaysFor(kind)` is now the app's own publish set for every kind, since the
General list it preferred, the Blocked list it filtered by and the schema relays it
widened to no longer exist; and `relayUrlOrNull` moves into the view model's
companion from the deleted `GroupRelaySet`, unchanged. The hint on the screen says
"the relays this app publishes to" rather than "where the group has said it lives".
Its two tests are rewritten around the new seed: the screen test answers for the
first two seeded relays and counts the rest as asked, and empties the list by hand
to see the empty state, since the seed always has something in it. Deleting
broadcast outright -- the tidier tree -- was the recommendation and was declined.
**Every pre-existing file is back at its pre-pull content plus the kept lines'
hunks, and nothing else.** Eleven files -- `ChatMessage.kt`, `NostrEventDao.kt`,
`NostrEvent.kt`, `LocalChatRoom.kt`, `Member.kt`, `ProposedEvent.kt`,
`ChatTranscript.kt`, `ProfileAvatar.kt`, the group screen's view model and state,
and `m3-title-case.py` -- were touched by no kept commit and are restored from
ba0830a3 byte for byte, so `inComparableGroups` is a private helper of the group
screen again rather than a shared extension one deleted screen needed, and
`ProfileAvatar` has one overload again. The rest were restored and had the kept
hunks re-applied: the back button's three on `ChatRoomDetailScreen`, broadcast's
`groupSignedEvent` read on the two chat repositories, and on the two nostr
repositories the sign-in reads, by reverse-applying the queue commit's hunk. The
check is `git diff ba0830a3 -- <file>`, which shows only those. Reverting the eight
commits was rejected because the back button and the read-only identity work landed
on top of them and would have conflicted in every one of the shared files; editing
the current files by hand was rejected because it leaves residue that a diff against
the base cannot distinguish from a decision.
**The strings that went are exactly the ones nothing references any more and that
the pull added.** Six strings were unreferenced before the pull and stay; the header
sentence and one capitalised "Mantra" that the queue commit's join carried are prose,
not feature, and stay too. The seven section comments that described removed blocks
go; the broadcast block's stays.
**The plan's third decision said "hide the two rows behind a constant".** That
covered the curated rows and not the profile, relays and posts beside them, and the
call was to take the whole identity block out. The record of what went and what
stayed is the paragraph after the built table in docs/curated-to-mantra.md, with the
seam it leaves for the next pull: an upstream commit that touches the identity block
conflicts at `ChatRoomDetailScreen`, `MantraNavHost` and `strings.xml`, and is
dropped. docs/README.md says the same in a sentence. The nsec and npub notes still
name `EditGroupCuratedSchemaViewModel`; they are records of what was built upstream
and are left as written.
Verified with :composeApp:compileDebugKotlinAndroid, :composeApp:compileKotlinJvm,
:composeApp:jvmTest (826 tests, from 1,039), :composeApp:testDebugUnitTest (413,
from 530) and :composeApp:m3Audit, every budget met, 12 adaptive uses at the floor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
4.4 KiB
Python
Executable File
100 lines
4.4 KiB
Python
Executable File
#!/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'
|
|
CATALOGUE = 'composeApp/src/commonMain/composeResources/values/strings.xml'
|
|
|
|
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 = []
|
|
# The catalogue, first. When phase 4 moved 364 strings out of composables it moved
|
|
# them out of this checker's reach at the same time -- it scanned .kt files only, so
|
|
# it went on reporting zero while "%1$s Key Packages" and three surviving mentions of
|
|
# the old product name sat in strings.xml. Externalising narrows what a source scan
|
|
# can see; the check has to follow.
|
|
if os.path.exists(CATALOGUE):
|
|
for m in re.finditer(r'<string name="([^"]+)">([^<]*)</string>',
|
|
open(CATALOGUE, encoding='utf-8').read()):
|
|
phrase = m.group(2)
|
|
if phrase in SAMPLE:
|
|
continue
|
|
words = [w for w in re.sub(r'%\d+\$s', '', phrase).split() if w]
|
|
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
|
|
if all(w[:1].isupper() for w in later) and \
|
|
any(w[:1].isupper() and w[1:].islower() for w in later):
|
|
found.append((CATALOGUE, m.group(1), phrase))
|
|
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())
|