Titles, dates & status
Title Design
Titles are intentionally more structured than ordinary text fields.
KizunaShelf exposes:
| Entity property | Purpose |
|---|---|
entity.title | Language-agnostic fallback title, used in search and wherever no viewer language applies. |
entity.titles | Title map keyed by language, used for language switching, subtitle display, and search. |
There is no defaultTitle flag. The displayed title is chosen by the viewer's language (see Language preference below). Each surface resolves a title as:
- The viewer language's title —
entity.titles[language] - Otherwise
entity.title(the language-agnostic fallback below)
The core computes entity.title as the fallback, in this order:
- The
titleRole: originaltitle field, when present - Otherwise the first configured
fieldType: titlefield - Otherwise the first value in
entity.titles(e.g. afilename.titleLanguagebasename) - Otherwise the filename basename
So the effective resolution is selected language → original → other titles.
Language preference
Each client holds one per-device language preference (not in the vault config — it's a viewer choice, so it lives in web localStorage / iOS UserDefaults, sourced per runtime). Everything language-sensitive derives from that single value:
- UI language — the app chrome is translated into English, Japanese, Simplified Chinese, and Traditional Chinese. A preference outside that set falls back to the English UI (titles still follow the preference). Which languages a client's UI ships is a per-client fact, not something the core reports —
GET /api/languagesreturns the selectable options (userLanguages:code, endonymlabel,titleLanguage), and each client layers "is my UI translated into this" on top. - Title/content language — the preference's bare primary subtag (
zh-Hans→zh) keysentity.titles[language]. Title languages and the schema'stitleLanguageare always bare ISO codes — script subtags never entertitlesmaps,titleLanguageconfig, or the dedup index. - Provider request language — the raw preference (which may carry a script subtag) is sent to external providers on search / quick-add / episode fetch, so a provider that distinguishes Simplified vs Traditional (TMDB, Steam) can localize; the core normalizes the subtag per provider (TheTVDB collapses to one Chinese bucket, TMDB maps
zh-Hant→zh-TW, and the Apple providers map every preference to its iTunes storefront —ja→jp,zh-Hans→cn,zh-Hant→tw— since the storefront picks both result relevance and metadata language).
Simplified vs Traditional Chinese. The picker offers zh-Hans (简体中文) and zh-Hant (繁體中文) as distinct UI languages, but both map to the single title language zh. A vault therefore has one zh title bucket, not two: whichever script a provider returned (or the user typed) is what's stored, and every Chinese viewer sees that stored script. This is a deliberate simplicity trade-off — keeping two Chinese title fields per type would burden every user to avoid an occasional script mismatch.
Server responses (ApiError messages, etc.) are not localized — clients surface them in English. Only the schema-derived, data-driven labels (type/field names, headings) and the client UI strings are translated.
Type presets are the deliberate exception. The built-in presets seed the schema — type labels, folder paths, field display names, status values, suggested list titles, daily-note hashtags — and that text becomes user data the user reads forever, so it must arrive in their language. The preset registry ships every string in en/ja/zh-Hans/zh-Hant, and the picker offers one language choice (a user-language preference code, defaulting to the app's preference) that drives both derivations: its bare primary subtag is stamped as the title/filename/season language, and it selects the seeded text. A language the presets aren't written in still stamps its titles — ko gets Korean titles with English labels (the picker says so explicitly). Script subtags affect text glyphs only: a zh-Hant choice seeds 繁體 labels while the stamped titleLanguage and every stored key remain bare zh. The choice also picks the provider wiring where sources are language-bound: Bangumi (Chinese name_cn, Japanese-original name, Chinese summaries) leads the search priority and feeds titles for Chinese vaults, feeds Japanese originals for Japanese vaults, and drops to last (covers and links only) everywhere else, where TMDB/TheTVDB/MAL localize per request instead. GET /api/type-presets takes the same language to localize the picker metadata.
All configured title fields are title data:
fields:
- field: title
fieldType: title
titleLanguage: zh
- field: title_en
fieldType: title
titleLanguage: en
- field: title_original
fieldType: title
displayName: Title (Original)
titleRole: original
Title keys are chosen like this:
| Field config | entity.titles key |
|---|---|
titleLanguage: en | en |
no titleLanguage | raw field key, for example title_original |
filename.titleLanguage: zh | zh |
This means a titleRole: original field is still a title even if it is not language-specific. It appears in entity.titles under its field key and the UI can label it using displayName.
Dates and Calendar Design
Dates are not just display fields. They drive:
- date badges in entity cards
- the entity detail Dates panel
- calendar views
- planning and completion views
- timeline analytics
Use dateRole to tell KizunaShelf what kind of date a field represents:
- field: release_date
fieldType: date
displayName: Release date
dateRole: planning
- field: complete_date
fieldType: date
displayName: Completed date
dateRole: completed
The roles — planning, started, completed, and event (a date you attend rather than a release you consume; whether it reads as upcoming or attended is derived from the entity's status) — are enumerated with their exact meanings in Date roles.
fieldType: season can also use dateRole. It is useful when a collection uses seasons instead of exact dates.
- field: season
fieldType: season
displayName: Season
dateRole: planning
seasonLanguage: zh
seasonLanguage selects the season label language — zh, ja, or en (see Season languages).
Exact dates such as 2025-04-20 are normalized for calendar links. Broader values such as seasons and years are still useful for planning/timeline views.
Status
An enum field can be marked as the type's lifecycle status with enumRole: status. Like dateRole, this is a role — the meaning comes from the role, never the field name, so the field and its options can be named anything (状态, state, 進捗…). A type may declare at most one status field.
On its own, enumRole: status just tells KizunaShelf "this is the status field" (used for a badge and a filter facet); every option you define stays selectable and nothing else changes. To let the engine reason about status — flip it when you log, keep planned items from nagging after you finish, hide a dropped show's remaining episodes — add a statusValues mapping from each canonical status to the option strings that mean it:
- field: 状态
fieldType: enum
enumRole: status
enumOptions: [想看, 在看, 搁置, 看完, 抛弃]
statusValues:
planning: [想看]
ongoing: [在看]
paused: [搁置]
completed: [看完, 刷过] # both mean completed; 看完 is written when a log sets "completed"
dropped: [抛弃]
The five canonicals and their exact meanings are enumerated in Canonical statuses — in short: planning (intend to), ongoing, paused (deferred, a log resumes it), completed, and dropped (abandoned, a log never auto-resumes it; past records still appear in the activity feed).
Notes:
- The first option listed for a canonical is the write target — what a log flip writes when it sets that status (e.g. logging a "completed" event writes
看完, the firstcompletedoption). - A canonical you don't map (or omitting
statusValuesentirely) simply leaves that canonical unmapped — no behavior fires for it. - A frontmatter value that isn't in any list is preserved and still shown; it just has no canonical meaning. Hand-edited and legacy values are never dropped.