@murumets-ee/content 0.0.0-preview-20261014110621
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +94 -0
- package/README.md +15 -0
- package/dist/admin.d.mts +56 -0
- package/dist/admin.d.mts.map +1 -0
- package/dist/admin.mjs +1 -0
- package/dist/block-resolver.d.mts +320 -0
- package/dist/block-resolver.d.mts.map +1 -0
- package/dist/block-resolver.mjs +2 -0
- package/dist/block-resolver.mjs.map +1 -0
- package/dist/breadcrumbs-BqSuMNac.mjs +2 -0
- package/dist/breadcrumbs-BqSuMNac.mjs.map +1 -0
- package/dist/chrome-entity-names-zYJzP-_U.mjs +2 -0
- package/dist/chrome-entity-names-zYJzP-_U.mjs.map +1 -0
- package/dist/chrome-provider.d.mts +143 -0
- package/dist/chrome-provider.d.mts.map +1 -0
- package/dist/chrome-provider.mjs +2 -0
- package/dist/chrome-provider.mjs.map +1 -0
- package/dist/client.d.mts +89 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +2 -0
- package/dist/client.mjs.map +1 -0
- package/dist/content-config-store-8omQF5p5.mjs +2 -0
- package/dist/content-config-store-8omQF5p5.mjs.map +1 -0
- package/dist/context-D7eEItCY.mjs +2 -0
- package/dist/context-D7eEItCY.mjs.map +1 -0
- package/dist/detail-template-names-CD1Y9n1U.mjs +2 -0
- package/dist/detail-template-names-CD1Y9n1U.mjs.map +1 -0
- package/dist/detail-template-provider.d.mts +28 -0
- package/dist/detail-template-provider.d.mts.map +1 -0
- package/dist/detail-template-provider.mjs +2 -0
- package/dist/detail-template-provider.mjs.map +1 -0
- package/dist/document-site-B8tmEqLw.mjs +2 -0
- package/dist/document-site-B8tmEqLw.mjs.map +1 -0
- package/dist/entities/index.d.mts +64 -0
- package/dist/entities/index.d.mts.map +1 -0
- package/dist/entities/index.mjs +2 -0
- package/dist/entities/index.mjs.map +1 -0
- package/dist/enumerate-CYHJKQz6.mjs +2 -0
- package/dist/enumerate-CYHJKQz6.mjs.map +1 -0
- package/dist/errors-NRNILhhm.mjs +2 -0
- package/dist/errors-NRNILhhm.mjs.map +1 -0
- package/dist/index-cT8Qk4gu.d.mts +275 -0
- package/dist/index-cT8Qk4gu.d.mts.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/locale-entity-CoSq3C77.mjs +2 -0
- package/dist/locale-entity-CoSq3C77.mjs.map +1 -0
- package/dist/locale-registry-COsJrVZl.mjs +2 -0
- package/dist/locale-registry-COsJrVZl.mjs.map +1 -0
- package/dist/locale-utils-B0mFoV47.mjs +2 -0
- package/dist/locale-utils-B0mFoV47.mjs.map +1 -0
- package/dist/lock-service.d.mts +58 -0
- package/dist/lock-service.d.mts.map +1 -0
- package/dist/lock-service.mjs +2 -0
- package/dist/lock-service.mjs.map +1 -0
- package/dist/locks-5779ugSW.mjs +2 -0
- package/dist/locks-5779ugSW.mjs.map +1 -0
- package/dist/plugin-CjgqBSFy.mjs +2 -0
- package/dist/plugin-CjgqBSFy.mjs.map +1 -0
- package/dist/plugin.d.mts +41 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +1 -0
- package/dist/query-client.d.mts +131 -0
- package/dist/query-client.d.mts.map +1 -0
- package/dist/query-client.mjs +2 -0
- package/dist/query-client.mjs.map +1 -0
- package/dist/registry-CaPbY5lm.mjs +2 -0
- package/dist/registry-CaPbY5lm.mjs.map +1 -0
- package/dist/resolve-context-DklN53BH.mjs +2 -0
- package/dist/resolve-context-DklN53BH.mjs.map +1 -0
- package/dist/resolve.d.mts +651 -0
- package/dist/resolve.d.mts.map +1 -0
- package/dist/resolve.mjs +9 -0
- package/dist/resolve.mjs.map +1 -0
- package/dist/rolldown-runtime-DK3Fl9T5.mjs +1 -0
- package/dist/routes-siXmjKDA.mjs +2 -0
- package/dist/routes-siXmjKDA.mjs.map +1 -0
- package/dist/routing-strategy-CAG3tZP0.d.mts +155 -0
- package/dist/routing-strategy-CAG3tZP0.d.mts.map +1 -0
- package/dist/routing-strategy-D6B4ngT9.mjs +2 -0
- package/dist/routing-strategy-D6B4ngT9.mjs.map +1 -0
- package/dist/schema-BYrYzsmO.mjs +2 -0
- package/dist/schema-BYrYzsmO.mjs.map +1 -0
- package/dist/schema-LlK6EkOJ.mjs +2 -0
- package/dist/schema-LlK6EkOJ.mjs.map +1 -0
- package/dist/seed.d.mts +177 -0
- package/dist/seed.d.mts.map +1 -0
- package/dist/seed.mjs +2 -0
- package/dist/seed.mjs.map +1 -0
- package/dist/site-chrome-BAtQaAqC.mjs +2 -0
- package/dist/site-chrome-BAtQaAqC.mjs.map +1 -0
- package/dist/site-chrome-DNCcePSR.d.mts +98 -0
- package/dist/site-chrome-DNCcePSR.d.mts.map +1 -0
- package/dist/translation-provenance-B41lEDM-.mjs +2 -0
- package/dist/translation-provenance-B41lEDM-.mjs.map +1 -0
- package/dist/translation-provenance-BZ0MlbFR.d.mts +253 -0
- package/dist/translation-provenance-BZ0MlbFR.d.mts.map +1 -0
- package/dist/translation.d.mts +144 -0
- package/dist/translation.d.mts.map +1 -0
- package/dist/translation.mjs +2 -0
- package/dist/translation.mjs.map +1 -0
- package/dist/types-BOk9tg_9.d.mts +150 -0
- package/dist/types-BOk9tg_9.d.mts.map +1 -0
- package/dist/versionable-Br_G5VGR.mjs +2 -0
- package/dist/versionable-Br_G5VGR.mjs.map +1 -0
- package/dist/versionable-CRvV87nZ.d.mts +18 -0
- package/dist/versionable-CRvV87nZ.d.mts.map +1 -0
- package/dist/versions-w2ui-Vzb.mjs +2 -0
- package/dist/versions-w2ui-Vzb.mjs.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./content-config-store-8omQF5p5.mjs";import{t}from"./registry-CaPbY5lm.mjs";import"./locale-utils-B0mFoV47.mjs";import{blockTreeFieldsOf as n}from"@murumets-ee/entity";import{getApp as r,getContext as i,isTokenDerivedDoor as a}from"@murumets-ee/core";import{column as o,defineTable as s}from"@murumets-ee/db";import{AsyncResource as c}from"node:async_hooks";import{createHash as l}from"node:crypto";import{isProseText as u,treeTextStrings as d}from"@murumets-ee/blocks/translation-text";const f=s({name:`toolkit_translation_provenance`,columns:{id:o.uuid({primaryKey:!0,defaultRandom:!0}),entityType:o.varchar({length:100,notNull:!0,pgName:`entity_type`}),entityId:o.varchar({length:255,notNull:!0,pgName:`entity_id`}),locale:o.varchar({length:10,notNull:!0}),contentHash:o.varchar({length:64,notNull:!0,pgName:`content_hash`}),contentChangedAt:o.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`content_changed_at`}),sourceLocale:o.varchar({length:10,pgName:`source_locale`}),sourceHash:o.varchar({length:64,pgName:`source_hash`}),method:o.varchar({length:10}),reviewedAt:o.timestamp({withTimezone:!0,pgName:`reviewed_at`}),reviewedBy:o.varchar({length:255,pgName:`reviewed_by`}),updatedAt:o.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`}),updatedBy:o.varchar({length:255,pgName:`updated_by`})},unique:[{on:[`entityType`,`entityId`,`locale`],name:`uq_translation_provenance_doc_locale`}]}),p=f.table;function m(e){return e.allFields}function h(e){let t=[];for(let[n,r]of Object.entries(m(e)))r.translatable===!0&&(r.type===`text`||r.type===`richtext`)&&t.push(n);return t}function g(e){let t=m(e);return n(e).map(e=>e.name).filter(e=>t[e]?.translatable===!0)}function _(e,t){let n=[];for(let r of h(e)){let e=t[r];typeof e==`string`&&u(e)&&n.push([r,e])}return n}function v(e){return h(e).length>0||g(e).length>0}const y=/<\/?(?:a|abbr|b|code|em|i|mark|s|small|span|strong|sub|sup|u)(?:\s[^<>]*)?\/?>/gi;function b(e){return e.replace(y,``).replace(/<[^<>]*>/g,` `)}function x(e){return e.replace(/\s+/g,` `).trim()}function S(e,t){let n=[],r=m(e);for(let i of h(e)){let e=t[i];if(typeof e!=`string`||!u(e))continue;let a=r[i]?.type===`richtext`?b(e):e;n.push(`${i}=${x(a)}`)}for(let r of g(e)){let e=t[r];if(Array.isArray(e))for(let t of d(e)){let e=x(t);e!==``&&n.push(e)}}return n.sort(),l(`sha256`).update(JSON.stringify(n)).digest(`hex`)}function C(e,t){return e===`_`?t:e}function w(e=r().db.readWrite){return f.makeClient(e)}function T(){try{return r()}catch{return}}function E(e){let t=e.method;return{locale:String(e.locale),contentHash:String(e.contentHash),contentChangedAt:e.contentChangedAt,sourceLocale:typeof e.sourceLocale==`string`?e.sourceLocale:null,sourceHash:typeof e.sourceHash==`string`?e.sourceHash:null,method:t===`ai`||t===`human`||t===`copy`?t:null,reviewedAt:e.reviewedAt??null,reviewedBy:typeof e.reviewedBy==`string`?e.reviewedBy:null}}async function D(e,n,i,a,o){let s=r(),c=i===a,l=await s.getClient(e).findById(n,{...c?{}:{locale:i,defaultLocale:a}});if(!l)return null;if(o!==void 0)return{row:l,working:{...l,...o},drafted:!0};let{drafts:u}=t(e.name);if(!u)return{row:l,working:l,drafted:!1};let{siteForDocumentType:d}=await import(`./document-site-B8tmEqLw.mjs`).then(e=>e.t),f=(await u.makeClient(s.db.readWrite,await d(e.name)).findOne({entityId:n,locale:c?`_`:i}))?.data;return f&&typeof f==`object`?{row:l,working:{...l,...f},drafted:!0}:{row:l,working:l,drafted:!1}}async function O(t){let{entityName:n,entityId:r}=t,o=T();if(o)try{let s=o.entities.get(n);if(!s||!v(s))return;let c=e().defaultLocale,l=C(t.locale,c),u=await D(s,r,l,c,t.draftData===void 0?void 0:{...t.draftData});if(!u)return;let d=S(s,u.working),f=w(),p=new Date,m={entityType:s.name,entityId:r};if(t.contentOnly===!0||l===c&&t.sourceLocale===void 0){let e=t.contentOnly===!0||t.reverted===!0?void 0:t.userId;await k(f,{...m,locale:l},d,p,e);return}let h=C(t.sourceLocale??c,c);if(h===l)return;let g=t.sourceHash;if(g===void 0){let e=await f.findOne({...m,locale:h});if(e)g=String(e.contentHash);else{let e=await D(s,r,h,c);if(!e)return;g=S(s,e.working),await k(f,{...m,locale:h},g,p,void 0)}}let _=await f.findOne({...m,locale:l}),y=_===null?u.drafted?S(s,u.row)!==d:d!==g:_.contentHash!==d,b=t.method===`ai`||y&&t.reverted!==!0&&a(i()?.door),x=t.userId!==void 0&&t.reverted!==!0&&(_===null||_.contentHash!==d),T=_?.method===`ai`,E=b||T?`ai`:d===g?`copy`:`human`;await f.upsert({...m,locale:l,contentHash:d,contentChangedAt:p,sourceLocale:h,sourceHash:g,method:E,reviewedAt:null,reviewedBy:null,updatedAt:p,updatedBy:t.userId??null},{target:[`entityType`,`entityId`,`locale`],set:{contentHash:d,...(_===null||_.contentHash!==d)&&{contentChangedAt:p},sourceLocale:h,sourceHash:g,method:E,...b&&{reviewedAt:null,reviewedBy:null},updatedAt:p,...x&&{updatedBy:t.userId}}})}catch(e){o.logger.warn({err:e,entity:n,entityId:r,locale:t.locale},`translation provenance: could not record a locale save`)}}async function k(e,t,n,r,i){let a=await e.findOne(t);a&&a.contentHash===n||await e.upsert({...t,contentHash:n,contentChangedAt:r,updatedAt:r,updatedBy:i??null},{target:[`entityType`,`entityId`,`locale`],set:{contentHash:n,contentChangedAt:r,updatedAt:r,...i!==void 0&&{updatedBy:i}}})}async function A(e){let t=w(),n={entityType:e.entityType,entityId:e.entityId,locale:e.locale},r=await t.findOne(n);if(!r)return`none`;if(e.refuseSelf===!0&&r.updatedBy===e.userId)return`self`;let i=new Date;return await t.update(n,{reviewedAt:i,reviewedBy:e.userId,updatedAt:i}),`reviewed`}async function j(e,t,n){let r=new Map;if(t.length===0)return r;let i=await w(n).findMany({where:{entityType:{eq:e},entityId:{in:[...t]}}});for(let e of i){let t=String(e.entityId),n=r.get(t);n||(n=new Map,r.set(t,n));let i=E(e);n.set(i.locale,i)}return r}function M(e,t){let n=e?.get(t);if(!n)return null;let r=n.sourceLocale===null?void 0:e?.get(n.sourceLocale),i=r!==void 0&&n.sourceHash!==null&&r.contentHash!==n.sourceHash;return{method:n.method,sourceLocale:n.sourceLocale,outdated:i,outdatedSince:i&&r?new Date(r.contentChangedAt).toISOString():null,machineUnreviewed:n.method===`ai`&&n.reviewedAt===null,reviewedAt:n.reviewedAt?new Date(n.reviewedAt).toISOString():null}}const N=new Map,P=new Map;function F(){for(let[e,t]of N){if(P.size>=2)return;if(P.has(e))continue;N.delete(e);let n=t.run().finally(()=>{P.delete(e),F()});P.set(e,n)}}function I(e){let t=T();if(!t)return;let n=R(e.entityName,e.entityId,e.locale);if(!N.has(n)&&N.size>=200){t.logger.warn({entity:e.entityName,entityId:e.entityId,locale:e.locale,waiting:N.size},`translation provenance: too many recordings waiting — this save was not recorded`);return}N.set(n,{run:c.bind(()=>O(e))}),F()}const L=e=>{if(e.action===`delete`)return;let t=T();if(!t)return;let n=t.entities.get(e.entityName);!n||!v(n)||I({entityName:e.entityName,entityId:e.id,locale:e.locale??`_`,userId:i()?.user?.id})};function R(e,t,n){return`${e}:${t}:${n}`}async function z(e,t,n,r=3e3){let i=R(e,t,n),a=Date.now()+r;for(;Date.now()<a;){let e=P.get(i);if(e!==void 0){await Promise.race([e,new Promise(e=>setTimeout(e,50))]);continue}if(!N.has(i))return;await new Promise(e=>setTimeout(e,5))}}async function B(){for(;P.size>0||N.size>0;)await new Promise(e=>setTimeout(e,5))}export{z as a,O as c,g as d,M as f,v as i,_ as l,p as m,I as n,A as o,L as p,j as r,B as s,S as t,h as u};
|
|
2
|
+
//# sourceMappingURL=translation-provenance-B41lEDM-.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translation-provenance-B41lEDM-.mjs","names":[],"sources":["../src/schema/translation-provenance.ts","../src/translation-provenance.ts"],"sourcesContent":["/**\n * Translation provenance — what each locale of a document was last saved FROM\n * (agent-access D017).\n *\n * One row per (document, locale), the source locale included:\n *\n * - `contentHash` is a hash of that locale's own reader text as of its last\n * save. For the SOURCE locale this is the value every translation is\n * compared against, and `contentChangedAt` is when it last moved — the date\n * in \"Outdated since …\".\n * - `sourceLocale` / `sourceHash` record, for a translation, which locale it\n * was made from and that locale's `contentHash` at the time. A translation\n * is outdated when the source's `contentHash` no longer equals it.\n * - `method` is how the text got there (`ai` | `human` | `copy`); machine\n * output is \"not reviewed\" until `reviewedAt` is set.\n *\n * Storing the source's hash on its OWN row is what keeps \"outdated\" a\n * comparison of two stored values: a list or a report never has to re-read and\n * re-hash a document to answer it.\n *\n * Shared, not per-entity (like `toolkit_content_locks`). A row is addressed by\n * a document's uuid and is only ever read for a document the caller has\n * already been handed by its own, site-confined client.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\nexport const translationProvenanceTable = defineTable({\n name: 'toolkit_translation_provenance',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityType: column.varchar({ length: 100, notNull: true, pgName: 'entity_type' }),\n entityId: column.varchar({ length: 255, notNull: true, pgName: 'entity_id' }),\n /** A real locale code — the default locale by its code, never `'_'`. */\n locale: column.varchar({ length: 10, notNull: true }),\n contentHash: column.varchar({ length: 64, notNull: true, pgName: 'content_hash' }),\n contentChangedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'content_changed_at',\n }),\n sourceLocale: column.varchar({ length: 10, pgName: 'source_locale' }),\n sourceHash: column.varchar({ length: 64, pgName: 'source_hash' }),\n method: column.varchar({ length: 10 }),\n reviewedAt: column.timestamp({ withTimezone: true, pgName: 'reviewed_at' }),\n // A user id — better-auth ids are random strings, never uuids.\n reviewedBy: column.varchar({ length: 255, pgName: 'reviewed_by' }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n updatedBy: column.varchar({ length: 255, pgName: 'updated_by' }),\n },\n // Named explicitly: drizzle's default\n // (`toolkit_translation_provenance_entity_type_entity_id_locale_unique`) is\n // 66 characters, over Postgres's 63-character identifier limit.\n unique: [\n { on: ['entityType', 'entityId', 'locale'], name: 'uq_translation_provenance_doc_locale' },\n ],\n})\n\nexport const toolkitTranslationProvenance = translationProvenanceTable.table\n","/**\n * Translation provenance — recording what each locale was saved from, and\n * reading \"outdated\" / \"machine translated, not reviewed\" back\n * (agent-access D017).\n *\n * WHAT IS HASHED. A locale's reader TEXT, as the editor would show it: the\n * stored row with that locale's draft laid over it. Text means the entity's\n * translatable `text` / `richtext` fields and the text of every block tree\n * (`treeTextStrings`, the same walk machine translation uses). The strings are\n * hashed as a SORTED list, so moving a block changes nothing; an image, a link\n * or a design field is not text, so swapping one changes nothing either.\n * Slugs are derived, not written, and are left out.\n *\n * WHERE IT IS RECORDED. No single hook sees every locale save (R016), so there\n * are two:\n *\n * - the DRAFT writers record for themselves — this is every edit a person\n * makes to publishable content. `mergeDraft` / `saveDraft` / `deleteDraft`\n * await {@link recordLocaleSave}; the block editor's\n * `DocumentDraftStore.writeDraft` runs inside op-commit's transaction, so it\n * QUEUES the recording ({@link enqueueLocaleSave}) instead of awaiting it;\n * - a write that lands on the live row without a draft (a generic PATCH, a\n * version restore, a seeder, a non-publishable entity) reaches\n * {@link translationProvenanceWriteListener} through\n * `'entity.writeListeners'`.\n *\n * Publish needs no recording of its own: it moves the draft's text onto the\n * row, and the working text — draft over row — is the same before and after.\n *\n * NEVER FAILS A SAVE, AND NEVER HOLDS ONE UP. Recording runs on its own\n * connection, outside the caller's transaction — which also means a save that\n * rolls back can leave its hash recorded until the next save corrects it — and\n * every failure is logged and dropped: a missing\n * provenance row reads as \"unknown\", which is what every document saved before\n * this table existed already is.\n */\n\nimport { AsyncResource } from 'node:async_hooks'\nimport { createHash } from 'node:crypto'\nimport { isProseText, treeTextStrings } from '@murumets-ee/blocks/translation-text'\nimport { getApp, getContext, isTokenDerivedDoor } from '@murumets-ee/core'\nimport { blockTreeFieldsOf, type Entity, type FieldConfig } from '@murumets-ee/entity'\nimport type { EntityWriteObserver } from '@murumets-ee/entity/admin'\nimport { getContentConfig } from './content-config-store.js'\nimport { DEFAULT_LOCALE_SENTINEL } from './locale-utils.js'\nimport { getContentTables } from './schema/registry.js'\nimport { translationProvenanceTable } from './schema/translation-provenance.js'\n\n/** How a locale's text got there. */\nexport type TranslationMethod = 'ai' | 'human' | 'copy'\n\n/** One locale's stored provenance. */\nexport interface TranslationProvenance {\n locale: string\n contentHash: string\n contentChangedAt: Date\n sourceLocale: string | null\n sourceHash: string | null\n method: TranslationMethod | null\n reviewedAt: Date | null\n reviewedBy: string | null\n}\n\ntype Row = Record<string, unknown>\ntype Fields = Readonly<Record<string, FieldConfig>>\n\nfunction fieldsOf(entity: Pick<Entity, 'allFields'>): Fields {\n return entity.allFields as Fields\n}\n\n/**\n * The entity's translatable fields whose value is prose: `text` and `richtext`.\n * A slug is derived from its source field, and a select, a reference or a\n * media id is not text. The translation engine translates exactly these.\n */\nexport function translatableTextFields(entity: Pick<Entity, 'allFields'>): string[] {\n const out: string[] = []\n for (const [name, config] of Object.entries(fieldsOf(entity))) {\n if (config.translatable !== true) continue\n if (config.type === 'text' || config.type === 'richtext') out.push(name)\n }\n return out\n}\n\n/** The entity's translatable block-tree fields. */\nexport function translatableTreeFields(entity: Pick<Entity, 'allFields'>): string[] {\n const fields = fieldsOf(entity)\n return blockTreeFieldsOf(entity as Entity)\n .map((ref) => ref.name)\n .filter((name) => fields[name]?.translatable === true)\n}\n\n/** Whether a document of this entity has any text a translation could go stale against. */\n/**\n * The prose a document's translatable text fields hold right now: `[field,\n * text]` for every field whose value is a sentence — not an address (a\n * translatable `canonicalUrl` is a `text` field too) and not empty.\n */\nexport function translatableFieldTexts(\n entity: Pick<Entity, 'allFields'>,\n data: Readonly<Record<string, unknown>>,\n): [string, string][] {\n const out: [string, string][] = []\n for (const name of translatableTextFields(entity)) {\n const value = data[name]\n if (typeof value === 'string' && isProseText(value)) out.push([name, value])\n }\n return out\n}\n\nexport function hasTranslatableText(entity: Pick<Entity, 'allFields'>): boolean {\n return translatableTextFields(entity).length > 0 || translatableTreeFields(entity).length > 0\n}\n\nconst INLINE_TAG =\n /<\\/?(?:a|abbr|b|code|em|i|mark|s|small|span|strong|sub|sup|u)(?:\\s[^<>]*)?\\/?>/gi\n\n/**\n * The words of an HTML string. An INLINE tag disappears (bold in the middle of\n * a word, or before a comma, is the same words); any other tag is a break\n * between words.\n */\nfunction htmlText(html: string): string {\n return html.replace(INLINE_TAG, '').replace(/<[^<>]*>/g, ' ')\n}\n\nfunction squash(text: string): string {\n return text.replace(/\\s+/g, ' ').trim()\n}\n\n/**\n * Hash the reader text of one locale's data (a row, or a row with a draft laid\n * over it). Order-independent across blocks; whitespace-insensitive.\n */\nexport function documentTextHash(entity: Pick<Entity, 'allFields'>, data: Row): string {\n const parts: string[] = []\n const fields = fieldsOf(entity)\n for (const name of translatableTextFields(entity)) {\n const value = data[name]\n if (typeof value !== 'string' || !isProseText(value)) continue\n // An entity `richtext` column is an HTML string: its tags are not text, so\n // changing a link's target or making a word bold is not a text change.\n const text = fields[name]?.type === 'richtext' ? htmlText(value) : value\n parts.push(`${name}=${squash(text)}`)\n }\n for (const name of translatableTreeFields(entity)) {\n const value = data[name]\n if (!Array.isArray(value)) continue\n for (const text of treeTextStrings(value as never)) {\n const squashed = squash(text)\n if (squashed !== '') parts.push(squashed)\n }\n }\n parts.sort()\n return createHash('sha256').update(JSON.stringify(parts)).digest('hex')\n}\n\n/** `'_'` → the configured default locale's code; anything else unchanged. */\nfunction realLocale(locale: string, defaultLocale: string): string {\n return locale === DEFAULT_LOCALE_SENTINEL ? defaultLocale : locale\n}\n\ntype Db = Parameters<typeof translationProvenanceTable.makeClient>[0]\n\nfunction provenanceClient(db: Db = getApp().db.readWrite) {\n return translationProvenanceTable.makeClient(db)\n}\n\n/** The app, or `undefined` in a realm that has none (a unit test of a neighbouring module). */\nfunction appOrNone(): ReturnType<typeof getApp> | undefined {\n try {\n return getApp()\n } catch {\n // No app in this realm — there is no database to record against either.\n return undefined\n }\n}\n\nfunction toProvenance(row: Row): TranslationProvenance {\n const method = row.method\n return {\n locale: String(row.locale),\n contentHash: String(row.contentHash),\n contentChangedAt: row.contentChangedAt as Date,\n sourceLocale: typeof row.sourceLocale === 'string' ? row.sourceLocale : null,\n sourceHash: typeof row.sourceHash === 'string' ? row.sourceHash : null,\n method: method === 'ai' || method === 'human' || method === 'copy' ? method : null,\n reviewedAt: (row.reviewedAt as Date | null | undefined) ?? null,\n reviewedBy: typeof row.reviewedBy === 'string' ? row.reviewedBy : null,\n }\n}\n\n/**\n * One locale's stored row (locale-merged) and its working data — the row with\n * that locale's draft, if any, laid over it. `null` when the caller cannot read\n * the row.\n */\nasync function readWorkingData(\n entity: Entity,\n entityId: string,\n locale: string,\n defaultLocale: string,\n draftData?: Row,\n): Promise<{ row: Row; working: Row; drafted: boolean } | null> {\n const app = getApp()\n const isDefault = locale === defaultLocale\n const row = (await app.getClient(entity).findById(entityId, {\n ...(isDefault ? {} : { locale, defaultLocale }),\n })) as Row | null\n if (!row) return null\n if (draftData !== undefined) return { row, working: { ...row, ...draftData }, drafted: true }\n const { drafts } = getContentTables(entity.name)\n if (!drafts) return { row, working: row, drafted: false }\n const { siteForDocumentType } = await import('./internal/document-site.js')\n const draft = await drafts\n .makeClient(app.db.readWrite, await siteForDocumentType(entity.name))\n .findOne({ entityId, locale: isDefault ? DEFAULT_LOCALE_SENTINEL : locale })\n const data = draft?.data\n return data && typeof data === 'object'\n ? { row, working: { ...row, ...(data as Row) }, drafted: true }\n : { row, working: row, drafted: false }\n}\n\n/** What {@link recordLocaleSave} is told about the save. */\nexport interface LocaleSaveInput {\n /** The entity type. One the app does not know is ignored. */\n entityName: string\n entityId: string\n /** The saved locale — a real code, or `'_'` for the default locale. */\n locale: string\n /** Who saved. */\n userId?: string | undefined\n /**\n * The locale's whole draft AS JUST WRITTEN, when the caller holds it. A draft\n * writer inside a transaction must pass it: this module reads on its own\n * connection and would otherwise see the draft as it was before the save.\n */\n draftData?: Readonly<Record<string, unknown>> | undefined\n /**\n * `'ai'` when the caller KNOWS the text is machine output (the translation\n * engine). Left out, a save through a token-derived door is `'ai'` too — an\n * agent typing a translation is a machine translating — and a person's save\n * is `'human'`, or `'copy'` when the text is identical to the source's.\n */\n method?: 'ai' | undefined\n /**\n * This save REMOVED text rather than wrote it (a draft was discarded). It is\n * recorded — the working text changed back to the row's — but it is never\n * machine output, whoever did it: nobody wrote a word.\n */\n reverted?: boolean | undefined\n /**\n * Refresh this locale's TEXT HASH only — never its method, its source or its\n * review. For a locale that was READ as a source, not written: without it a\n * non-default source would be re-judged as a translation, under the caller's\n * door, and a hand-written Russian page an agent translated FROM would be\n * recorded as machine output.\n */\n contentOnly?: boolean | undefined\n /** The locale the text was translated from. Defaults to the default locale. */\n sourceLocale?: string | undefined\n /**\n * The source's text hash AS TRANSLATED, when the caller translated something\n * other than the source's current working text (its published row).\n */\n sourceHash?: string | undefined\n}\n\n/**\n * Record one locale save. Reads that locale's working text, stores its hash,\n * and — for a translation — the source hash it now corresponds to.\n *\n * Machine output stays `ai` and unreviewed through later human edits: fixing a\n * typo is not a review. Only \"Mark reviewed\" ({@link markTranslationReviewed})\n * clears it, and a fresh machine translation sets it again.\n */\nexport async function recordLocaleSave(input: LocaleSaveInput): Promise<void> {\n const { entityName, entityId } = input\n const app = appOrNone()\n if (!app) return\n try {\n const entity = app.entities.get(entityName)\n if (!entity || !hasTranslatableText(entity)) return\n const defaultLocale = getContentConfig().defaultLocale\n const locale = realLocale(input.locale, defaultLocale)\n const read = await readWorkingData(\n entity,\n entityId,\n locale,\n defaultLocale,\n input.draftData === undefined ? undefined : { ...input.draftData },\n )\n if (!read) return\n const contentHash = documentTextHash(entity, read.working)\n const client = provenanceClient()\n const now = new Date()\n const key = { entityType: entity.name, entityId }\n\n if (\n input.contentOnly === true ||\n (locale === defaultLocale && input.sourceLocale === undefined)\n ) {\n // A hash-only refresh and a discard author nothing — no user is recorded.\n const author =\n input.contentOnly === true || input.reverted === true ? undefined : input.userId\n await upsertContent(client, { ...key, locale }, contentHash, now, author)\n return\n }\n\n const sourceLocale = realLocale(input.sourceLocale ?? defaultLocale, defaultLocale)\n if (sourceLocale === locale) return\n let sourceHash = input.sourceHash\n if (sourceHash === undefined) {\n // The source's own row is the comparison target, so make sure it exists:\n // a document last saved before this table did has none.\n const source = await client.findOne({ ...key, locale: sourceLocale })\n if (source) {\n sourceHash = String(source.contentHash)\n } else {\n const sourceData = await readWorkingData(entity, entityId, sourceLocale, defaultLocale)\n if (!sourceData) return\n sourceHash = documentTextHash(entity, sourceData.working)\n await upsertContent(client, { ...key, locale: sourceLocale }, sourceHash, now, undefined)\n }\n }\n\n const existing = await client.findOne({ ...key, locale })\n // Did this save change the locale's TEXT? Against the recorded hash when\n // there is one. A locale with nothing recorded (every document saved before\n // this table existed, and every brand-new translation) has no hash to\n // compare with, so:\n // - with a DRAFT over the row, the row is the \"before\": a draft whose text\n // equals the row's changed no words — an agent swapping an image in a\n // hand-translated page has not machine-translated it;\n // - with NO draft, the write landed on the row itself and there is no\n // \"before\" left to read. Then the question is whether the locale holds a\n // translation at all (its text differs from the source's). That errs\n // toward `ai` for an agent's first write to an old hand translation —\n // a badge a person clears with \"Mark reviewed\" — and never toward\n // recording machine text as human, which is the failure that matters.\n const changed =\n existing !== null\n ? existing.contentHash !== contentHash\n : read.drafted\n ? documentTextHash(entity, read.row) !== contentHash\n : contentHash !== sourceHash\n // Machine output is: what the engine says it wrote, or TEXT that arrived\n // through a token-derived door. The second needs the text to have changed —\n // an agent publishing, or re-saving the same words, has written nothing,\n // and must not turn a reviewed translation back into an unreviewed one. A\n // discard writes nothing either.\n const machine =\n input.method === 'ai' ||\n (changed && input.reverted !== true && isTokenDerivedDoor(getContext()?.door))\n const authored =\n input.userId !== undefined &&\n input.reverted !== true &&\n (existing === null || existing.contentHash !== contentHash)\n const wasMachine = existing?.method === 'ai'\n const method: TranslationMethod =\n machine || wasMachine ? 'ai' : contentHash === sourceHash ? 'copy' : 'human'\n await client.upsert(\n {\n ...key,\n locale,\n contentHash,\n contentChangedAt: now,\n sourceLocale,\n sourceHash,\n method,\n reviewedAt: null,\n reviewedBy: null,\n updatedAt: now,\n updatedBy: input.userId ?? null,\n },\n {\n target: ['entityType', 'entityId', 'locale'],\n set: {\n contentHash,\n ...((existing === null || existing.contentHash !== contentHash) && {\n contentChangedAt: now,\n }),\n sourceLocale,\n sourceHash,\n method,\n // Fresh machine output is unreviewed again; anything else keeps the\n // review it had.\n ...(machine && { reviewedAt: null, reviewedBy: null }),\n updatedAt: now,\n // WHO LAST WROTE THE TEXT — what \"an agent may not review its own\n // translation\" is checked against. So it moves only when this save\n // changed the words AND says who did: a discard, a publish or a\n // recording with no known user wrote nothing, and letting one of\n // them blank it would let an agent launder its own authorship\n // (write, stage a draft, discard it, then review).\n ...(authored && { updatedBy: input.userId }),\n },\n },\n )\n } catch (error) {\n // Deliberately swallowed — see the module docblock (\"never fails a save\").\n app.logger.warn(\n { err: error, entity: entityName, entityId, locale: input.locale },\n 'translation provenance: could not record a locale save',\n )\n }\n}\n\nasync function upsertContent(\n client: ReturnType<typeof provenanceClient>,\n key: { entityType: string; entityId: string; locale: string },\n contentHash: string,\n now: Date,\n userId: string | undefined,\n): Promise<void> {\n const existing = await client.findOne(key)\n if (existing && existing.contentHash === contentHash) return\n await client.upsert(\n {\n ...key,\n contentHash,\n contentChangedAt: now,\n updatedAt: now,\n updatedBy: userId ?? null,\n },\n {\n target: ['entityType', 'entityId', 'locale'],\n // The author moves only when one is known — see `authored` above.\n set: {\n contentHash,\n contentChangedAt: now,\n updatedAt: now,\n ...(userId !== undefined && { updatedBy: userId }),\n },\n },\n )\n}\n\n/**\n * Mark a translation reviewed. The caller checks permission.\n *\n * - `'none'` — nothing is recorded for that locale; there is nothing to review.\n * - `'self'` — `refuseSelf` was set and the reviewer is the one who last wrote\n * the text. A review is someone ELSE vouching for it: an agent that could\n * clear \"not reviewed\" on its own output would make the badge say nothing\n * (the approval queue refuses the asking principal for the same reason).\n */\nexport async function markTranslationReviewed(input: {\n entityType: string\n entityId: string\n locale: string\n userId: string\n refuseSelf?: boolean | undefined\n}): Promise<'reviewed' | 'none' | 'self'> {\n const client = provenanceClient()\n const key = { entityType: input.entityType, entityId: input.entityId, locale: input.locale }\n const existing = await client.findOne(key)\n if (!existing) return 'none'\n if (input.refuseSelf === true && existing.updatedBy === input.userId) return 'self'\n const now = new Date()\n // `updatedBy` is left alone: it is who last WROTE the text.\n await client.update(key, { reviewedAt: now, reviewedBy: input.userId, updatedAt: now })\n return 'reviewed'\n}\n\n/** Every stored provenance row for a set of documents of one entity type — ONE query. */\nexport async function getTranslationProvenanceBatch(\n entityType: string,\n entityIds: readonly string[],\n db?: Db,\n): Promise<Map<string, Map<string, TranslationProvenance>>> {\n const out = new Map<string, Map<string, TranslationProvenance>>()\n if (entityIds.length === 0) return out\n const rows = await provenanceClient(db).findMany({\n where: { entityType: { eq: entityType }, entityId: { in: [...entityIds] } },\n })\n for (const row of rows) {\n const id = String(row.entityId)\n let byLocale = out.get(id)\n if (!byLocale) {\n byLocale = new Map()\n out.set(id, byLocale)\n }\n const provenance = toProvenance(row as Row)\n byLocale.set(provenance.locale, provenance)\n }\n return out\n}\n\n/** What a translation's provenance says about it, against its source's. */\nexport interface TranslationFreshness {\n method: TranslationMethod | null\n sourceLocale: string | null\n /** The source text has changed since this translation was saved. */\n outdated: boolean\n /** When the source text changed — ISO, only when {@link outdated}. */\n outdatedSince: string | null\n /** Machine output nobody has marked reviewed. */\n machineUnreviewed: boolean\n reviewedAt: string | null\n}\n\n/** Compare one locale's provenance with its source's. `null` when nothing is recorded. */\nexport function translationFreshness(\n byLocale: ReadonlyMap<string, TranslationProvenance> | undefined,\n locale: string,\n): TranslationFreshness | null {\n const own = byLocale?.get(locale)\n if (!own) return null\n const source = own.sourceLocale !== null ? byLocale?.get(own.sourceLocale) : undefined\n const outdated =\n source !== undefined && own.sourceHash !== null && source.contentHash !== own.sourceHash\n return {\n method: own.method,\n sourceLocale: own.sourceLocale,\n outdated,\n outdatedSince: outdated && source ? new Date(source.contentChangedAt).toISOString() : null,\n machineUnreviewed: own.method === 'ai' && own.reviewedAt === null,\n reviewedAt: own.reviewedAt ? new Date(own.reviewedAt).toISOString() : null,\n }\n}\n\n// ---------------------------------------------------------------------------\n// The queue — recordings that must not be awaited by the write that caused them\n// ---------------------------------------------------------------------------\n\n/**\n * A recording waiting to run. `run` is bound to the async context it was\n * QUEUED in (`AsyncResource.bind`): the queue is drained from the completion of\n * whichever recording finished last, and without the binding a queued recording\n * would run under THAT request's context — another caller's door, user and\n * site (review, PR10).\n */\ninterface Pending {\n run: () => Promise<void>\n}\n\n/** Keyed so a burst of writes to one document and locale collapses to the last. */\nconst pending = new Map<string, Pending>()\n/** Recordings running now, by key — one document and locale is never recorded twice at once. */\nconst inFlight = new Map<string, Promise<void>>()\n\n/** Recordings in flight at once — far below any connection pool. */\nconst MAX_CONCURRENT = 2\n/**\n * Beyond this many waiting, new ones are dropped (and logged): a bulk import\n * must not queue without end, and each entry may hold a draft's data.\n */\nconst MAX_PENDING = 200\n\nfunction drain(): void {\n for (const [key, entry] of pending) {\n if (inFlight.size >= MAX_CONCURRENT) return\n // The same document and locale is already being recorded: this one waits\n // for it. Started together, the OLDER one could finish last and leave its\n // hash behind — two quick edits of one page are ordinary.\n if (inFlight.has(key)) continue\n pending.delete(key)\n const done = entry.run().finally(() => {\n inFlight.delete(key)\n drain()\n })\n inFlight.set(key, done)\n }\n}\n\n/**\n * Record a locale save WITHOUT being awaited by it: bounded, coalesced per\n * document and locale, and run under the caller's own request context.\n *\n * For a writer that holds a transaction (the block editor's draft write inside\n * op-commit's): awaiting a recording there would take a SECOND pool connection\n * while holding the first, and enough concurrent commits would each wait for a\n * connection none of them can release.\n */\nexport function enqueueLocaleSave(input: LocaleSaveInput): void {\n const app = appOrNone()\n if (!app) return\n const key = queueKey(input.entityName, input.entityId, input.locale)\n if (!pending.has(key) && pending.size >= MAX_PENDING) {\n app.logger.warn(\n {\n entity: input.entityName,\n entityId: input.entityId,\n locale: input.locale,\n waiting: pending.size,\n },\n 'translation provenance: too many recordings waiting — this save was not recorded',\n )\n return\n }\n pending.set(key, { run: AsyncResource.bind(() => recordLocaleSave(input)) })\n drain()\n}\n\n/**\n * A write that landed on the live row — the `'entity.writeListeners'`\n * contribution. Synchronous and `void`, per the slot's contract.\n *\n * A `delete` is NOT recorded and removes nothing: the event does not say\n * whether the row was trashed or purged, and a trashed document that is\n * restored must come back with what it was translated from and whether that\n * was reviewed. A purged document's rows are a few hundred bytes addressed by\n * a uuid that is never reused.\n */\nexport const translationProvenanceWriteListener: EntityWriteObserver = (event) => {\n if (event.action === 'delete') return\n const app = appOrNone()\n if (!app) return\n const entity = app.entities.get(event.entityName)\n if (!entity || !hasTranslatableText(entity)) return\n enqueueLocaleSave({\n entityName: event.entityName,\n entityId: event.id,\n locale: event.locale ?? DEFAULT_LOCALE_SENTINEL,\n userId: getContext()?.user?.id,\n })\n}\n\nfunction queueKey(entityName: string, entityId: string, locale: string): string {\n return `${entityName}:${entityId}:${locale}`\n}\n\n/**\n * Resolves once the queued recordings for ONE document and locale have run —\n * for a caller about to write that row itself, which must come last. Bounded:\n * it waits for that key only, never the whole queue, and gives up after\n * `maxMs` rather than hold a request on a recording that will not settle.\n */\nexport async function localeSaveSettled(\n entityName: string,\n entityId: string,\n locale: string,\n maxMs = 3000,\n): Promise<void> {\n const key = queueKey(entityName, entityId, locale)\n const deadline = Date.now() + maxMs\n while (Date.now() < deadline) {\n const running = inFlight.get(key)\n if (running !== undefined) {\n await Promise.race([running, new Promise((resolve) => setTimeout(resolve, 50))])\n continue\n }\n if (!pending.has(key)) return\n await new Promise((resolve) => setTimeout(resolve, 5))\n }\n}\n\n/** Resolves once every queued recording has run. For tests; a request uses {@link localeSaveSettled}. */\nexport async function provenanceQueueIdle(): Promise<void> {\n while (inFlight.size > 0 || pending.size > 0)\n await new Promise((resolve) => setTimeout(resolve, 5))\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Entity } from "@murumets-ee/entity";
|
|
2
|
+
//#region src/schema/translation-provenance.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Translation provenance — what each locale of a document was last saved FROM
|
|
5
|
+
* (agent-access D017).
|
|
6
|
+
*
|
|
7
|
+
* One row per (document, locale), the source locale included:
|
|
8
|
+
*
|
|
9
|
+
* - `contentHash` is a hash of that locale's own reader text as of its last
|
|
10
|
+
* save. For the SOURCE locale this is the value every translation is
|
|
11
|
+
* compared against, and `contentChangedAt` is when it last moved — the date
|
|
12
|
+
* in "Outdated since …".
|
|
13
|
+
* - `sourceLocale` / `sourceHash` record, for a translation, which locale it
|
|
14
|
+
* was made from and that locale's `contentHash` at the time. A translation
|
|
15
|
+
* is outdated when the source's `contentHash` no longer equals it.
|
|
16
|
+
* - `method` is how the text got there (`ai` | `human` | `copy`); machine
|
|
17
|
+
* output is "not reviewed" until `reviewedAt` is set.
|
|
18
|
+
*
|
|
19
|
+
* Storing the source's hash on its OWN row is what keeps "outdated" a
|
|
20
|
+
* comparison of two stored values: a list or a report never has to re-read and
|
|
21
|
+
* re-hash a document to answer it.
|
|
22
|
+
*
|
|
23
|
+
* Shared, not per-entity (like `toolkit_content_locks`). A row is addressed by
|
|
24
|
+
* a document's uuid and is only ever read for a document the caller has
|
|
25
|
+
* already been handed by its own, site-confined client.
|
|
26
|
+
*/
|
|
27
|
+
declare const translationProvenanceTable: {
|
|
28
|
+
table: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
29
|
+
name: string;
|
|
30
|
+
schema: undefined;
|
|
31
|
+
columns: {
|
|
32
|
+
[x: string]: import("drizzle-orm/pg-core").PgColumn<{
|
|
33
|
+
name: string;
|
|
34
|
+
tableName: string;
|
|
35
|
+
dataType: import("drizzle-orm").ColumnDataType;
|
|
36
|
+
columnType: string;
|
|
37
|
+
data: unknown;
|
|
38
|
+
driverParam: unknown;
|
|
39
|
+
notNull: false;
|
|
40
|
+
hasDefault: false;
|
|
41
|
+
isPrimaryKey: false;
|
|
42
|
+
isAutoincrement: false;
|
|
43
|
+
hasRuntimeDefault: false;
|
|
44
|
+
enumValues: string[] | undefined;
|
|
45
|
+
baseColumn: never;
|
|
46
|
+
identity: undefined;
|
|
47
|
+
generated: undefined;
|
|
48
|
+
}, {}, {}>;
|
|
49
|
+
};
|
|
50
|
+
dialect: "pg";
|
|
51
|
+
}>;
|
|
52
|
+
schema: import("@murumets-ee/db").TableDefinition<{
|
|
53
|
+
readonly id: import("@murumets-ee/db").ColumnFactory<string, "uuid", true, true>;
|
|
54
|
+
readonly entityType: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
|
|
55
|
+
readonly entityId: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>; /** A real locale code — the default locale by its code, never `'_'`. */
|
|
56
|
+
readonly locale: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
|
|
57
|
+
readonly contentHash: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
|
|
58
|
+
readonly contentChangedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", true, true>;
|
|
59
|
+
readonly sourceLocale: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
60
|
+
readonly sourceHash: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
61
|
+
readonly method: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
62
|
+
readonly reviewedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", false, false>;
|
|
63
|
+
readonly reviewedBy: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
64
|
+
readonly updatedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", true, true>;
|
|
65
|
+
readonly updatedBy: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
66
|
+
}>;
|
|
67
|
+
columnKinds: Readonly<Record<string, import("@murumets-ee/db").ColumnKind>>;
|
|
68
|
+
primaryKeyColumns: readonly string[];
|
|
69
|
+
makeClient: (database: import("drizzle-orm/postgres-js").PostgresJsDatabase, partitionValue?: string) => import("@murumets-ee/db").TableClient<{
|
|
70
|
+
readonly id: import("@murumets-ee/db").ColumnFactory<string, "uuid", true, true>;
|
|
71
|
+
readonly entityType: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
|
|
72
|
+
readonly entityId: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>; /** A real locale code — the default locale by its code, never `'_'`. */
|
|
73
|
+
readonly locale: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
|
|
74
|
+
readonly contentHash: import("@murumets-ee/db").ColumnFactory<string, "varchar", true, false>;
|
|
75
|
+
readonly contentChangedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", true, true>;
|
|
76
|
+
readonly sourceLocale: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
77
|
+
readonly sourceHash: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
78
|
+
readonly method: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
79
|
+
readonly reviewedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", false, false>;
|
|
80
|
+
readonly reviewedBy: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
81
|
+
readonly updatedAt: import("@murumets-ee/db").ColumnFactory<Date, "timestamp", true, true>;
|
|
82
|
+
readonly updatedBy: import("@murumets-ee/db").ColumnFactory<string, "varchar", false, false>;
|
|
83
|
+
}, import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
84
|
+
name: string;
|
|
85
|
+
schema: undefined;
|
|
86
|
+
columns: {
|
|
87
|
+
[x: string]: import("drizzle-orm/pg-core").PgColumn<{
|
|
88
|
+
name: string;
|
|
89
|
+
tableName: string;
|
|
90
|
+
dataType: import("drizzle-orm").ColumnDataType;
|
|
91
|
+
columnType: string;
|
|
92
|
+
data: unknown;
|
|
93
|
+
driverParam: unknown;
|
|
94
|
+
notNull: false;
|
|
95
|
+
hasDefault: false;
|
|
96
|
+
isPrimaryKey: false;
|
|
97
|
+
isAutoincrement: false;
|
|
98
|
+
hasRuntimeDefault: false;
|
|
99
|
+
enumValues: string[] | undefined;
|
|
100
|
+
baseColumn: never;
|
|
101
|
+
identity: undefined;
|
|
102
|
+
generated: undefined;
|
|
103
|
+
}, {}, {}>;
|
|
104
|
+
};
|
|
105
|
+
dialect: "pg";
|
|
106
|
+
}>>;
|
|
107
|
+
partitionColumn: "entityId" | "locale" | "id" | "updatedAt" | "entityType" | "contentHash" | "contentChangedAt" | "sourceLocale" | "sourceHash" | "method" | "reviewedAt" | "reviewedBy" | "updatedBy" | undefined;
|
|
108
|
+
};
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/translation-provenance.d.ts
|
|
111
|
+
/** How a locale's text got there. */
|
|
112
|
+
type TranslationMethod = 'ai' | 'human' | 'copy';
|
|
113
|
+
/** One locale's stored provenance. */
|
|
114
|
+
interface TranslationProvenance {
|
|
115
|
+
locale: string;
|
|
116
|
+
contentHash: string;
|
|
117
|
+
contentChangedAt: Date;
|
|
118
|
+
sourceLocale: string | null;
|
|
119
|
+
sourceHash: string | null;
|
|
120
|
+
method: TranslationMethod | null;
|
|
121
|
+
reviewedAt: Date | null;
|
|
122
|
+
reviewedBy: string | null;
|
|
123
|
+
}
|
|
124
|
+
type Row = Record<string, unknown>;
|
|
125
|
+
/**
|
|
126
|
+
* The entity's translatable fields whose value is prose: `text` and `richtext`.
|
|
127
|
+
* A slug is derived from its source field, and a select, a reference or a
|
|
128
|
+
* media id is not text. The translation engine translates exactly these.
|
|
129
|
+
*/
|
|
130
|
+
declare function translatableTextFields(entity: Pick<Entity, 'allFields'>): string[];
|
|
131
|
+
/** The entity's translatable block-tree fields. */
|
|
132
|
+
declare function translatableTreeFields(entity: Pick<Entity, 'allFields'>): string[];
|
|
133
|
+
/** Whether a document of this entity has any text a translation could go stale against. */
|
|
134
|
+
/**
|
|
135
|
+
* The prose a document's translatable text fields hold right now: `[field,
|
|
136
|
+
* text]` for every field whose value is a sentence — not an address (a
|
|
137
|
+
* translatable `canonicalUrl` is a `text` field too) and not empty.
|
|
138
|
+
*/
|
|
139
|
+
declare function translatableFieldTexts(entity: Pick<Entity, 'allFields'>, data: Readonly<Record<string, unknown>>): [string, string][];
|
|
140
|
+
declare function hasTranslatableText(entity: Pick<Entity, 'allFields'>): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Hash the reader text of one locale's data (a row, or a row with a draft laid
|
|
143
|
+
* over it). Order-independent across blocks; whitespace-insensitive.
|
|
144
|
+
*/
|
|
145
|
+
declare function documentTextHash(entity: Pick<Entity, 'allFields'>, data: Row): string;
|
|
146
|
+
type Db = Parameters<typeof translationProvenanceTable.makeClient>[0];
|
|
147
|
+
/** What {@link recordLocaleSave} is told about the save. */
|
|
148
|
+
interface LocaleSaveInput {
|
|
149
|
+
/** The entity type. One the app does not know is ignored. */
|
|
150
|
+
entityName: string;
|
|
151
|
+
entityId: string;
|
|
152
|
+
/** The saved locale — a real code, or `'_'` for the default locale. */
|
|
153
|
+
locale: string;
|
|
154
|
+
/** Who saved. */
|
|
155
|
+
userId?: string | undefined;
|
|
156
|
+
/**
|
|
157
|
+
* The locale's whole draft AS JUST WRITTEN, when the caller holds it. A draft
|
|
158
|
+
* writer inside a transaction must pass it: this module reads on its own
|
|
159
|
+
* connection and would otherwise see the draft as it was before the save.
|
|
160
|
+
*/
|
|
161
|
+
draftData?: Readonly<Record<string, unknown>> | undefined;
|
|
162
|
+
/**
|
|
163
|
+
* `'ai'` when the caller KNOWS the text is machine output (the translation
|
|
164
|
+
* engine). Left out, a save through a token-derived door is `'ai'` too — an
|
|
165
|
+
* agent typing a translation is a machine translating — and a person's save
|
|
166
|
+
* is `'human'`, or `'copy'` when the text is identical to the source's.
|
|
167
|
+
*/
|
|
168
|
+
method?: 'ai' | undefined;
|
|
169
|
+
/**
|
|
170
|
+
* This save REMOVED text rather than wrote it (a draft was discarded). It is
|
|
171
|
+
* recorded — the working text changed back to the row's — but it is never
|
|
172
|
+
* machine output, whoever did it: nobody wrote a word.
|
|
173
|
+
*/
|
|
174
|
+
reverted?: boolean | undefined;
|
|
175
|
+
/**
|
|
176
|
+
* Refresh this locale's TEXT HASH only — never its method, its source or its
|
|
177
|
+
* review. For a locale that was READ as a source, not written: without it a
|
|
178
|
+
* non-default source would be re-judged as a translation, under the caller's
|
|
179
|
+
* door, and a hand-written Russian page an agent translated FROM would be
|
|
180
|
+
* recorded as machine output.
|
|
181
|
+
*/
|
|
182
|
+
contentOnly?: boolean | undefined;
|
|
183
|
+
/** The locale the text was translated from. Defaults to the default locale. */
|
|
184
|
+
sourceLocale?: string | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* The source's text hash AS TRANSLATED, when the caller translated something
|
|
187
|
+
* other than the source's current working text (its published row).
|
|
188
|
+
*/
|
|
189
|
+
sourceHash?: string | undefined;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Record one locale save. Reads that locale's working text, stores its hash,
|
|
193
|
+
* and — for a translation — the source hash it now corresponds to.
|
|
194
|
+
*
|
|
195
|
+
* Machine output stays `ai` and unreviewed through later human edits: fixing a
|
|
196
|
+
* typo is not a review. Only "Mark reviewed" ({@link markTranslationReviewed})
|
|
197
|
+
* clears it, and a fresh machine translation sets it again.
|
|
198
|
+
*/
|
|
199
|
+
declare function recordLocaleSave(input: LocaleSaveInput): Promise<void>;
|
|
200
|
+
/**
|
|
201
|
+
* Mark a translation reviewed. The caller checks permission.
|
|
202
|
+
*
|
|
203
|
+
* - `'none'` — nothing is recorded for that locale; there is nothing to review.
|
|
204
|
+
* - `'self'` — `refuseSelf` was set and the reviewer is the one who last wrote
|
|
205
|
+
* the text. A review is someone ELSE vouching for it: an agent that could
|
|
206
|
+
* clear "not reviewed" on its own output would make the badge say nothing
|
|
207
|
+
* (the approval queue refuses the asking principal for the same reason).
|
|
208
|
+
*/
|
|
209
|
+
declare function markTranslationReviewed(input: {
|
|
210
|
+
entityType: string;
|
|
211
|
+
entityId: string;
|
|
212
|
+
locale: string;
|
|
213
|
+
userId: string;
|
|
214
|
+
refuseSelf?: boolean | undefined;
|
|
215
|
+
}): Promise<'reviewed' | 'none' | 'self'>;
|
|
216
|
+
/** Every stored provenance row for a set of documents of one entity type — ONE query. */
|
|
217
|
+
declare function getTranslationProvenanceBatch(entityType: string, entityIds: readonly string[], db?: Db): Promise<Map<string, Map<string, TranslationProvenance>>>;
|
|
218
|
+
/** What a translation's provenance says about it, against its source's. */
|
|
219
|
+
interface TranslationFreshness {
|
|
220
|
+
method: TranslationMethod | null;
|
|
221
|
+
sourceLocale: string | null;
|
|
222
|
+
/** The source text has changed since this translation was saved. */
|
|
223
|
+
outdated: boolean;
|
|
224
|
+
/** When the source text changed — ISO, only when {@link outdated}. */
|
|
225
|
+
outdatedSince: string | null;
|
|
226
|
+
/** Machine output nobody has marked reviewed. */
|
|
227
|
+
machineUnreviewed: boolean;
|
|
228
|
+
reviewedAt: string | null;
|
|
229
|
+
}
|
|
230
|
+
/** Compare one locale's provenance with its source's. `null` when nothing is recorded. */
|
|
231
|
+
declare function translationFreshness(byLocale: ReadonlyMap<string, TranslationProvenance> | undefined, locale: string): TranslationFreshness | null;
|
|
232
|
+
/**
|
|
233
|
+
* Record a locale save WITHOUT being awaited by it: bounded, coalesced per
|
|
234
|
+
* document and locale, and run under the caller's own request context.
|
|
235
|
+
*
|
|
236
|
+
* For a writer that holds a transaction (the block editor's draft write inside
|
|
237
|
+
* op-commit's): awaiting a recording there would take a SECOND pool connection
|
|
238
|
+
* while holding the first, and enough concurrent commits would each wait for a
|
|
239
|
+
* connection none of them can release.
|
|
240
|
+
*/
|
|
241
|
+
declare function enqueueLocaleSave(input: LocaleSaveInput): void;
|
|
242
|
+
/**
|
|
243
|
+
* Resolves once the queued recordings for ONE document and locale have run —
|
|
244
|
+
* for a caller about to write that row itself, which must come last. Bounded:
|
|
245
|
+
* it waits for that key only, never the whole queue, and gives up after
|
|
246
|
+
* `maxMs` rather than hold a request on a recording that will not settle.
|
|
247
|
+
*/
|
|
248
|
+
declare function localeSaveSettled(entityName: string, entityId: string, locale: string, maxMs?: number): Promise<void>;
|
|
249
|
+
/** Resolves once every queued recording has run. For tests; a request uses {@link localeSaveSettled}. */
|
|
250
|
+
declare function provenanceQueueIdle(): Promise<void>;
|
|
251
|
+
//#endregion
|
|
252
|
+
export { documentTextHash as a, hasTranslatableText as c, provenanceQueueIdle as d, recordLocaleSave as f, translationFreshness as g, translatableTreeFields as h, TranslationProvenance as i, localeSaveSettled as l, translatableTextFields as m, TranslationFreshness as n, enqueueLocaleSave as o, translatableFieldTexts as p, TranslationMethod as r, getTranslationProvenanceBatch as s, LocaleSaveInput as t, markTranslationReviewed as u };
|
|
253
|
+
//# sourceMappingURL=translation-provenance-BZ0MlbFR.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translation-provenance-BZ0MlbFR.d.mts","names":[],"sources":["../src/schema/translation-provenance.ts","../src/translation-provenance.ts"],"mappings":";;;;;;AA2BA;;;;;;;;;;;;;;;;;;;;cAAa,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KCsBD,iBAAA;;UAGK,qBAAA;EACf,MAAA;EACA,WAAA;EACA,gBAAA,EAAkB,IAAA;EAClB,YAAA;EACA,UAAA;EACA,MAAA,EAAQ,iBAAA;EACR,UAAA,EAAY,IAAA;EACZ,UAAA;AAAA;AAAA,KAGG,GAAA,GAAM,MAAM;;;;;;iBAYD,sBAAA,CAAuB,MAAA,EAAQ,IAAI,CAAC,MAAA;;iBAUpC,sBAAA,CAAuB,MAAA,EAAQ,IAAI,CAAC,MAAA;;;;;;;iBAapC,sBAAA,CACd,MAAA,EAAQ,IAAA,CAAK,MAAA,gBACb,IAAA,EAAM,QAAA,CAAS,MAAA;AAAA,iBAUD,mBAAA,CAAoB,MAAA,EAAQ,IAAI,CAAC,MAAA;;;;;iBAwBjC,gBAAA,CAAiB,MAAA,EAAQ,IAAA,CAAK,MAAA,gBAAsB,IAAA,EAAM,GAAA;AAAA,KA4BrE,EAAA,GAAK,UAAU,QAAQ,0BAAA,CAA2B,UAAA;;UA8DtC,eAAA;;EAEf,UAAA;EACA,QAAA;;EAEA,MAAA;;EAEA,MAAA;;;;;;EAMA,SAAA,GAAY,QAAQ,CAAC,MAAA;;;;;;;EAOrB,MAAA;;;;;;EAMA,QAAA;;;;;;;;EAQA,WAAA;;EAEA,YAAA;;;;;EAKA,UAAA;AAAA;;;;;;;;;iBAWoB,gBAAA,CAAiB,KAAA,EAAO,eAAA,GAAkB,OAAO;;;;;;;;;;iBA2KjD,uBAAA,CAAwB,KAAA;EAC5C,UAAA;EACA,QAAA;EACA,MAAA;EACA,MAAA;EACA,UAAA;AAAA,IACE,OAAO;AApZkB;AAAA,iBAiaP,6BAAA,CACpB,UAAA,UACA,SAAA,qBACA,EAAA,GAAK,EAAA,GACJ,OAAA,CAAQ,GAAA,SAAY,GAAA,SAAY,qBAAA;;UAoBlB,oBAAA;EACf,MAAA,EAAQ,iBAAiB;EACzB,YAAA;EAjbY;EAmbZ,QAAA;EAnbgB;EAqbhB,aAAA;EA1bA;EA4bA,iBAAA;EACA,UAAA;AAAA;;iBAIc,oBAAA,CACd,QAAA,EAAU,WAAA,SAAoB,qBAAA,eAC9B,MAAA,WACC,oBAAA;;;;;;AA9bS;AACX;;;iBAkgBe,iBAAA,CAAkB,KAAsB,EAAf,eAAe;;;;;;;iBAsDlC,iBAAA,CACpB,UAAA,UACA,QAAA,UACA,MAAA,UACA,KAAA,YACC,OAAO;AAriB8D;AAAA,iBAojBlD,mBAAA,IAAuB,OAAO"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { a as documentTextHash, c as hasTranslatableText, d as provenanceQueueIdle, f as recordLocaleSave, g as translationFreshness, h as translatableTreeFields, i as TranslationProvenance, l as localeSaveSettled, m as translatableTextFields, n as TranslationFreshness, o as enqueueLocaleSave, p as translatableFieldTexts, r as TranslationMethod, s as getTranslationProvenanceBatch, t as LocaleSaveInput, u as markTranslationReviewed } from "./translation-provenance-BZ0MlbFR.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/translatable-sources.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `'content.translatableSources'` — text OUTSIDE the entity translation tables
|
|
6
|
+
* that a whole-site translation still has to cover (agent-access D019).
|
|
7
|
+
*
|
|
8
|
+
* A menu keeps its per-locale labels inside one JSON tree, so the translation
|
|
9
|
+
* engine's walk over translatable entity fields never sees them. Content owns
|
|
10
|
+
* this slot so that the engine (`@murumets-ee/translation`) and the plugin that
|
|
11
|
+
* owns such text (`@murumets-ee/menus`) need no dependency on each other: the
|
|
12
|
+
* owner DECLARES a {@link TranslatableSource} on its plugin's `contributes`,
|
|
13
|
+
* and the engine reads them with {@link getTranslatableSources}.
|
|
14
|
+
*
|
|
15
|
+
* No `server-only` and no database import: this module is types plus one
|
|
16
|
+
* registry read, so a plugin entry that jiti loads may import from it.
|
|
17
|
+
*/
|
|
18
|
+
/** One piece of source-locale text a {@link TranslatableSource} wants translated. */
|
|
19
|
+
interface TranslatableSourceText {
|
|
20
|
+
/** Stable within one item — the job hands it back to `write`. */
|
|
21
|
+
key: string;
|
|
22
|
+
text: string;
|
|
23
|
+
maxLength?: number;
|
|
24
|
+
}
|
|
25
|
+
/** One translatable thing of a kind — a menu, say — as `list` reports it. */
|
|
26
|
+
interface TranslatableSourceItem {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
}
|
|
30
|
+
interface TranslatableSourceRead {
|
|
31
|
+
/** What `texts` was read from. `write` refuses (`'changed'`) when the item no longer matches it. */
|
|
32
|
+
revision: string;
|
|
33
|
+
/** The source-locale texts that need a translation for this target, per `mode`. Empty = nothing to do. */
|
|
34
|
+
texts: TranslatableSourceText[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Text outside the entity translation tables that a whole-site translation
|
|
38
|
+
* covers (agent-access D019).
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ **Every method runs INSIDE the caller's request context.** `getApp()` /
|
|
41
|
+
* `app.getClient(entity)` see the ambient user and permission checker, and that
|
|
42
|
+
* is the only authority a source has: it does its reads and writes through
|
|
43
|
+
* `AdminClient` / `ContentClient` and NEVER elevates — no
|
|
44
|
+
* `createSystemAdminClient`, no `runAsCli`, no hand-built context. The engine
|
|
45
|
+
* checks {@link TranslatableSource.permission} with the starter's own checker
|
|
46
|
+
* before it lists anything; the entity firewall under these methods is the
|
|
47
|
+
* second gate, not a formality.
|
|
48
|
+
*
|
|
49
|
+
* Each call touches ONE item. `list` is the only enumeration, and it is a
|
|
50
|
+
* bounded page.
|
|
51
|
+
*/
|
|
52
|
+
interface TranslatableSource {
|
|
53
|
+
/** Unique: `'menu'`. Lower-case `[a-z][a-z0-9_]*`. */
|
|
54
|
+
kind: string;
|
|
55
|
+
/** English fallback beside {@link labelKey} (CLAUDE.md "Interface copy") — never rendered when the key resolves. */
|
|
56
|
+
label: string;
|
|
57
|
+
/** next-intl key for {@link label}, in the contributing plugin's own namespace. */
|
|
58
|
+
labelKey: string;
|
|
59
|
+
/** What the caller must hold to translate this kind. The job checks it with the starter's own checker before listing. */
|
|
60
|
+
permission: {
|
|
61
|
+
resource: string;
|
|
62
|
+
action: string;
|
|
63
|
+
};
|
|
64
|
+
/** `true`: `write` lands in a draft and nothing is live until someone publishes. */
|
|
65
|
+
draft: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A bounded keyset page, ordered by id: the items after `after`, at most
|
|
68
|
+
* `limit` of them. `limit` is never above 200, and a source must honour it
|
|
69
|
+
* EXACTLY up to that: the job asks for one item more than it reads, and
|
|
70
|
+
* takes a page of FEWER rows than it asked for to mean "there are no more
|
|
71
|
+
* rows". A source that caps its page lower than the `limit` it was given
|
|
72
|
+
* silently ends the scan there — the rest is never planned, and nothing
|
|
73
|
+
* says so.
|
|
74
|
+
*
|
|
75
|
+
* Under scope `all` a source's items are taken by EVERY run: a source
|
|
76
|
+
* records nothing that says "this is already a current machine
|
|
77
|
+
* translation" (`read` with mode `'all'` returns every text that has a
|
|
78
|
+
* source). Only scope `missing` comes to an end for a source.
|
|
79
|
+
*/
|
|
80
|
+
list(input: {
|
|
81
|
+
after?: string;
|
|
82
|
+
limit: number;
|
|
83
|
+
}): Promise<TranslatableSourceItem[]>;
|
|
84
|
+
/**
|
|
85
|
+
* `null` = no such item (or not readable by this caller). mode `'missing'`:
|
|
86
|
+
* only texts the target does not have yet; `'all'`: every text that has a
|
|
87
|
+
* source.
|
|
88
|
+
*/
|
|
89
|
+
read(input: {
|
|
90
|
+
id: string;
|
|
91
|
+
sourceLocale: string;
|
|
92
|
+
targetLocale: string;
|
|
93
|
+
mode: 'missing' | 'all';
|
|
94
|
+
}): Promise<TranslatableSourceRead | null>;
|
|
95
|
+
write(input: {
|
|
96
|
+
id: string;
|
|
97
|
+
targetLocale: string;
|
|
98
|
+
revision: string; /** key → translated text, for keys `read` returned. A key not in `texts` is left alone. */
|
|
99
|
+
texts: Readonly<Record<string, string>>;
|
|
100
|
+
user: {
|
|
101
|
+
id: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
};
|
|
104
|
+
}): Promise<'written' | 'changed' | 'not_found'>;
|
|
105
|
+
}
|
|
106
|
+
declare module '@murumets-ee/core' {
|
|
107
|
+
interface PluginContributionSlots {
|
|
108
|
+
/**
|
|
109
|
+
* Translatable text that lives outside the entity translation tables, one
|
|
110
|
+
* list per contributing plugin. Read it with `getTranslatableSources()`
|
|
111
|
+
* from `@murumets-ee/content/translation`, which flattens the lists.
|
|
112
|
+
*/
|
|
113
|
+
'content.translatableSources': readonly TranslatableSource[];
|
|
114
|
+
/**
|
|
115
|
+
* Where a person starts a whole-site translation run — the admin page, and
|
|
116
|
+
* the permission it needs. Contributed by `@murumets-ee/translation`; read
|
|
117
|
+
* with `getSiteTranslationEntry()` by the editor, which links to it.
|
|
118
|
+
*/
|
|
119
|
+
'content.siteTranslation': SiteTranslationEntry;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Every contributed {@link TranslatableSource}, as one flat list.
|
|
124
|
+
*
|
|
125
|
+
* One entry per `kind`: when two plugins contribute the same kind the LATER
|
|
126
|
+
* plugin (in `plugins:` order) wins and keeps the earlier one's position, so
|
|
127
|
+
* an app can replace a source without the list reordering. `[]` when nothing
|
|
128
|
+
* contributes — an app without menus simply has no extra text to translate.
|
|
129
|
+
*/
|
|
130
|
+
declare function getTranslatableSources(): TranslatableSource[];
|
|
131
|
+
/** The whole-site translation page, as the content editor links to it. */
|
|
132
|
+
interface SiteTranslationEntry {
|
|
133
|
+
/** An admin path without the locale prefix, e.g. `/admin/translate-site`. */
|
|
134
|
+
href: string;
|
|
135
|
+
permission: {
|
|
136
|
+
resource: string;
|
|
137
|
+
action: string;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** The contributed whole-site translation page, or `undefined` when no plugin offers one. */
|
|
141
|
+
declare function getSiteTranslationEntry(): SiteTranslationEntry | undefined;
|
|
142
|
+
//#endregion
|
|
143
|
+
export { type LocaleSaveInput, type SiteTranslationEntry, type TranslatableSource, type TranslatableSourceItem, type TranslatableSourceRead, type TranslatableSourceText, type TranslationFreshness, type TranslationMethod, type TranslationProvenance, documentTextHash, enqueueLocaleSave, getSiteTranslationEntry, getTranslatableSources, getTranslationProvenanceBatch, hasTranslatableText, localeSaveSettled, markTranslationReviewed, provenanceQueueIdle, recordLocaleSave, translatableFieldTexts, translatableTextFields, translatableTreeFields, translationFreshness };
|
|
144
|
+
//# sourceMappingURL=translation.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translation.d.mts","names":[],"sources":["../src/translatable-sources.ts"],"mappings":";;;;;;AAkBA;;;;;;;;AAIW;AAIX;;;UARiB,sBAAA;EAUV;EARL,GAAA;EACA,IAAA;EACA,SAAA;AAAA;;UAIe,sBAAA;EACf,EAAA;EACA,KAAK;AAAA;AAAA,UAGU,sBAAA;EAuBkB;EArBjC,QAAA;EA8CwD;EA5CxD,KAAA,EAAO,sBAAsB;AAAA;;;;;;;;;;;;;;;;;UAmBd,kBAAA;EAyBiC;EAvBhD,IAAA;EA6BA;EA3BA,KAAA;EA6BE;EA3BF,QAAA;EA6BE;EA3BF,UAAA;IAAc,QAAA;IAAkB,MAAA;EAAA;EA8B9B;EA5BF,KAAA;EA8BE;;;;;;;;;;AAIS;AACZ;;;EApBC,IAAA,CAAK,KAAA;IAAS,KAAA;IAAgB,KAAA;EAAA,IAAkB,OAAA,CAAQ,sBAAA;EAmCtD;;;;AAA+C;EA7BjD,IAAA,CAAK,KAAA;IACH,EAAA;IACA,YAAA;IACA,YAAA;IACA,IAAA;EAAA,IACE,OAAA,CAAQ,sBAAA;EACZ,KAAA,CAAM,KAAA;IACJ,EAAA;IACA,YAAA;IACA,QAAA,UA4CF;IA1CE,KAAA,EAAO,QAAA,CAAS,MAAA;IAChB,IAAA;MAAQ,EAAA;MAAY,IAAA;IAAA;EAAA,IAClB,OAAA;AAAA;AAAA;EAAA,UAIM,uBAAA;;;;;;IAMR,6BAAA,WAAwC,kBAAA;;;;;;IAMxC,yBAAA,EAA2B,oBAAoB;EAAA;AAAA;;;;;;;;;iBAYnC,sBAAA,IAA0B,kBAAkB;;UAS3C,oBAAA;;EAEf,IAAA;EACA,UAAA;IAAc,QAAA;IAAkB,MAAA;EAAA;AAAA;;iBAIlB,uBAAA,IAA2B,oBAAoB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as e,c as t,d as n,f as r,i,l as a,n as o,o as s,r as c,s as l,t as u,u as d}from"./translation-provenance-B41lEDM-.mjs";import{getContributions as f}from"@murumets-ee/core";function p(){let e=new Map;for(let t of f(`content.translatableSources`))for(let n of t)e.set(n.kind,n);return[...e.values()]}function m(){return f(`content.siteTranslation`).at(-1)}export{u as documentTextHash,o as enqueueLocaleSave,m as getSiteTranslationEntry,p as getTranslatableSources,c as getTranslationProvenanceBatch,i as hasTranslatableText,e as localeSaveSettled,s as markTranslationReviewed,l as provenanceQueueIdle,t as recordLocaleSave,a as translatableFieldTexts,d as translatableTextFields,n as translatableTreeFields,r as translationFreshness};
|
|
2
|
+
//# sourceMappingURL=translation.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translation.mjs","names":[],"sources":["../src/translatable-sources.ts"],"sourcesContent":["/**\n * `'content.translatableSources'` — text OUTSIDE the entity translation tables\n * that a whole-site translation still has to cover (agent-access D019).\n *\n * A menu keeps its per-locale labels inside one JSON tree, so the translation\n * engine's walk over translatable entity fields never sees them. Content owns\n * this slot so that the engine (`@murumets-ee/translation`) and the plugin that\n * owns such text (`@murumets-ee/menus`) need no dependency on each other: the\n * owner DECLARES a {@link TranslatableSource} on its plugin's `contributes`,\n * and the engine reads them with {@link getTranslatableSources}.\n *\n * No `server-only` and no database import: this module is types plus one\n * registry read, so a plugin entry that jiti loads may import from it.\n */\n\nimport { getContributions } from '@murumets-ee/core'\n\n/** One piece of source-locale text a {@link TranslatableSource} wants translated. */\nexport interface TranslatableSourceText {\n /** Stable within one item — the job hands it back to `write`. */\n key: string\n text: string\n maxLength?: number\n}\n\n/** One translatable thing of a kind — a menu, say — as `list` reports it. */\nexport interface TranslatableSourceItem {\n id: string\n label: string\n}\n\nexport interface TranslatableSourceRead {\n /** What `texts` was read from. `write` refuses (`'changed'`) when the item no longer matches it. */\n revision: string\n /** The source-locale texts that need a translation for this target, per `mode`. Empty = nothing to do. */\n texts: TranslatableSourceText[]\n}\n\n/**\n * Text outside the entity translation tables that a whole-site translation\n * covers (agent-access D019).\n *\n * ⚠️ **Every method runs INSIDE the caller's request context.** `getApp()` /\n * `app.getClient(entity)` see the ambient user and permission checker, and that\n * is the only authority a source has: it does its reads and writes through\n * `AdminClient` / `ContentClient` and NEVER elevates — no\n * `createSystemAdminClient`, no `runAsCli`, no hand-built context. The engine\n * checks {@link TranslatableSource.permission} with the starter's own checker\n * before it lists anything; the entity firewall under these methods is the\n * second gate, not a formality.\n *\n * Each call touches ONE item. `list` is the only enumeration, and it is a\n * bounded page.\n */\nexport interface TranslatableSource {\n /** Unique: `'menu'`. Lower-case `[a-z][a-z0-9_]*`. */\n kind: string\n /** English fallback beside {@link labelKey} (CLAUDE.md \"Interface copy\") — never rendered when the key resolves. */\n label: string\n /** next-intl key for {@link label}, in the contributing plugin's own namespace. */\n labelKey: string\n /** What the caller must hold to translate this kind. The job checks it with the starter's own checker before listing. */\n permission: { resource: string; action: string }\n /** `true`: `write` lands in a draft and nothing is live until someone publishes. */\n draft: boolean\n /**\n * A bounded keyset page, ordered by id: the items after `after`, at most\n * `limit` of them. `limit` is never above 200, and a source must honour it\n * EXACTLY up to that: the job asks for one item more than it reads, and\n * takes a page of FEWER rows than it asked for to mean \"there are no more\n * rows\". A source that caps its page lower than the `limit` it was given\n * silently ends the scan there — the rest is never planned, and nothing\n * says so.\n *\n * Under scope `all` a source's items are taken by EVERY run: a source\n * records nothing that says \"this is already a current machine\n * translation\" (`read` with mode `'all'` returns every text that has a\n * source). Only scope `missing` comes to an end for a source.\n */\n list(input: { after?: string; limit: number }): Promise<TranslatableSourceItem[]>\n /**\n * `null` = no such item (or not readable by this caller). mode `'missing'`:\n * only texts the target does not have yet; `'all'`: every text that has a\n * source.\n */\n read(input: {\n id: string\n sourceLocale: string\n targetLocale: string\n mode: 'missing' | 'all'\n }): Promise<TranslatableSourceRead | null>\n write(input: {\n id: string\n targetLocale: string\n revision: string\n /** key → translated text, for keys `read` returned. A key not in `texts` is left alone. */\n texts: Readonly<Record<string, string>>\n user: { id: string; name?: string }\n }): Promise<'written' | 'changed' | 'not_found'>\n}\n\ndeclare module '@murumets-ee/core' {\n interface PluginContributionSlots {\n /**\n * Translatable text that lives outside the entity translation tables, one\n * list per contributing plugin. Read it with `getTranslatableSources()`\n * from `@murumets-ee/content/translation`, which flattens the lists.\n */\n 'content.translatableSources': readonly TranslatableSource[]\n /**\n * Where a person starts a whole-site translation run — the admin page, and\n * the permission it needs. Contributed by `@murumets-ee/translation`; read\n * with `getSiteTranslationEntry()` by the editor, which links to it.\n */\n 'content.siteTranslation': SiteTranslationEntry\n }\n}\n\n/**\n * Every contributed {@link TranslatableSource}, as one flat list.\n *\n * One entry per `kind`: when two plugins contribute the same kind the LATER\n * plugin (in `plugins:` order) wins and keeps the earlier one's position, so\n * an app can replace a source without the list reordering. `[]` when nothing\n * contributes — an app without menus simply has no extra text to translate.\n */\nexport function getTranslatableSources(): TranslatableSource[] {\n const byKind = new Map<string, TranslatableSource>()\n for (const contribution of getContributions('content.translatableSources')) {\n for (const source of contribution) byKind.set(source.kind, source)\n }\n return [...byKind.values()]\n}\n\n/** The whole-site translation page, as the content editor links to it. */\nexport interface SiteTranslationEntry {\n /** An admin path without the locale prefix, e.g. `/admin/translate-site`. */\n href: string\n permission: { resource: string; action: string }\n}\n\n/** The contributed whole-site translation page, or `undefined` when no plugin offers one. */\nexport function getSiteTranslationEntry(): SiteTranslationEntry | undefined {\n return getContributions('content.siteTranslation').at(-1)\n}\n"],"mappings":"uLA8HA,SAAgB,GAA+C,CAC7D,IAAM,EAAS,IAAI,IACnB,IAAK,IAAM,KAAgB,EAAiB,6BAA6B,EACvE,IAAK,IAAM,KAAU,EAAc,EAAO,IAAI,EAAO,KAAM,CAAM,EAEnE,MAAO,CAAC,GAAG,EAAO,OAAO,CAAC,CAC5B,CAUA,SAAgB,GAA4D,CAC1E,OAAO,EAAiB,yBAAyB,CAAC,CAAC,GAAG,EAAE,CAC1D"}
|