Repository navigation
Add Locale.languages: a choice of languages selects the pool (#753, 3b) - #945
Merged
Merged
Conversation
A Locale carries a choice of BCP 47 tags. parser_for unions the packs' choices and applies it to the base lexicon as a delta against the default's compile: a chosen file's default = false readings join; on a word a chosen file holds a vocabulary reading of, other files' vocabulary readings leave, save mul.toml's; a chosen conjunction drops may_be_initial. Name readings never leave. A tag selects its own file and its ancestors', case-insensitively. A pack choosing no language leaves the vocabulary as it is, and never loads the reading index. The deviation surface of a choice is derived (changed_words) and the non-interference gate reads it; snapshot.py takes --languages. mechanisms.md#LOCALE-PACKS-PURE-DATA's add-only contract becomes selection, with the decisions, RFC, AGENTS.md, locales.rst and release log amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Locale stores each tag in BCP 47's canonical case, so equal choices compare and hash equal; the repr lists at most three tags. - languages has its own mapping message, its offered fix pinned. - Prefer leaves und.toml's readings as it leaves mul.toml's; und holds only name readings today, so no parse moves. - snapshot.py refuses --since with --languages. - The case-mask branch of the delta is tested with a stand-in compile. - The docs say exactly what a choice does to a caller's base entries, the three ways a tag is checked, and what stays under a choice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A choice chained through base= applies on top of the base's rather than unioning with it, and a choice drops the caller's own base entries it removes; one parser_for call choosing both languages is how both readings are kept. Recorded as a decisions entry with the Parser.languages option left open with the configure-by-reading method names, stated in locales.rst (doctests), the parser_for docstring, mechanisms.md and the RFC, and pinned by two tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oice # Conflicts: # AGENTS.md # docs/design/decisions/vocabulary-model.md # nameparser/_reading_index.py # tests/v2/test_layering.py # tests/v2/test_types.py
After the merge with the Reading.languages follow-up: the field keeps naming the files that hold a reading on by default, whatever choice built the parser, since a Parser records no choice; a field that followed the choice waits with Parser.languages. Recorded in the 3b decisions entry (superseding the follow-up's expectation that 3b would change languages_of's filter) and pinned by a test. The no-choice index test now holds what 3b owns: building a parser without a choice loads no index, a parse loading it since the follow-up. Tests cover the canonical case of numeric and variant subtags and a choice that changes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 3b of #753: a choice of languages selects which of the default vocabulary's readings compile. No pack ships here (3d is the first). A caller who chooses no language parses exactly as before.
The API as shipped
Locale.languages, afrozenset[str]of BCP 47 tags, empty by default (decided in Phase 3: language packs that prefer or restrict their language #753, item 1). Each tag is stored in BCP 47's canonical case (PT→pt,pt-br→pt-BR), so equal choices compare and hash equal. It is markedADDED_IN = "3.0.0", so aLocalepickled before this field loads choosing nothing. It is validated by shape: a bare string, bytes or a mapping raiseTypeError, and a malformed tag raisesValueError. It is compared, hashed and shown in the repr: by its tags up to three (Locale('pt': languages pt)), by its size past that (Locale('xx': languages: 4)). Reprs are bounded (AGENTS.md); a pack chooses one or two languages, so the tags are what a reader needs, and a long choice is not a pack's.parser_forunions the packs'languagesinto one choice and applies it to the base lexicon before any fragment is unioned, so the order of the packs doesn't matter. It's applied as a delta against the default's compile. It adds what the choice turns on and removes what the choice drops from the base, a caller's own listing of a dropped word included:parser_for(EN, base=Parser(lexicon=Lexicon.empty().add(particles={'do','van'})))keeps onlyvan. Entries the choice doesn't touch stay. How a choice combines with a base is decided (below). A tag that no file holds (ormul/und) raisesValueError, wrapped with the pack's code (rules/D2, construction raises, parse never does).Lexicon.default().add(particles_ambiguous={'vd'})underenraised. An error the Lexicon still raises while applying the choice is wrapped and names the languages chosen (rules/D2, construction raises, parse never does; recorded in decisions/D2).LexiconandParsergain no field. The selection lives in the private_reading_index(selected,choose,compile_choice,changed_words).Semantics (
_reading_index.choose)default = falsereadings join the pool.mul.toml(every language's) andund.toml(no known language's). I chose to exemptundas well asmul. It holds only name readings today, so this moves nothing. Name readings never leave and never settle anything (decision 15).may_be_initial(decision 20). Underpt, a one-caseereads exactly as a lowercaseein mixed case does, in every comma shape and under all threename_orders.pt-brselectspt-BRandpt). A region with no file selects its language's file (pt-PTselectspt). No macrolanguage table is read.Measured (each tag chosen alone, against the default's readings)
Committed corpora,
snapshot.py --languages TAG, entries moved (names held only bycorpus_cldr.jsonlin brackets):do,e,srabd,do,ra,sr,vdi,raesth,srabdLocal corpora (
--local SOURCE --base . --languages TAG):e(LIMA E SOUZAbecomes the family).do, which loses its particle.Abdnames, whose Arabic bound given name leaves for the English credential.Choosing
enalone is the costliest choice, since English holds the credentials.No-choice proof:
test_snapshot.pypasses unchanged.measure.py --base <master bd02d68f>reports no move in any committed or local check. A subprocess test shows that building a parser without a choice never loads the reading index. Since #944, a parse loads the index anyway, forReading.languages, so the test now holds only what 3b owns. After merging master (#944), the snapshot test still passes.The derived deviation surface is
changed_words.test_non_interference_each_choice(pt, en, cs, de, pt+en) requires the default's reading for every corpus name that holds none of those words. It has a negative control.Decided
A choice is one call's. This was decided 2026-10-29 and is recorded in decisions/vocabulary-model's chained-choices entry. A
Parserrecords no choice, so a choice chained throughbase=applies on top of the base's choice; it is not combined with it:parser_for(EN, base=parser_for(PT))readsMaria do Carmoas familyCarmoandJohn Smith DOas familyDO.parser_for(PT, EN), one call, is how both languages are chosen, and it keeps both readings (do Carmo, suffixDO).parser_for(EN, base=Parser(lexicon=Lexicon(particles=frozenset({'do','van'}), ...)))keeps onlyvan.Two tests in
tests/v2/test_parser.pypin both behaviors, so a later change has to be deliberate.docs/locales.rst("Choosing in one call", with doctests), theparser_fordocstring, mechanisms.md and the RFC all state it. Still open, and decided together with the configure-by-reading method names: aParser.languagesfield recording the choice, so that chained choices combine. It would be pickle-safe, because Parser's pickle guard honorsADDED_IN.Reading.languagesdoes not follow a choice. It names the files that hold the reading on by default, whatever choice built the parser, as #944 defined the field. That decision supersedes #944's expectation that 3b would changelanguages_of's filter. Two readings show the cost:pt, the one-caseEinANA SILVA E SOUZAreadsconjunctionand lists('it', 'pt'), although prefer droppedit's reading.cs, thest.inJiří Dienstbier st.readsgenerationand lists(), althoughcsturned that reading on.A parse can't know its choice, because a
Parserrecords none (decision above). A field that followed the choice therefore waits onParser.languages.test_reading_languages_name_the_default_files_under_a_choicepins both readings.Open questions
Turning on also prefers.
deturns on its generationi, so Catalan's and Polish's conjunctionileave underde(32 committed Catalan names move). This follows the rule as agreed. Whetherde'sishould be a reading at all is the shipping pack's question.For the pack PRs, left as is:
esturns on suffixH(Perez, Juan H→ suffixH, unreported).de'si(Carod i Rovira, Josep→ suffixi)._apply_choicehandles only words that leave a base set. A word a choice joins to a marker set while its base set is unchanged is not reconciled; no shipped choice does that.Docs amended (same-PR rule)
docs/locales.rst"Choosing languages" (doctests)Refs #753.
🤖 Generated with Claude Code