Canonical vocabulary for Wurstfinger. Written primarily for LLM agents working in this repo, but binding for humans too.
Scope: new and changed code only. This file is not a TODO list, and nothing here justifies a rename in code you were not already modifying — the legacy names it once grandfathered have since been brought in line, one family per dedicated change. When code and this file disagree, the code wins for reading; this file wins for writing.
How to use it: before naming a type, parameter, or test — and before writing a doc comment — check whether the concept already has a canonical term here. Use that term and nothing else. What the codebase still spells the old way is measured, not guessed: it is listed in The backlog, which is generated from the sources and is now empty, so a name that contradicts this file is a defect rather than a counter-example.
How it is enforced. The machine-checkable half of this document — the vocabulary, the
rejected spellings, the shape rules — lives in glossary.toml.
scripts/check_naming.py reads it and reports every identifier that contradicts it;
.naming-budget.json recorded the backlog per file, could shrink but never grow, and was
deleted when it reached zero — so the check now allows no rejected spelling anywhere. The
same source generates the glossary_* rules in .swiftlint.yml, so a rejected spelling is
flagged in Xcode as you type it, in every file, since no rule carries exclusions any more.
Sections 7, 8 and
9 below are generated from those two files — edit glossary.toml and run
render, not this document.
python3 scripts/check_naming.py list <path> # what a file carries
python3 scripts/check_naming.py check # what CI runs
python3 scripts/check_naming.py update # record a renameWhere this file does not decide something, the Swift API Design Guidelines do: clarity at the point of use, no abbreviations, names by role rather than by type.
These five are the ones that actually cause wrong code. Read them first.
A mode is one keyboard state with its own key set and arrangement: main, shifted,
capsLock, numeric, symbols, emoji (ModeNames in
Definition/Language/KeyboardDefinition.swift). The type is KeyboardMode, the action is
.switchMode(name).
"Layer" is the non-canonical spelling of the same thing — never introduce it in new code, comments, or test names for a keyboard state.
The word itself is not banned: "the definition layer", "the view layer" and "layered on
top" are the architectural and ordinary English senses, and they stay. Only the sense
that means a keyboard state is wrong, and that one is always replaceable by mode
without changing the sentence. KeyboardMode+Shifted.swift
currently manages both words in one sentence; that is the bug this rule prevents.
Do not confuse KeyboardMode (keyboard state) with SwipeMode (which directions a
single key accepts: .eightWay, .fourWayCross, …). Different axes, same word — always
qualify which one you mean.
| Usage | Meaning |
|---|---|
UtilitySlot.return, KeyAction.newline |
the Return/Enter key |
KeyBinding.returnAction, isReturn, returnOverrides, ReturnSwipeResolver |
the return-swipe gesture — swipe out and back to the start |
Swift return |
the language keyword |
returnAction is the output of a return swipe, not the action of the Return key.
When writing prose, always say "return swipe" in full; never abbreviate it to "return".
A slot is a named position in the layout. A key id is the string identifying a
KeyDefinition. They are the same value: KeyDefinition.id is documented as "Semantic
slot name", and KeyPlacement.keyId references it.
- The names live in
GridSlot(topLeft…bottomRight,zero) andUtilitySlot(globe,delete,return,space,symbols). - The variable/parameter name for such a string is
keyId(established API surface, e.g.GestureResolver.resolve(keyId:gesture:in:)). slotIdis a rejected alias with no sites left. The SwiftLint ruleglossary_slot_idnow covers every file, so writing it is a build error.
Use "slot" when talking about the position ("the topLeft slot"), "key" when talking
about the thing bound to it ("the topLeft key commits q").
KeyStyle (.primary, .utility, .spacebar, …) and KeyboardStyle (.classic,
.liquidGlass) are appearance. NumpadType (digit order) and CursorMovementType
(drag behavior) are not visual and therefore carry …Type.
For new code: …Style = how it looks, …Type = a closed set of behavioral variants (as in
GestureType, SlideType). Never …Mode for either — that word belongs to keyboard
state.
Two classification enums over the same keys on different axes. Both are legitimate; picking the wrong one is a silent behavior bug.
| Axis | Cases | |
|---|---|---|
KeyCategory |
runtime behavior: auto-shift, haptics, hint styling | letter, digit, symbol, compose, modifier, utility, whitespace |
LabelCategory |
render-time visibility (the "hide labels" settings) | letter, standardSymbol, extraSymbol, number, functional |
Note the vocabulary clash: the same concept is digit in one and number in the other.
That two enums named …Category classify the same keys is the underlying smell. A new
classification enum must name its axis, not just the word "category" — the reader has
to be able to tell from the name alone which of several classifications applies.
Before renaming anything, ask who owns the name. Every term in glossary.toml records it.
-
Zone A, free. Types, properties, parameters, locals, test names. Nobody outside the code sees them, so renaming is pure refactoring.
-
Zone B, migratable. Anything read back from disk. The Swift spelling and the stored spelling are two different names: rename the property and pin the old string —
case areLetterLabelsHidden = "hideLetters"— or ship a migration. That line is real: the four label-visibility and long-press flags were renamed exactly that way, and the strings on their cases are the persisted keys. The same divergence exists from the other side, where the type isNumpadTypewhile the stored key staysnumpadStyle; both are correct rather than drift.Name Note SettingsKeycases24 cases; four carry a pinned rawValue after their rename, the other 20 rely on the implicit one — there the case name is the stored key, so a rename orphans the setting unless the old string is pinned. AppSettingsKeycases3 cases, all with explicit rawValues ( onboarding.*). Here the case names are free and the strings are the contract.gesture.*The tuning keys in GesturePreprocessor, written as string literals.KeyboardHealthLog.EntrypropertiesCodablewith noCodingKeys, written to a JSON file in the app group — the property names are the on-disk field names.Language ids ( de_DE,ja_JP_katakana, …)Persisted in selectedLanguageId,enabledLanguageIds,pinnedLanguageId. -
Zone C, contract. Names that leave the codebase. Renaming one costs somebody something outside this repository, so it is a release decision, not a refactoring:
Name What a rename costs Localizable.xcstringskeysThe English source string is the key. Rewording a UI string re-keys the entry and drops its translations — 156 keys across 22 languages. A case-only difference is a separate key, and has collided once already ( Switch Keyboardvs.Switch keyboard).accessibilityIdentifiervaluesThe UI tests query keys by slot id; renaming a slot renames the test hook with it. group.de.akator.wurstfinger.sharedThe app-group suite. Renaming it orphans every setting on every installed device. FORCE_LANGUAGE,FORCE_LAYER,SCREENSHOT_MODE,GENERATE_SCREENSHOTS, …Read by scripts/generate-screenshots.shand the workflows. They move together with those files or not at all — which is whyFORCE_LAYERstill says "layer", and why its values arelower,upper,numbers,symbolsrather than the mode names they map to.ISOLATED_DEFAULTSDeclared in SharedDefaultsand hand-copied intoUITestApp, because the UI-test target cannot import the app module. Renaming one side compiles cleanly and silently stops isolating the test defaults.
The checker only sees zone A and the Swift half of zone B. Zone C is documented here because no rule can catch it: it fails in the App Store, not in CI.
| Term | Type | Means |
|---|---|---|
| definition | KeyboardDefinition |
the complete declarative description of one keyboard: all modes, keys, bindings, arrangements |
| mode | KeyboardMode |
one state of a definition (main, shifted, numeric, …) with its own keys + arrangement |
| key | KeyDefinition |
one key: id, bindings, swipeMode, slideType, style, tapCycleActions |
| binding | KeyBinding |
what one gesture on one key does: label, action, category, returnAction, accessibilityLabel |
| action | KeyAction |
the command enum — commitText, compose, cycleAccents, switchMode, capitalizeWord, advanceToNextInputMode, dismissKeyboard, switchToNextLanguage, deleteBackward, deleteForward, space, newline, moveCursor, copy, paste, cut, cutAll, none |
| label | KeyBinding.label |
the text drawn on the key. May differ from the output ("⇧" for shift) and is empty for icon-driven utility keys |
| compose | ComposeRuleSet (here), ComposeEngine (in Runtime/Compose/) |
table-driven character composition (' + a → á) |
| input method | InputMethodType |
stateful transformation of committed text: direct, telex, hangul |
| descriptor | LanguageDescriptor |
the authoring handle for a language: cheap metadata plus a builder that materializes the definition on demand. LanguageDefinitions declares languages as these |
| metadata | LanguageMetadata |
the value object a language is passed around as: id, title, localeIdentifier, and a locale derived from it. No builder, so it is constructible anywhere — which is why it is a separate type from the descriptor |
| Term | Type | Means |
|---|---|---|
| slot | GridSlot, UtilitySlot |
semantic name of a position; the string that is also a key id |
| arrangement | GridArrangement |
which keys sit where, at what size, for one ArrangementContext |
| arrangement context | ArrangementContext |
the situation an arrangement applies to: portrait, portraitUtilityLeft, landscape, landscapeUtilityLeft |
| placement | KeyPlacement |
one key's entry in an arrangement: keyId + widthMultiplier + heightMultiplier |
| cell | SolvedCell |
a computed grid rectangle, output of GridLayoutSolver. "Cell" is always a result, never a declaration — declare with slot/placement, compute into cells |
| metrics | KeyboardLayoutMetrics (in Settings/) |
resolved pixel geometry (heights, insets) for the current device and orientation |
| Term | Means |
|---|---|
| gesture | one recognized input, typed as GestureType: tap, the eight swipe… directions, circularClockwise / circularCounterclockwise, longPress |
| swipe | a directional gesture from the key center. Qualify direction; never say "swipe" for a slide |
| return swipe | out-and-back to the start position; fires KeyBinding.returnAction. Always two words |
| slide | a sustained drag on a held key, typed as SlideType (none, moveCursor, delete; declared in Definition/Model/) and phased as SlidePhase (began, changed(deltaX:), swipeUp(isReturn:), ended, tap, cancelled). Never call this a swipe |
| ghost key | a binding inherited from a fallback mode when the active mode leaves that gesture unbound (GhostKeyResolver) |
| trail | the visual stroke drawn behind the finger (GestureTrail*). Purely cosmetic — it never resolves to an action |
| Term | Means |
|---|---|
| resolver | GestureResolver — maps (keyId, gesture, mode) to a KeyBinding?. Returning nil delegates to the next resolver |
| resolver chain | GestureResolverChain — priority-ordered resolvers; first non-nil wins, else KeyAction.none |
| middleware | ActionMiddleware — process(_ context:next:); may mutate the context, call next, or short-circuit |
| action pipeline | ActionPipeline — the ordered middleware chain an action runs through |
| context | ActionContext — the mutable state carried through the pipeline |
| target | TextInputTarget (declared in Definition/Model/) — the abstraction all text manipulation goes through. Production implementation is DocumentProxyTarget wrapping UITextDocumentProxy; tests substitute a mock |
| Term | Means |
|---|---|
| settings | user-changeable, persisted values (LayoutSettings, HapticSettings, LanguageSettings) |
| constants | compile-time values that users cannot change (KeyboardConstants) |
| shared defaults | the app-group UserDefaults bridging host app and extension (SharedDefaults) |
| Suffix | Reserved for | Example |
|---|---|---|
…Definition |
complete declarative data | KeyboardDefinition |
…Descriptor |
lazy handle: metadata + a builder that materializes the real thing | LanguageDescriptor |
…Metadata |
the cheap descriptive fields of a thing, without the thing itself | LanguageMetadata |
…Configuration |
injected parameters of a runtime component | SlideGestureConfiguration |
…Settings |
user-changeable, persisted | LayoutSettings |
…Metrics |
computed geometry | KeyboardLayoutMetrics |
…Style |
visual appearance | KeyStyle |
…Type |
closed set of behavioral variants | GestureType, SlideType |
…Mode |
keyboard state — reserved, do not use for anything else | KeyboardMode |
…Category |
classification; the prefix must name the axis | KeyCategory |
…Resolver / …Middleware |
pipeline participants | GhostKeyResolver |
…Factory / …Registry |
build / look up + cache definitions | GridKeyboardFactory |
Spell suffixes out: …Configuration, not …Config. Apple's own APIs are consistent about
this (URLSessionConfiguration, WKWebViewConfiguration), and the Swift API Design
Guidelines rule out abbreviations. No …Config type is left in the tree, and the
config_suffix rule keeps it that way.
Do not introduce …Manager or …Helper. Both describe no responsibility; name the type
after what it actually does.
| Verb | Reserved for |
|---|---|
resolve… |
resolver-chain lookups returning an optional |
process… |
middleware entry points |
handle… |
view-model entry points reacting to user input |
make… |
pure factory returning a value (makeDefinition) |
build… |
assembling a larger structure in place (buildMode) |
make… is the factory verb — do not introduce create… alongside it.
These are not domain-specific, but they are where new code drifts most.
- Booleans read as assertions:
is…,has…,should…(isSliding,shouldCapitalize). A settings flag isisSomethingEnabled, notenableSomethingorhideSomething— the latter read as commands.areLetterLabelsHiddenandisLongPressDigitEnabledwere spelledhideLettersandlongPressNumbersEnableduntil they were renamed; the stored keys still are. - Tests carry no
testprefix. All@Testfunctions are named as the assertion they make:symbolsKeySwitchesToNumeric(),allLanguagesHaveUniqueIds(). Write the expected behavior as a sentence; the@Testattribute already says it is a test. Exception: the XCTest-based UI tests (wurstfingerUITests) must keep thetestprefix — XCTest discovers test methods by that prefix, so a UI test without it silently never runs. - No abbreviations beyond
id,min,max, and the…Idxloop-index locals already in use. - Name parameters by role, not by type:
keyId: String, notstring: String.
Generated from glossary.toml; scripts/check_naming.py enforces every row.
| Avoid | Write instead |
|---|---|
| config suffix | …Configuration for injected runtime parameters, …Definition for declarative data |
| create prefix | make… |
GesturePreprocessorConfig |
GesturePreprocessorConfiguration |
hideExtraSymbols |
areExtraSymbolLabelsHidden |
hideLetters |
areLetterLabelsHidden |
hideStandardSymbols |
areStandardSymbolLabelsHidden |
KeyboardInfo |
LanguageMetadata |
KeyConfig |
KeyDefinition |
| kind suffix | …Type |
LanguageConfig |
LanguageMetadata |
| layer word | mode |
longPressNumbersEnabled |
isLongPressDigitEnabled |
ScreenshotConfig |
ScreenshotConfiguration |
ScreenshotMode |
ScreenshotType |
showLanguageLabel |
isLanguageLabelShown |
slotId |
keyId |
| vague type suffix | a name that says what the type does |
The rules a checker cannot see, because they are about which word describes a thing rather than how an identifier is spelled.
| Avoid | Write instead |
|---|---|
| "layer" for a keyboard state | "mode" |
| "return" for the gesture | "return swipe" |
| "swipe" for a held drag | "slide" |
| "number" for the 0–9 keys | "digit" — except inside LabelCategory, where the case is named number |
What the codebase still spells the old way, counted from the sources. It is empty:
every rejected spelling has been retired, .naming-budget.json is gone, and the check
allows no rejected spelling anywhere. Should a term added later find the tree already
spelling it the old way, update writes the file again and freezes the new counts per
file: they may then shrink, never grow — and a shrink has to be recorded with update, or
the slot it frees stays open for the next rejected name. Do not rename a family in
passing — each comes out in its own dedicated change, so a rename you did not sign up
for never lands in your pull request.
The backlog is empty: no source file carries a rejected spelling.
Comments are not counted — the checker reads identifiers only — so nothing prose says can raise that count. The keyboard-mode sense of "layer" is gone from the comments too; what is left is the architectural sense, which is the ordinary English word and not the rejected one: nine lines about the definition layer, the view layer, and the order of the hint layer.
Where a rejected spelling is the correct name after all. Each one needs a reason; without it the checker's findings stop being trustworthy.
No waivers are recorded.