From 92bf4eb3521d624ed1fef8b74e64467ef1e8255c Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 13 Sep 2026 11:39:54 +0000 Subject: [PATCH] docs: OpenL10n and OpenI18n, the words half and the languages half of OpenFile OpenL10n is the record of what a file says, in any language: one transcript per media per language, addressed by the OpenFile id (or a url: or live: identity for what has no bytes), lines as seconds into the media, a translation kept beside the original and marked with what it came from, complete or heard in pieces, made once on request with 202 progress, served as JSON, SRT, VTT or text. OpenI18n is one file a service serves at /.well-known/openi18n.json about the languages it speaks: which, which it can turn into which, how to ask for one, and where texts are translated, with limits, models and the pivot said out loud. Both are what nixamp 0.24 and 0.25 serve today, written down so anything that hears or translates can keep the same record. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015KAKuRngFbQg3kET6RS5yN --- apps/logicsrc-web/src/lib/specs.ts | 4 +- docs/openi18n.md | 116 ++++++++++++++++++++++++++++ docs/openl10n.md | 119 +++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 docs/openi18n.md create mode 100644 docs/openl10n.md diff --git a/apps/logicsrc-web/src/lib/specs.ts b/apps/logicsrc-web/src/lib/specs.ts index 4b7c0a2..a7ff32a 100644 --- a/apps/logicsrc-web/src/lib/specs.ts +++ b/apps/logicsrc-web/src/lib/specs.ts @@ -94,7 +94,9 @@ export const FAMILIES: Family[] = [ s("openthreat", "OpenThreat", "One file a security tool serves about what it found in the open: public subjects only, secrets never located"), s("openrental", "OpenRental", "One file an operator serves about the agents and file swarms it rents out: members, metadata and rates through CoinPay", { landing: undefined, status: "draft" }), s("opensite", "OpenSite", "One record about a page or a site: the card a reader would draw, declared by the site or read from it, kept by an index"), - s("openwebring", "OpenWebring", "A webring that says who made it: one file a ring serves about its members, one a member serves about itself, and made_by on every member") + s("openwebring", "OpenWebring", "A webring that says who made it: one file a ring serves about its members, one a member serves about itself, and made_by on every member"), + s("openl10n", "OpenL10n", "The record of what a file says, in any language: one transcript per media per language, kept once, translations beside the original", { parent: "openfile" }), + s("openi18n", "OpenI18n", "One file a service serves about the languages it speaks, which it can turn into which, how to ask for one, and where texts are translated") ] }, { diff --git a/docs/openi18n.md b/docs/openi18n.md new file mode 100644 index 0000000..e4bb6a6 --- /dev/null +++ b/docs/openi18n.md @@ -0,0 +1,116 @@ +# OpenI18n + +OpenI18n is one file a service serves about the languages it speaks: which ones, which it can turn into which, how a reader asks for anything in one of them, and where texts are sent to be translated. A reader learns what a site or an API can say in Swedish before asking, a directory lists services by the languages they serve, and a program that has a line to translate finds a translator the way it finds anything else on the web, by a well-known address rather than a vendor's SDK. It is the languages half of [OpenL10n](/openl10n): OpenL10n keeps what was said in a language, OpenI18n says which languages there are. It is maintained by Profullstack, Inc. as part of the LogicSRC open-standards surface, with a reference implementation in [nixamp](https://nixamp.com). + +Status: **0.1**. A description of a document already served, published so any service can serve one and any reader can read it. + +Slug: `openi18n` + +## The problem + +Every service that speaks more than one language says so in its own way: a flag menu, an `Accept-Language` header it may or may not honour, a `?lang=` it invented, a translation API behind a key. A reader cannot ask "do you speak Swedish, and can you make this in it" without reading the docs of each one. And translation itself, now that it runs on a service's own CPU with open models, has no door: each product exposes its own, or none. + +The pieces exist. `Accept-Language` and `Content-Language` say what a person wants and what they got. `hreflang` says where the other languages of a page are. ISO 639-1 names the languages. What is missing is the one file that puts them together for a service, so its languages can be discovered instead of guessed, and its translator can be found instead of configured. + +## Terms + +- A **service** is anything that speaks languages: a site, an API, a player, a keeper of transcripts. Its **descriptor** is the file it serves about them. +- A **language** is an ISO 639-1 code, lowercase: `en`, `de`, `sv`. +- A **pair** is a direction the service can translate: from one language to another. +- A **translator** is the endpoint that turns texts in one language into another. +- A **reader** is anything that reads the descriptor: a person's browser, a directory, a program, an agent. + +## The descriptor + +A service serves a JSON document at `/.well-known/openi18n.json` on its own origin. + +```json +{ + "openi18n": "0.1", + "name": "nixamp", + "url": "https://nixamp.com", + "languages": [ + { "code": "en", "name": "English", "native": "English", "targets": ["de", "sv", "es", "fr"] }, + { "code": "de", "name": "German", "native": "Deutsch", "targets": ["en", "sv", "fr", "es"] }, + { "code": "sv", "name": "Swedish", "native": "Svenska", "targets": ["en", "de"] } + ], + "default": "", + "ask": { "query": "language", "header": "Accept-Language" }, + "translate": { + "url": "https://nixamp.com/api/v1/translate", + "auth": { "kind": "bearer", "url": "https://nixamp.com/account" }, + "limits": { "texts": 200, "charactersPerMinute": 20000 }, + "models": { "en-sv": "Xenova/opus-mt-en-sv", "sv-de": "Xenova/opus-mt-sv-en then Xenova/opus-mt-en-de" }, + "pivot": "en" + }, + "operator": "https://logicsrc.com/.well-known/openprofile.md" +} +``` + +The smallest valid descriptor is a list of languages: + +```json +{ "languages": [{ "code": "en" }, { "code": "sv" }] } +``` + +The rules, and every one degrades: + +1. **`languages` is required and is the only required key.** Each entry has a `code`; `name` is the language in English and `native` in itself, for a picker. A descriptor with codes alone is valid, and a reader names them from its own table. +2. **`targets`** on a language are the languages the service can turn it into. Absent means the service serves content in that language and translates nothing from it. A reader offering a picker for a piece of content in `en` offers exactly `en`'s targets. +3. **`default`** is the language the service answers in when not asked; `""` means as spoken, which is what a transcript or a recording has and a site does not. Absent means the first language listed. +4. **`ask`** says how a reader asks for a language on any resource the service serves: `query` names the parameter (`?language=sv`), `header` names the header, and the query wins when both are given. Absent means `?language=` and `Accept-Language`. A resource answered in a language says so with `Content-Language`, and a resource that exists in several links them with ``, as the web already does. +5. **`translate.url`** is the translator. `POST` it `{ "texts": ["…", "…"], "from": "en", "to": "sv" }` and it answers `{ "texts": ["…", "…"], "from": "en", "to": "sv", "model": "…" }`, the same order, empty strings kept in place. A `GET` of the same URL answers this descriptor's `languages`, so a reader that found the translator found the languages. Absent `translate` means the service serves languages and translates nothing. +6. **`translate.auth`** is `none`, `bearer`, `oauth` or `api-key`, with `url` where a person gets a credential, as [OpenMCP](/openmcp) spells it. Absent means unstated, which a reader reports rather than assumes. +7. **`translate.limits`** says how many `texts` one call takes and how many `charactersPerMinute` one caller may send. A translator over its limit answers `429`; a reader waits and asks again. Absent means unstated. +8. **`translate.models`** names what does each pair, by pair (`from-to`), and **`pivot`** names the language a pair with no model of its own goes through. A reader that cares which model touched its words can see it; a pair through a pivot is two models and twice the wait, and the descriptor says so rather than hiding it. +9. **`operator`** is the person or organisation answerable for the service, as an [OpenProfile.md](/openprofile) URL. +10. **Unknown keys are kept.** A service says more than this document names, and a reader passes it through. + +Serve it as `application/json`. A descriptor fetched from `/.well-known/` on the service's own origin is verified; one found elsewhere is a claim about the service by whoever hosts it. + +## Asking for a language + +A reader that wants a resource in a language: + +1. Reads the descriptor and checks the language is listed, or is a target of the resource's own language. +2. Asks with `ask.query` (or `?language=`), falling back to `Accept-Language`. +3. Reads `Content-Language` on the answer. A service that could not answer in the language asked for answers in the resource's own language and says so; it never answers a different language silently. +4. When the resource is a transcript, expects `202` with progress while a translation is being made for the first time ([OpenL10n](/openl10n) rule 9), and asks again. + +## Translating + +A reader with texts to translate: + +1. Finds a translator: the descriptor of the service it is already talking to, or a directory listing translators by pair. +2. Checks the pair is in `languages[from].targets`, or that both `from` and `to` reach `pivot`. +3. Sends at most `limits.texts` at a time, keeps under `limits.charactersPerMinute`, and treats `429` as a wait. +4. Keeps what it got. Translation is made once; a reader that has the answer does not ask twice, and a keeper of transcripts keeps it beside the original. + +## Directories + +A directory reading descriptors lists services by language and by pair, and says for each whether the translator answered when last probed, when, and with which auth. It reports absence as absence: an unstated `translate` is a service that translates nothing, not one that might. + +## What is deliberately absent + +**No quality score.** `models` says what did the work; whether it did it well is the reader's judgement. + +**No locale.** A language is a language. Regions, scripts, date formats and currencies are the web's `lang` attribute and BCP 47's business; a service that needs `pt-BR` says so under its own key. + +**No string bundles.** This is not a format for a program's own interface strings. A program that wants its menus in Swedish has gettext and its descendants; this is for the content a service serves and the words it can translate. + +## Related standards + +- [OpenL10n](/openl10n): the transcript of a file in a language, and its translations. +- [OpenFile](/openfile): the media a transcript belongs to. +- [OpenMCP](/openmcp): the `auth` block, and how a directory probes a service. +- [OpenProfile.md](/openprofile): the `operator` behind a service. + +## Version history + +| Version | Date | Change | +|---|---|---| +| 0.1 | 2026-09-13 | First publication: the descriptor, languages and targets, asking for a language, the translator, limits, models and pivot, directories. | + +## License + +The specification text is CC BY 4.0. Serve it, copy it, extend it. diff --git a/docs/openl10n.md b/docs/openl10n.md new file mode 100644 index 0000000..631fcb3 --- /dev/null +++ b/docs/openl10n.md @@ -0,0 +1,119 @@ +# OpenL10n + +OpenL10n is the record of what a file says, in any language: a transcript of a recording, a film or a broadcast, kept once under the identity of the media rather than of whoever played it, with every translation of it kept beside the original and marked with the language it came from. A player that meets the same file reads the lines instead of hearing them again; a reader that wants Swedish gets the Swedish that was made once, or asks for it and it is made and kept. It is the words half of [OpenFile](/openfile): OpenFile says what the bytes are, OpenL10n says what they say. It is maintained by Profullstack, Inc. as part of the LogicSRC open-standards surface, with a reference implementation in [nixamp](https://nixamp.com). + +Status: **0.1**. A description of a record already kept, published so anything that hears or translates can keep the same one. + +Slug: `openl10n` + +## The problem + +Speech to text costs a CPU for as long as the sound lasts, and translation costs one for as long as the text is. A film heard on Tuesday says the same words on Thursday, on every machine that plays it, and today each of them hears it again. The transcript that was made is thrown away with the process that made it, or kept in a shape only that program reads, under a name only that machine knows: the channel it was on, the path it was at. + +The pieces exist. OpenFile names a file by the hash of its bytes, so the same file anywhere is one identity. SRT and WebVTT carry timed lines. What is missing is the record between them: one transcript per file per language, addressed by the file, that any program can read, add to, and translate once. + +## Terms + +- The **media** is what was said: a file, a link, or a broadcast. Its **identity** is how the record is addressed. +- A **transcript** is the media's words in one language, as timed lines. The **original** is the transcript in the language that was spoken; a **translation** is a transcript in another language, made from an original or from another translation. +- A **line** is one thing said, with when it began and ended as seconds into the media. +- A **keeper** is whatever holds transcripts and answers for them: a site, a service, a program's own store. +- A **hearer** is whatever turns sound into an original: a speech model and the machine it runs on. +- A **reader** is anything that reads a transcript: a player drawing captions, a subtitle file, a search index, an agent. + +## The record + +A keeper serves a transcript as a JSON document. + +```json +{ + "id": "sha256:d6c3f8285b7871d6a400cba14408288a9acde679f12e1e7dc276f29ca7c493ff", + "language": "sv", + "translatedFrom": "en", + "complete": true, + "model": "Xenova/opus-mt-en-sv", + "seconds": 5400, + "updated": "2026-09-13T09:24:39Z", + "lines": [ + { "start": 0, "end": 6.14, "text": "Och sedan, frågan om behörigheter i molnet." }, + { "start": 6.14, "end": 11.3, "text": "Jag tror att jag glömde vilket land det var." } + ], + "languages": [ + { "language": "en", "translatedFrom": null, "lines": 700, "complete": true, "url": "https://nixamp.com/hash/d6c3f828…c493ff.srt?language=en" }, + { "language": "sv", "translatedFrom": "en", "lines": 700, "complete": true, "url": "https://nixamp.com/hash/d6c3f828…c493ff.srt?language=sv" } + ] +} +``` + +The smallest valid record is an identity, a language and lines: + +```json +{ "id": "sha256:d6c3f828…c493ff", "language": "en", "lines": [{ "start": 0, "end": 5, "text": "Hello." }] } +``` + +The rules, and every one degrades: + +1. **`id`, `language` and `lines` are the only required keys.** A reader lists what it was given and reports the rest as unstated. +2. **`id` is the media's identity.** For a file it is the OpenFile id, `sha256:` and the hex of every byte, so the transcript of a file is found from the file's own record. For a link that is played rather than fetched it is `url:` and the address the player was given, without the fragment. For a broadcast, which has no bytes to hash, it is `live:` and the host, the channel and when it began: `live:server1.example:4321/main@1789292084930`, the moment as milliseconds since the epoch, so a second airing is a second transcript. A keeper may accept a quicker fingerprint of a file as an alias and say so; the `sha256:` id is the one two keepers agree on. +3. **`language`** is the ISO 639-1 code of the lines. `""` means the hearer did not say, which a reader shows as unknown rather than as English. +4. **`translatedFrom`** is the language this transcript was made from, and absent or `null` means these are the words as spoken. A translation of a translation names the language it was made from, not the original's, so a reader can see the chain. +5. **`lines`** are in order of `start`. `start` and `end` are seconds into the media, decimals allowed. `text` is plain text, not markup. A line is what one hearing produced: a sentence, or a window of a few seconds when the hearer worked in windows. Two lines may overlap a little at their edges; a reader that finds two lines saying the same thing for the same seconds keeps the longer. +6. **`complete`** says the whole media was heard in one pass. Absent means it was heard in pieces as it played, which may have gaps where nobody was listening, and a keeper appends to an incomplete transcript and replaces it with a complete one. Nothing appends to a complete transcript. +7. **`model`** names what heard or translated it, as the hearer names itself. `seconds` is how far into the media the lines reach. `updated` is when the record last changed. +8. **`languages`** lists every transcript the keeper has for this media, each with its `language`, `translatedFrom`, how many `lines`, whether it is `complete`, and the `url` it is served at. A reader picks a language from this list rather than guessing. +9. **A language asked for that the keeper does not have is made, once, and kept**, when the keeper can translate. The keeper answers `202` with `{ "translating": { "done": 120, "total": 700 } }` and the lines it has so far while a long one is being made, and `200` with the transcript once it is as far along as the original. A reader asks again later; it never asks the model itself. +10. **Formats.** A keeper serves the same transcript as SubRip (`.srt`, `application/x-subrip`), WebVTT (`.vtt`, `text/vtt`) and plain text (`.txt`, one line per line) by extension or by `?format=`, with `?language=` picking the transcript. The JSON is the record; the others are renderings of it. +11. **Unknown keys are kept.** A keeper says more than this document names, under its own key, and a reader passes it through. + +Serve it as `application/json`. + +## Discovery + +A reader finds a transcript three ways: + +1. From the media's OpenFile record: a `transcripts` list there with the same rows as `languages` above, or, until a keeper serves it at the top, the same list under the keeper's own key. nixamp serves it as `nixamp.transcripts` today. +2. `` on a page about the media, or `Link: <...>; rel="openl10n"` on the media itself, pointing at the record in the original language; `languages` in it points at the rest. +3. A URL handed to the reader directly. + +A keeper that lists transcripts across many media serves `/.well-known/openl10n.json`: `{ "keeper": { "name", "web" }, "transcripts": [ ...records without their lines... ] }`, newest first, so a directory learns what has been written down without asking file by file. + +## Hearing and keeping + +A conforming hearer: + +1. Says which language it heard. A speech model told nothing assumes a language, and a Swedish recording heard as English comes back as three English words repeated; detecting first is what makes the transcript say anything. +2. Stamps each line in seconds into the media, not in wall-clock time. A player that hears a stream live converts on the way in, from where the stream is in the media or from when the broadcast began, so a transcript of a file means the same thing whichever machine made it. +3. Sends what it heard to the keeper as it goes, in batches, under the media's identity and the language heard, so a process that dies keeps most of what it heard. +4. Asks the keeper first. A moment the keeper has already been through is read out, not heard again, and only what is new goes to the model. + +A conforming keeper merges what it is sent (rule 6), never re-hears what it has, translates once per language (rule 9), and serves every rendering (rule 10). + +## Live captions + +A broadcast is heard as it happens, a few seconds at a time, and a reader watching it wants each line as it is said, in its own language. A keeper or a player streams lines with wall-clock stamps beside the media-second ones: `{ "at": 1789292125085, "until": 1789292130085, "start": 40.2, "end": 45.2, "text": "…", "language": "sv", "original": "…" }`, `at` and `until` being when the sound was at the live edge, so a page can hold each line until its own playback gets there. A translated line carries the words as spoken in `original`, so a reader can show both. The stream is the player's business; the kept record is this document's. + +## What is deliberately absent + +**No model.** A hearer is whatever hears; the record says which one it was and nothing about how good it is. Two keepers may disagree about the same seconds and a reader sees both models. + +**No word timing.** A line is the unit. Word-level timing doubles the cost of hearing for a caption that lands close to the voice either way; a keeper that has it puts it under its own key. + +**No rights.** What may be transcribed or translated is the media's business, and OpenFile's `attestation` is where that is said. A keeper that requires consent reads it there. + +**No search.** A keeper answers by identity. Finding a file by what it says is a directory's job. + +## Related standards + +- [OpenFile](/openfile): the media's identity and its record, which lists these transcripts. +- [OpenI18n](/openi18n): how a keeper says which languages it can translate between, and how a reader asks for one. +- [OpenSite](/opensite): the card a page draws for the media, which may quote the transcript. + +## Version history + +| Version | Date | Change | +|---|---|---| +| 0.1 | 2026-09-13 | First publication: the record, identities for files, links and broadcasts, translations, completeness, formats, discovery, hearing and keeping, live lines. | + +## License + +The specification text is CC BY 4.0. Serve it, copy it, extend it.