Template format, version 1
A template defines which fields items of one category have. Templates are UTF-8 JSON documents. Built-in templates ship in Packages/KuriosaKit/Sources/KuriosaCore/Resources/Templates/; every template used by a library is copied into its template table, so a library is self-contained.
Document
{
"id": "builtin.watch",
"version": 1,
"name": { "en": "Watches", "de": "Uhren", "fr": "Montres", "it": "Orologi", "es": "Relojes" },
"icon": "watch.analog",
"sections": [ { "id": "identity", "label": { "en": "Identity", "…": "…" } } ],
"fields": [ { "key": "brand", "type": "text", "label": { "en": "Brand" }, "section": "identity", "identifying": true } ]
}
| Key | Meaning |
|---|---|
id | Stable ID. Built-in: builtin.<name>. User templates: custom.<uuid>. |
version | Content version; the app replaces stored built-ins with higher versions. |
name, label, suffix | Localized text: object of language code → text; en is required and the fallback. |
icon | SF Symbol name (Android maps it to its own icon set). |
sections | Field groups in display order. |
fields | Fields in display order. |
highlights | Optional: up to three key facts shown on gallery cards and lists, in this order: visible, non-sensitive field keys, and @count for the count of a collection that counts (quantity below; issue #55). @count cannot be a field key; it is left out while the count is removed (removedStandardFields) and comes back with it. Without it, the first three visible, non-sensitive fields are used; a list with only @count shows the count only. |
basedOn | Optional: for a customized copy, the ID of the template it was copied from. |
quantity | Optional: the collection's pieces are counted (issue #41, decision 0031), and what one unit is called: {"one": <localized>, "other": <localized>}, for example {"one": {"en": "bottle"}, "other": {"en": "bottles"}}; both are optional (the app then says "piece" and "pieces"), {} counts pieces. Without it, nothing is counted. The count comes from the item's journal (item.purchase_quantity and item.events, see catalog-schema.md). Built-ins: wine, spirits, and inks (bottles), trading cards (copies), ammunition (rounds), coins, banknotes, stamps, sealed products, and the blank template (pieces). |
suggestions | Optional: the sub-collections this category suggests (issue #86, decision 0041), in the order they are offered; see "Suggested sub-collections" below. [] suggests nothing on purpose. |
removedStandardFields | Optional: standard fields this collection does not show or edit (issue #24): any of quantity (only with quantity above), purchase_date, purchase_price, source, estimated_value, condition, place, tags, files (the documents; photos stay), notes, in this order. The name cannot be removed. Values stay on the items, so adding a field back shows them again. Without it, all standard fields are shown. |
showcase | Optional: how the objects are shown on their showcase stage: strap (watches, bracelets; fills the stage height, the ends fade), standing (bottles, figurines; stands on a plinth line), wide (cars, knives in profile), lying (coins, stamps, art on paper; on a tray, front and back side by side), or passepartout (a photo inside a mat). Without it, the photo's shape decides: a photo that is not cut out goes into a passepartout, a cut-out wider than 1.6 × its height is wide, every other cut-out stands. lying only ever comes from the template. A customized copy without its own mode uses the mode of the template it is basedOn. Built-ins: watches and straps strap; coins, banknotes, stamps, trading cards, comics, and records lying; wine, spirits, perfume, inks, handbags, books, and toys standing; cars, knives, firearms, pens, scale models, and optics wide; the others none. |
Built-in templates
builtin.<name> with <name>.json in the resources. "New collection" offers them in groups (TemplateGroup, issue #86), in this order:
| Group | Templates |
|---|---|
| Wear and carry | watch, jewelry, sneaker, handbag, pen, knife |
| Coins and paper | coin, banknote, stamp, trading_card, comic, book |
| Art, music, and play | art, record, instrument, toy, scale_model, video_game |
| Drinks and scents | wine, spirits, perfume |
| Gear and nature | car, camera, mineral, firearm, ammunition |
| Start empty | blank ("Custom collection") |
Offered only as suggested sub-collections, never in "New collection": strap, optic, lens, ink, console, sealed_product. wishlist holds the wishlist's own fields (below). The group names are app text (String Catalog), not template data.
Suggested sub-collections (decision 0041)
A template may name the sub-collections it suggests, for example in builtin.firearm:
"suggestions": [
{ "template": "builtin.ammunition" },
{ "template": "builtin.optic" },
{ "template": "builtin.blank", "name": { "en": "Accessories", "de": "Zubehör", "…": "…" }, "icon": "tray.2" }
]
| Key | Meaning |
|---|---|
template | Optional: the built-in template the sub-collection is made from. Without it, the sub-collection gets the fields of the collection it sits in (a coin collection's "Medals"). A template the app does not know is not offered. |
name | Optional, localized: its name; without it, the template's name. Required when template is absent. |
icon | Optional: its SF Symbol; without it, the template's. Required when template is absent. |
Rules (reimplement exactly):
- A collection's suggestions are its template's own
suggestions. When the template has none (the key is absent), they are those of the built-in template it is (id) or isbasedOn, as shipped with the app: a library keeps the copy of a built-in it stored first, which may be older than the suggestions.[]means none. - "New sub-collection" offers the parent's suggestions first, named, with the first one (or the one tapped in the empty collection) chosen; then the groups of "New collection".
- A collection without any piece, also in its sub-collections, shows its suggestions as chips at its top; a suggestion whose name equals a sub-collection's name (ignoring case) is left out.
- Creating a sub-collection from a suggestion stores the built-in template in the library on first use, as for any collection. When the suggestion's
icondiffers from its template'sicon, the sub-collection gets its own copy of that template (custom.<uuid>,basedOnthe template, the suggestion'sicon, and"suggestions": []). One undoable step creates the collection. - Built-ins with suggestions: watches (straps, accessories), firearms (ammunition, optics, accessories), cameras (lenses, accessories), video games (consoles, accessories), coins (banknotes, medals), stamps (covers), trading cards (sealed products), wine (spirits), perfume (samples and decants), handbags (small leather goods), pens (inks).
Field
| Key | Type | Default | Meaning |
|---|---|---|---|
key | string | – | Storage key, [a-z][a-z0-9_]*, unique, never renamed once shipped |
type | string | – | text, number, date, currency, dropdown, multiple_choice, rating, checkbox, url, quantity, measurement, relation, calculation |
label | localized | – | Display label |
section | string | first section | Section ID |
required | bool | false | A value must be present |
sensitive | bool | false | Hidden by default, excluded from exports, blurred in privacy mode |
identifying | bool | false | Used by the duplicate check |
options | array | – | {id, label} choices for dropdown and multiple_choice, units for quantity; required for all three |
measurement | string | – | length, mass, or volume; required for measurement |
unitHint | object | – | {"metric": "<unit>", "imperial": "<unit>"} preferred display units |
ratingMax | int | 5 | 1…10 |
suffix | localized | – | Label after a number (for example h, kW) |
multiline | bool | false | Multi-line text |
example | localized | – | A typical value for text and number fields, shown as a quiet prompt in an empty field ("e.g. 79030N"). Never on sensitive fields. May be English only when it is a name or a number. |
formula | object | – | Required for calculation: {"left": <operand>, "operation": "add" \| "subtract" \| "multiply" \| "divide", "right": <operand>}; see below |
hidden | bool | false | Not shown or edited; values stay on items. Fields are never deleted. |
Older templates may have countsDown on a quantity field (issue #26); readers ignore it. The collection's count (quantity above) replaced it (decision 0031).
Options ({id, label, hidden}): a hidden option is not offered for new values but still displays on items that use it.
Multiple choice fields (decision 0042)
A multiple_choice field holds any number of its options (issue #87), a dropdown exactly one. Rules (reimplement exactly):
- Values are stored as the option IDs in the order of
options, at least one, each once (seeitem.fieldsin catalog-schema.md). No choice is no value. - Readers show the choices in the order of
options, joined by ", " ("Leather, Steel"), in every language and in exports; a choice the field does not know follows with its ID. - A
dropdownfield may hold amultiple_choicevalue and amultiple_choicefield adropdownvalue, from before the field's type changed (below); readers take adropdownvalue as a list of its one choice, and amultiple_choicevalue on adropdownfield as its choices. Validation accepts either form for either type, with known option IDs. - A CSV import splits a cell at commas and semicolons into choices, unless the whole cell names one choice; each part is matched like a
dropdownchoice (import.md).
Unit identifiers: mm cm m km in ft mi (length), g kg ct gr oz ozt lb (mass), ml cl l fl_oz gal (volume). Conversion factors to the canonical unit are defined in KuriosaCore/Values/Units.swift (DisplayUnit.canonicalFactor).
Calculation fields (decision 0025)
A calculation field shows a value worked out from two others, for example "Price per round = Purchase price ÷ Rounds":
{ "key": "price_per_round", "type": "calculation", "label": { "en": "Price per round" },
"formula": { "left": { "item": "purchasePrice" }, "operation": "divide", "right": { "field": "rounds" } } }
An operand is an object with exactly one key:
{"field": "<key>"}: a field of the same template of typenumber,currency,quantity(its amount),measurement(its canonical SI value),rating(its stars), orcalculation(its result);{"item": "purchasePrice"}or{"item": "estimatedValue"}: the item's own amounts;{"number": <number>}: a fixed number.
Rules (reimplement exactly):
- Results are never stored; item data never holds a value for a calculation's key (a stored value there is ignored), and
{"type": "calculation"}is not a valid field value. A result is calculated whenever it is shown or exported. - Units: numbers, quantities, stars, and fixed numbers are plain.
+and−keep equal units, and a plain number takes the other side's unit;×needs one plain side and keeps the other's unit;÷by a plain number keeps the left unit, and equal units give a plain number (a ratio). Money and measurements of different kinds never combine. Money results keep the currency and are rounded half away from zero to its minor unit; measurement results keep their kind and are shown with the unit hint of the first measurement they use. - No result (the line stays empty) while an operand has no value, when dividing by zero, when two amounts have different currencies, or when the units do not fit (for example after an operand became a text field).
- A calculation is never
requiredoridentifying. One that uses a sensitive field, directly or through another calculation, is sensitive itself; the editor sets this whenever a field changes. - Calculations may use other calculations, never in a circle.
- Hidden operands keep being used (their values stay on the items).
Validation
TemplateValidator enforces: valid key format, unique keys, English labels, known sections, non-empty unique options for dropdown/multiple_choice/quantity, a kind for measurement and hints of the same kind, ratingMax in 1…10, for calculation a formula whose fields exist and can be calculated with, no circle, neither required nor identifying, and sensitive when it uses a sensitive field, and for suggestions a non-empty template and icon where given and English in name.
Customizing (decision 0014)
- Editing a built-in or shared template gives the collection its own copy with ID
custom.<uuid>andbasedOnset; the original never changes. - New field keys are derived from the label: diacritics removed, lowercase ASCII letters and digits, other characters become
_, prefixed withfield_if it does not start with a letter, suffixed_2,_3, … if taken. - Keys never change. Renaming replaces the label with one text (
{"en": "<name>"}), used for every language. - Fields and options are hidden instead of deleted.
- A field's type changes only while no item has a value for its key, with two exceptions (decision 0042): a
dropdowncan always become amultiple_choice, and amultiple_choiceadropdownwhile no item has more than one of its choices. Stored values are not rewritten (see "Multiple choice fields"). A field that is no longer acalculationloses itsformula; one that is no longer adropdown,multiple_choice, orquantityloses itsoptions.
The wishlist template (decision 0017)
builtin.wishlist holds the fields every wish gets on top of its collection's fields. It ships with one field, shop_url (url, "Link to buy"), and is not offered as a collection template. It is customized like any template; the copy in use is named by library_meta.wishlist_template_id. Its values are stored in item.wish_fields.
Why translations live in the template
Templates are data and must stay portable to Android and shareable later, so their labels carry their own translations instead of living in the app's String Catalog. User-made templates usually have only one language; the English key doubles as the fallback.