feat(metadata): add original-language preferences (#526)
* feat(metadata): add original-language preferences * fix(settings): show metadata exceptions immediately * docs(settings): add metadata language screenshot * fix(settings): make language exceptions responsive * fix(settings): standardize language display names
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"fixture_version": 1,
|
||||
"manifest_revision": 2,
|
||||
"manifest_revision": 3,
|
||||
"description": "Cross-platform conformance cases for settings resolution. Every case runs against the shipped manifest in this directory: definitions are referenced by key, never restated, so an expectation can only be satisfied by resolving the real contract. Each platform's resolver (Go in internal/settingsresolve, TypeScript in web/src/lib/settingsResolve.ts, Kotlin and Swift in the client repos) runs every case through a hand-written runner; a runner must fail on any fixture field it does not know, because schema drift in the fixture itself is drift. A case's constraint_bindings attach a constraint to a copy of a real definition so constraint semantics stay testable even while no shipped definition carries that constraint kind. In expected entries, constrained:true requires stored_value and constraint_kind to be present, and stored_value may be null to mean the authored value was JSON null.",
|
||||
"cases": [
|
||||
{
|
||||
@@ -438,6 +438,27 @@
|
||||
"constraints": { "allowed_metadata_languages": ["en", "fr"] },
|
||||
"expected": [{ "key": "catalog.metadata_language", "value": "fr", "source": "profile" }]
|
||||
},
|
||||
{
|
||||
"name": "metadata_language_exceptions_resolve_as_one_object",
|
||||
"description": "The original-language exception map is a single profile-scoped value; resolution preserves every source-to-target entry together.",
|
||||
"keys": ["catalog.metadata_language_overrides"],
|
||||
"context": { "profile_id": "p1" },
|
||||
"stored": [
|
||||
{
|
||||
"key": "catalog.metadata_language_overrides",
|
||||
"scope": "profile",
|
||||
"profile_id": "p1",
|
||||
"value": { "ja": "en", "no": "x-silo-original" }
|
||||
}
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"key": "catalog.metadata_language_overrides",
|
||||
"value": { "ja": "en", "no": "x-silo-original" },
|
||||
"source": "profile"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "locked_replaces_a_differing_choice",
|
||||
"description": "locked is total: the policy value replaces the user's outright, and the authored choice is preserved as stored_value so it takes effect the day the lock lifts.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"api_version": 1,
|
||||
"revision": 2,
|
||||
"revision": 3,
|
||||
"option_sets": {
|
||||
"playback_audio_languages": {
|
||||
"type": "language_tag",
|
||||
@@ -384,11 +384,28 @@
|
||||
"default_value": null,
|
||||
"category": "catalog",
|
||||
"label": "Metadata language",
|
||||
"description": "Language Silo prefers for titles, descriptions, and artwork.",
|
||||
"description": "Fallback language Silo prefers for titles, descriptions, and artwork.",
|
||||
"recommended_control": "select",
|
||||
"suggested_options": "catalog_metadata_languages",
|
||||
"unset_label": "Library default",
|
||||
"notes": "Migrates user_profiles.preferred_metadata_language; that column is NOT NULL DEFAULT '', and the empty string means unset, so migration writes a row only where it is non-empty. Deliberately carries no constrained_by. An earlier draft declared an allowlist on policy input profile_preferred_metadata_language, which is circular: internal/policy/input.go populates that field from this very column and vendor/scope.rego relays it unchanged as a preference. Policy narrows nothing here, and an allowlist bound to a scalar equal to the current value would either be a no-op or reject every change the user makes."
|
||||
"notes": "Migrates user_profiles.preferred_metadata_language; that column is NOT NULL DEFAULT '', and the empty string means unset, so migration writes a row only where it is non-empty. Deliberately carries no constrained_by. An earlier draft declared an allowlist on policy input profile_preferred_metadata_language, which is circular: internal/policy/input.go populates that field from this very column and vendor/scope.rego relays it unchanged as a preference. Policy narrows nothing here, and an allowlist bound to a scalar equal to the current value would either be a no-op or reject every change the user makes. From revision 3, the private-use tag x-silo-original means resolve the target from each media item's original_language. It remains a valid value of the existing language_tag schema, so this is additive rather than a response-field type change."
|
||||
},
|
||||
{
|
||||
"key": "catalog.metadata_language_overrides",
|
||||
"introduced_in": 3,
|
||||
"persistence": "remote",
|
||||
"allowed_scopes": ["profile"],
|
||||
"resolution_order": ["profile", "default"],
|
||||
"value_schema": {
|
||||
"type": "object",
|
||||
"schema_ref": "metadata-language-overrides.json"
|
||||
},
|
||||
"default_value": {},
|
||||
"category": "catalog",
|
||||
"label": "Metadata language exceptions",
|
||||
"description": "Preferred metadata language for items in specific original languages.",
|
||||
"recommended_control": "panel",
|
||||
"notes": "Keys are canonical catalog original_language codes. Values are target BCP 47 language tags; x-silo-original means retain that source language. This key is separate from catalog.metadata_language so existing clients can continue changing the fallback without rewriting or discarding exceptions."
|
||||
},
|
||||
{
|
||||
"key": "player.hdr_enabled",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://silo-server.dev/contracts/settings/v1/schemas/metadata-language-overrides.json",
|
||||
"title": "Metadata language exceptions",
|
||||
"description": "Maps a media item's canonical original-language code to the metadata language a profile wants for that item. x-silo-original means use the source language itself.",
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"description": "Canonical ISO 639 language code as stored in media_items.original_language.",
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]{2,3}$"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"description": "A BCP 47 target language or the x-silo-original private-use sentinel.",
|
||||
"type": "string",
|
||||
"minLength": 2,
|
||||
"maxLength": 255,
|
||||
"pattern": "^([a-zA-Z]{2,3}(-[a-zA-Z]{3}){0,3}(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-([0-9a-zA-Z]{5,8}|[0-9][0-9a-zA-Z]{3}))*(-[0-9a-wy-zA-WY-Z](-[0-9a-zA-Z]{2,8})+)*(-[xX](-[0-9a-zA-Z]{1,8})+)?|[xX](-[0-9a-zA-Z]{1,8})+)$"
|
||||
},
|
||||
"maxProperties": 256
|
||||
}
|
||||
Reference in New Issue
Block a user