@_linked/translation 0.9.5 → 0.10.0
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/README.md +30 -0
- package/lib/esm/shape-key-copies.d.ts +62 -0
- package/lib/esm/shape-key-copies.js +156 -0
- package/lib/esm/shape-key-copies.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ package works without either peer.
|
|
|
37
37
|
- `@_linked/translation/key-sync` — development-time source extraction and sync
|
|
38
38
|
- `@_linked/translation/key-sync/node` — recursive source-tree scan + dry-run/apply orchestration
|
|
39
39
|
- `@_linked/translation/shape-catalog` — package-owned translations attached to stable shape/property IRIs
|
|
40
|
+
- `@_linked/translation/shape-key-copies` — server-side: one shape key across every app that holds it (list, propagate, clear overrides)
|
|
40
41
|
- `@_linked/translation/discovery` — portable declarations and deterministic inventory merging
|
|
41
42
|
- `@_linked/translation/linked-discovery` — serialized LINKED package catalogs
|
|
42
43
|
- `@_linked/translation/archive` — full-fidelity backup and pure restore planning
|
|
@@ -87,6 +88,35 @@ Existing app-authored translations are never overwritten. Packages may export
|
|
|
87
88
|
the TypeScript object or ship the same contract as JSON for a host's tooling
|
|
88
89
|
to discover without executing application code.
|
|
89
90
|
|
|
91
|
+
### One shape key across apps
|
|
92
|
+
|
|
93
|
+
Every app that uses a shape keeps its own copy of each key the shape carries.
|
|
94
|
+
An app may reword its copy; that marks the copy `overridden` and keeps the
|
|
95
|
+
shape's wording at that moment as `shapeSource`. The server-side
|
|
96
|
+
`@_linked/translation/shape-key-copies` entry point acts on all copies of one
|
|
97
|
+
shape key at once:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import {
|
|
101
|
+
listShapeKeyCopies,
|
|
102
|
+
propagateShapeKeySource,
|
|
103
|
+
clearShapeKeyOverride,
|
|
104
|
+
} from '@_linked/translation/shape-key-copies';
|
|
105
|
+
|
|
106
|
+
// Every app's copy, with its override state and translated languages.
|
|
107
|
+
await listShapeKeyCopies({ shapeIri, key: 'person.givenName' });
|
|
108
|
+
// New wording for every copy that follows the shape; overrides keep theirs.
|
|
109
|
+
await propagateShapeKeySource({ shapeIri, key: 'person.givenName', sourceText: 'Given name' });
|
|
110
|
+
// Revert overrides (all apps, or one with `appId`) so they follow the shape again.
|
|
111
|
+
await clearShapeKeyOverride({ shapeIri, key: 'person.givenName', appId });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each text change is a normal key write (`upsertTranslationKey`): it records a
|
|
115
|
+
new key version and marks that key's units stale. A clear reverts to the text of
|
|
116
|
+
a copy that still follows the shape, or else to the copy's own `shapeSource`.
|
|
117
|
+
The operations run on the default dataset and check no permissions: the host
|
|
118
|
+
decides who may call them and which dataset they run in.
|
|
119
|
+
|
|
90
120
|
Authoring tools — editors, menus, entitlements, capability activation — are
|
|
91
121
|
deliberately not part of this package. They belong to the host application, so
|
|
92
122
|
any app can use this runtime without depending on a particular host.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type TranslationKeyWriteContext } from './shapes/TranslationProvider.js';
|
|
2
|
+
/** One app's copy of a shape key. */
|
|
3
|
+
export interface ShapeKeyCopyRecord {
|
|
4
|
+
/** The key node. */
|
|
5
|
+
id: string;
|
|
6
|
+
key: string;
|
|
7
|
+
shapeIri: string;
|
|
8
|
+
appId: string;
|
|
9
|
+
sourceText?: string;
|
|
10
|
+
/** The app reworded this copy; propagation leaves it alone. */
|
|
11
|
+
overridden: boolean;
|
|
12
|
+
/** The shape's text when the app first reworded the copy: what a clear reverts to without a sibling. */
|
|
13
|
+
shapeSource?: string;
|
|
14
|
+
ofProperty?: string;
|
|
15
|
+
fromPackage?: string;
|
|
16
|
+
namespace?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
kind?: 'ui' | 'content';
|
|
19
|
+
format?: 'simple' | 'icu';
|
|
20
|
+
/** The languages this copy has a unit for, sorted. */
|
|
21
|
+
languages: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface ShapeKeyQuery {
|
|
24
|
+
/** One shape's keys; omitted, every shape key. */
|
|
25
|
+
shapeIri?: string;
|
|
26
|
+
/** One key name of that shape. */
|
|
27
|
+
key?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The copies of the shape keys `query` names, one per app. A key that names no app is not a
|
|
31
|
+
* copy and is left out.
|
|
32
|
+
*/
|
|
33
|
+
export declare function listShapeKeyCopies(query?: ShapeKeyQuery): Promise<ShapeKeyCopyRecord[]>;
|
|
34
|
+
export interface ShapeKeyPropagation {
|
|
35
|
+
/** Apps whose copy now has the new text (or had it already). */
|
|
36
|
+
updatedApps: string[];
|
|
37
|
+
/** Apps whose copy is overridden and was left alone. */
|
|
38
|
+
skippedOverrides: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Give one shape key a new source text in every app whose copy follows the shape. Overridden
|
|
42
|
+
* copies keep their text. A copy whose text changes gets a new key version and stale units.
|
|
43
|
+
*/
|
|
44
|
+
export declare function propagateShapeKeySource(data: {
|
|
45
|
+
shapeIri: string;
|
|
46
|
+
key: string;
|
|
47
|
+
sourceText: string;
|
|
48
|
+
}, authorship?: Pick<TranslationKeyWriteContext, 'createdBy'>): Promise<ShapeKeyPropagation>;
|
|
49
|
+
/**
|
|
50
|
+
* Clear the override on one shape key's copies: every overridden copy, or only `appId`'s. Each
|
|
51
|
+
* reverts to the text of a copy that follows the shape, or, when none does, to its own snapshot
|
|
52
|
+
* (`shapeSource`); with neither, it keeps its own text. The flag is reset (`overridden: false`)
|
|
53
|
+
* and the snapshot removed, so the copy follows the shape again. Returns the apps cleared; a copy
|
|
54
|
+
* with no text at all is not written and not counted.
|
|
55
|
+
*/
|
|
56
|
+
export declare function clearShapeKeyOverride(data: {
|
|
57
|
+
shapeIri: string;
|
|
58
|
+
key: string;
|
|
59
|
+
appId?: string;
|
|
60
|
+
}, authorship?: Pick<TranslationKeyWriteContext, 'createdBy'>): Promise<{
|
|
61
|
+
clearedApps: string[];
|
|
62
|
+
}>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape keys across apps: every app that uses a shape holds its own copy of each translation key
|
|
3
|
+
* the shape carries (`TranslationKey` with `ofShape`, one per `ofApplication`). These operations
|
|
4
|
+
* act on all of a shape key's copies at once:
|
|
5
|
+
*
|
|
6
|
+
* - {@link listShapeKeyCopies} reads them, with each copy's override state and the languages it
|
|
7
|
+
* is translated into;
|
|
8
|
+
* - {@link propagateShapeKeySource} gives every copy that follows the shape a new source text;
|
|
9
|
+
* a copy the app reworded (`overridden`) keeps its own text;
|
|
10
|
+
* - {@link clearShapeKeyOverride} reverts overridden copies (all, or one app's) to the shape's
|
|
11
|
+
* current text, so they follow the shape again.
|
|
12
|
+
*
|
|
13
|
+
* Every text change goes through {@link upsertTranslationKey}, so it gets a new key version and
|
|
14
|
+
* marks the key's units stale exactly as any other source change does. Like the other key writes,
|
|
15
|
+
* they run on the default dataset (`LinkedStorage`); a host that keeps each app's data in its own
|
|
16
|
+
* dataset runs them in that dataset's context. They decide nothing about who may call them: the
|
|
17
|
+
* host checks that before.
|
|
18
|
+
*/
|
|
19
|
+
import { TranslationKey } from './shapes/TranslationKey.js';
|
|
20
|
+
import { TranslationUnit } from './shapes/TranslationUnit.js';
|
|
21
|
+
import { upsertTranslationKey, } from './shapes/TranslationProvider.js';
|
|
22
|
+
const iri = (value) => typeof value === 'string' ? value || undefined : (value === null || value === void 0 ? void 0 : value.id) || undefined;
|
|
23
|
+
const text = (value) => value === undefined || value === null || value === '' ? undefined : String(value);
|
|
24
|
+
/** The flag as stored: a boolean, or the string "true" an older writer left. */
|
|
25
|
+
const isOverridden = (value) => value === true || value === 'true';
|
|
26
|
+
/** The node a key write addresses: `upsertTranslationKey` keys a copy by its app and key name. */
|
|
27
|
+
const keyIri = (appId, key) => `${appId.replace(/\/$/, '')}/translation/key/${encodeURIComponent(key)}`;
|
|
28
|
+
const keyWhere = (query) => (k) => {
|
|
29
|
+
const shape = query.shapeIri ? k.ofShape.equals({ id: query.shapeIri }) : k.ofShape.isDefined();
|
|
30
|
+
return query.key === undefined ? shape : shape.and(k.key.equals(query.key));
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The copies of the shape keys `query` names, one per app. A key that names no app is not a
|
|
34
|
+
* copy and is left out.
|
|
35
|
+
*/
|
|
36
|
+
export async function listShapeKeyCopies(query = {}) {
|
|
37
|
+
var _a, _b;
|
|
38
|
+
const [keys, units] = await Promise.all([
|
|
39
|
+
TranslationKey.select((k) => [
|
|
40
|
+
k.key,
|
|
41
|
+
k.ofShape,
|
|
42
|
+
k.ofApplication,
|
|
43
|
+
k.sourceText,
|
|
44
|
+
k.overridden,
|
|
45
|
+
k.shapeSource,
|
|
46
|
+
k.ofProperty,
|
|
47
|
+
k.fromPackage,
|
|
48
|
+
k.namespace,
|
|
49
|
+
k.description,
|
|
50
|
+
k.kind,
|
|
51
|
+
k.format,
|
|
52
|
+
]).where(keyWhere(query)),
|
|
53
|
+
TranslationUnit.select((u) => [u.ofKey, u.language]).where(((u) => keyWhere(query)(u.ofKey))),
|
|
54
|
+
]);
|
|
55
|
+
const languagesOf = new Map();
|
|
56
|
+
for (const unit of (units !== null && units !== void 0 ? units : [])) {
|
|
57
|
+
const keyId = iri(unit.ofKey);
|
|
58
|
+
const language = text(unit.language);
|
|
59
|
+
if (!keyId || !language)
|
|
60
|
+
continue;
|
|
61
|
+
const set = (_a = languagesOf.get(keyId)) !== null && _a !== void 0 ? _a : new Set();
|
|
62
|
+
set.add(language);
|
|
63
|
+
languagesOf.set(keyId, set);
|
|
64
|
+
}
|
|
65
|
+
const copies = [];
|
|
66
|
+
for (const row of (keys !== null && keys !== void 0 ? keys : [])) {
|
|
67
|
+
const id = iri(row.id);
|
|
68
|
+
const key = text(row.key);
|
|
69
|
+
const shapeIri = iri(row.ofShape);
|
|
70
|
+
const appId = iri(row.ofApplication);
|
|
71
|
+
if (!id || !key || !shapeIri || !appId)
|
|
72
|
+
continue;
|
|
73
|
+
copies.push({
|
|
74
|
+
id,
|
|
75
|
+
key,
|
|
76
|
+
shapeIri,
|
|
77
|
+
appId,
|
|
78
|
+
sourceText: text(row.sourceText),
|
|
79
|
+
overridden: isOverridden(row.overridden),
|
|
80
|
+
shapeSource: text(row.shapeSource),
|
|
81
|
+
ofProperty: iri(row.ofProperty),
|
|
82
|
+
fromPackage: text(row.fromPackage),
|
|
83
|
+
namespace: text(row.namespace),
|
|
84
|
+
description: text(row.description),
|
|
85
|
+
kind: row.kind === 'content' ? 'content' : row.kind === 'ui' ? 'ui' : undefined,
|
|
86
|
+
format: row.format === 'icu' ? 'icu' : row.format === 'simple' ? 'simple' : undefined,
|
|
87
|
+
languages: [...((_b = languagesOf.get(id)) !== null && _b !== void 0 ? _b : [])].sort(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return copies;
|
|
91
|
+
}
|
|
92
|
+
/** The copy's own declaration, so a text write keeps what the copy declares. */
|
|
93
|
+
function asKeyInput(copy, sourceText) {
|
|
94
|
+
if (copy.id !== keyIri(copy.appId, copy.key)) {
|
|
95
|
+
// A key write addresses the app's key node; writing this copy there would create a second key.
|
|
96
|
+
throw new Error(`Shape key copy ${copy.id} is not at its app's key node; it was not written.`);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
appId: copy.appId,
|
|
100
|
+
key: copy.key,
|
|
101
|
+
sourceText,
|
|
102
|
+
namespace: copy.namespace,
|
|
103
|
+
description: copy.description,
|
|
104
|
+
kind: copy.kind,
|
|
105
|
+
ofShape: copy.shapeIri,
|
|
106
|
+
ofProperty: copy.ofProperty,
|
|
107
|
+
fromPackage: copy.fromPackage,
|
|
108
|
+
format: copy.format,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const appsOf = (copies) => [...new Set(copies.map((c) => c.appId))].sort();
|
|
112
|
+
/**
|
|
113
|
+
* Give one shape key a new source text in every app whose copy follows the shape. Overridden
|
|
114
|
+
* copies keep their text. A copy whose text changes gets a new key version and stale units.
|
|
115
|
+
*/
|
|
116
|
+
export async function propagateShapeKeySource(data, authorship = {}) {
|
|
117
|
+
var _a;
|
|
118
|
+
const sourceText = (_a = data.sourceText) === null || _a === void 0 ? void 0 : _a.trim();
|
|
119
|
+
if (!sourceText)
|
|
120
|
+
throw new Error('sourceText is required.');
|
|
121
|
+
const copies = await listShapeKeyCopies({ shapeIri: data.shapeIri, key: data.key });
|
|
122
|
+
const following = copies.filter((c) => !c.overridden);
|
|
123
|
+
for (const copy of following) {
|
|
124
|
+
// A declaration refresh: it never marks the copy overridden.
|
|
125
|
+
await upsertTranslationKey(asKeyInput(copy, sourceText), { ...authorship, source: 'declaration-sync' });
|
|
126
|
+
}
|
|
127
|
+
return { updatedApps: appsOf(following), skippedOverrides: appsOf(copies.filter((c) => c.overridden)) };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Clear the override on one shape key's copies: every overridden copy, or only `appId`'s. Each
|
|
131
|
+
* reverts to the text of a copy that follows the shape, or, when none does, to its own snapshot
|
|
132
|
+
* (`shapeSource`); with neither, it keeps its own text. The flag is reset (`overridden: false`)
|
|
133
|
+
* and the snapshot removed, so the copy follows the shape again. Returns the apps cleared; a copy
|
|
134
|
+
* with no text at all is not written and not counted.
|
|
135
|
+
*/
|
|
136
|
+
export async function clearShapeKeyOverride(data, authorship = {}) {
|
|
137
|
+
var _a, _b;
|
|
138
|
+
const copies = await listShapeKeyCopies({ shapeIri: data.shapeIri, key: data.key });
|
|
139
|
+
const overridden = copies.filter((c) => c.overridden && (data.appId === undefined || c.appId === data.appId));
|
|
140
|
+
const canonical = (_a = copies.find((c) => !c.overridden && c.sourceText !== undefined)) === null || _a === void 0 ? void 0 : _a.sourceText;
|
|
141
|
+
const cleared = [];
|
|
142
|
+
for (const copy of overridden) {
|
|
143
|
+
const next = (_b = canonical !== null && canonical !== void 0 ? canonical : copy.shapeSource) !== null && _b !== void 0 ? _b : copy.sourceText;
|
|
144
|
+
if (next === undefined)
|
|
145
|
+
continue;
|
|
146
|
+
// An explicit app edit: the only write that may reset the override flag.
|
|
147
|
+
await upsertTranslationKey({ ...asKeyInput(copy, next), overridden: false }, { ...authorship, source: 'app-edit' });
|
|
148
|
+
if (copy.shapeSource !== undefined) {
|
|
149
|
+
// In the mutation DSL an explicit `undefined` removes the value.
|
|
150
|
+
await TranslationKey.update({ shapeSource: undefined }).for({ id: copy.id });
|
|
151
|
+
}
|
|
152
|
+
cleared.push(copy);
|
|
153
|
+
}
|
|
154
|
+
return { clearedApps: appsOf(cleared) };
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=shape-key-copies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape-key-copies.js","sourceRoot":"","sources":["../../src/shape-key-copies.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shape keys across apps: every app that uses a shape holds its own copy of each translation key\n * the shape carries (`TranslationKey` with `ofShape`, one per `ofApplication`). These operations\n * act on all of a shape key's copies at once:\n *\n * - {@link listShapeKeyCopies} reads them, with each copy's override state and the languages it\n * is translated into;\n * - {@link propagateShapeKeySource} gives every copy that follows the shape a new source text;\n * a copy the app reworded (`overridden`) keeps its own text;\n * - {@link clearShapeKeyOverride} reverts overridden copies (all, or one app's) to the shape's\n * current text, so they follow the shape again.\n *\n * Every text change goes through {@link upsertTranslationKey}, so it gets a new key version and\n * marks the key's units stale exactly as any other source change does. Like the other key writes,\n * they run on the default dataset (`LinkedStorage`); a host that keeps each app's data in its own\n * dataset runs them in that dataset's context. They decide nothing about who may call them: the\n * host checks that before.\n */\nimport { TranslationKey } from './shapes/TranslationKey.js';\nimport { TranslationUnit } from './shapes/TranslationUnit.js';\nimport {\n upsertTranslationKey,\n type TranslationKeyInput,\n type TranslationKeyWriteContext,\n} from './shapes/TranslationProvider.js';\n\n/** One app's copy of a shape key. */\nexport interface ShapeKeyCopyRecord {\n /** The key node. */\n id: string;\n key: string;\n shapeIri: string;\n appId: string;\n sourceText?: string;\n /** The app reworded this copy; propagation leaves it alone. */\n overridden: boolean;\n /** The shape's text when the app first reworded the copy: what a clear reverts to without a sibling. */\n shapeSource?: string;\n ofProperty?: string;\n fromPackage?: string;\n namespace?: string;\n description?: string;\n kind?: 'ui' | 'content';\n format?: 'simple' | 'icu';\n /** The languages this copy has a unit for, sorted. */\n languages: string[];\n}\n\nexport interface ShapeKeyQuery {\n /** One shape's keys; omitted, every shape key. */\n shapeIri?: string;\n /** One key name of that shape. */\n key?: string;\n}\n\nconst iri = (value: unknown): string | undefined =>\n typeof value === 'string' ? value || undefined : (value as { id?: string } | undefined)?.id || undefined;\nconst text = (value: unknown): string | undefined =>\n value === undefined || value === null || value === '' ? undefined : String(value);\n/** The flag as stored: a boolean, or the string \"true\" an older writer left. */\nconst isOverridden = (value: unknown): boolean => value === true || value === 'true';\n\n/** The node a key write addresses: `upsertTranslationKey` keys a copy by its app and key name. */\nconst keyIri = (appId: string, key: string): string =>\n `${appId.replace(/\\/$/, '')}/translation/key/${encodeURIComponent(key)}`;\n\nconst keyWhere = (query: ShapeKeyQuery) => (k: any) => {\n const shape = query.shapeIri ? k.ofShape.equals({ id: query.shapeIri }) : k.ofShape.isDefined();\n return query.key === undefined ? shape : shape.and(k.key.equals(query.key));\n};\n\n/**\n * The copies of the shape keys `query` names, one per app. A key that names no app is not a\n * copy and is left out.\n */\nexport async function listShapeKeyCopies(query: ShapeKeyQuery = {}): Promise<ShapeKeyCopyRecord[]> {\n const [keys, units] = await Promise.all([\n TranslationKey.select((k) => [\n k.key,\n k.ofShape,\n k.ofApplication,\n k.sourceText,\n k.overridden,\n k.shapeSource,\n k.ofProperty,\n k.fromPackage,\n k.namespace,\n k.description,\n k.kind,\n k.format,\n ]).where(keyWhere(query) as any),\n TranslationUnit.select((u) => [u.ofKey, u.language]).where(((u: any) => keyWhere(query)(u.ofKey)) as any),\n ]);\n const languagesOf = new Map<string, Set<string>>();\n for (const unit of (units ?? []) as any[]) {\n const keyId = iri(unit.ofKey);\n const language = text(unit.language);\n if (!keyId || !language) continue;\n const set = languagesOf.get(keyId) ?? new Set<string>();\n set.add(language);\n languagesOf.set(keyId, set);\n }\n const copies: ShapeKeyCopyRecord[] = [];\n for (const row of (keys ?? []) as any[]) {\n const id = iri(row.id);\n const key = text(row.key);\n const shapeIri = iri(row.ofShape);\n const appId = iri(row.ofApplication);\n if (!id || !key || !shapeIri || !appId) continue;\n copies.push({\n id,\n key,\n shapeIri,\n appId,\n sourceText: text(row.sourceText),\n overridden: isOverridden(row.overridden),\n shapeSource: text(row.shapeSource),\n ofProperty: iri(row.ofProperty),\n fromPackage: text(row.fromPackage),\n namespace: text(row.namespace),\n description: text(row.description),\n kind: row.kind === 'content' ? 'content' : row.kind === 'ui' ? 'ui' : undefined,\n format: row.format === 'icu' ? 'icu' : row.format === 'simple' ? 'simple' : undefined,\n languages: [...(languagesOf.get(id) ?? [])].sort(),\n });\n }\n return copies;\n}\n\n/** The copy's own declaration, so a text write keeps what the copy declares. */\nfunction asKeyInput(copy: ShapeKeyCopyRecord, sourceText: string): TranslationKeyInput {\n if (copy.id !== keyIri(copy.appId, copy.key)) {\n // A key write addresses the app's key node; writing this copy there would create a second key.\n throw new Error(`Shape key copy ${copy.id} is not at its app's key node; it was not written.`);\n }\n return {\n appId: copy.appId,\n key: copy.key,\n sourceText,\n namespace: copy.namespace,\n description: copy.description,\n kind: copy.kind,\n ofShape: copy.shapeIri,\n ofProperty: copy.ofProperty,\n fromPackage: copy.fromPackage,\n format: copy.format,\n };\n}\n\nconst appsOf = (copies: readonly ShapeKeyCopyRecord[]): string[] => [...new Set(copies.map((c) => c.appId))].sort();\n\nexport interface ShapeKeyPropagation {\n /** Apps whose copy now has the new text (or had it already). */\n updatedApps: string[];\n /** Apps whose copy is overridden and was left alone. */\n skippedOverrides: string[];\n}\n\n/**\n * Give one shape key a new source text in every app whose copy follows the shape. Overridden\n * copies keep their text. A copy whose text changes gets a new key version and stale units.\n */\nexport async function propagateShapeKeySource(\n data: { shapeIri: string; key: string; sourceText: string },\n authorship: Pick<TranslationKeyWriteContext, 'createdBy'> = {}\n): Promise<ShapeKeyPropagation> {\n const sourceText = data.sourceText?.trim();\n if (!sourceText) throw new Error('sourceText is required.');\n const copies = await listShapeKeyCopies({ shapeIri: data.shapeIri, key: data.key });\n const following = copies.filter((c) => !c.overridden);\n for (const copy of following) {\n // A declaration refresh: it never marks the copy overridden.\n await upsertTranslationKey(asKeyInput(copy, sourceText), { ...authorship, source: 'declaration-sync' });\n }\n return { updatedApps: appsOf(following), skippedOverrides: appsOf(copies.filter((c) => c.overridden)) };\n}\n\n/**\n * Clear the override on one shape key's copies: every overridden copy, or only `appId`'s. Each\n * reverts to the text of a copy that follows the shape, or, when none does, to its own snapshot\n * (`shapeSource`); with neither, it keeps its own text. The flag is reset (`overridden: false`)\n * and the snapshot removed, so the copy follows the shape again. Returns the apps cleared; a copy\n * with no text at all is not written and not counted.\n */\nexport async function clearShapeKeyOverride(\n data: { shapeIri: string; key: string; appId?: string },\n authorship: Pick<TranslationKeyWriteContext, 'createdBy'> = {}\n): Promise<{ clearedApps: string[] }> {\n const copies = await listShapeKeyCopies({ shapeIri: data.shapeIri, key: data.key });\n const overridden = copies.filter((c) => c.overridden && (data.appId === undefined || c.appId === data.appId));\n const canonical = copies.find((c) => !c.overridden && c.sourceText !== undefined)?.sourceText;\n const cleared: ShapeKeyCopyRecord[] = [];\n for (const copy of overridden) {\n const next = canonical ?? copy.shapeSource ?? copy.sourceText;\n if (next === undefined) continue;\n // An explicit app edit: the only write that may reset the override flag.\n await upsertTranslationKey({ ...asKeyInput(copy, next), overridden: false }, { ...authorship, source: 'app-edit' });\n if (copy.shapeSource !== undefined) {\n // In the mutation DSL an explicit `undefined` removes the value.\n await TranslationKey.update({ shapeSource: undefined } as any).for({ id: copy.id } as any);\n }\n cleared.push(copy);\n }\n return { clearedApps: appsOf(cleared) };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@_linked/translation",
|
|
3
3
|
"description": "LINKED translation management — graph-native i18n keys + units, Tolgee-compatible client, CDN delivery.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"packageManager": "npm@12.2.0",
|
|
6
6
|
"linked": {
|
|
7
7
|
"kind": "library"
|