@_linked/translation 0.8.0 → 0.8.2

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.
@@ -11,7 +11,7 @@ import { TranslationKeyVersion } from './TranslationKeyVersion.js';
11
11
  * (`status`, `decidedAt`, `decidedBy`).
12
12
  */
13
13
  export declare class TranslationRevision extends Shape {
14
- static targetClass: import("@_linked/core/utils/NodeReference").NodeReferenceValue;
14
+ static targetClass: import("@_linked/core/utils/NodeReference.js").NodeReferenceValue;
15
15
  /** IRI of the owning Application (same app-scoping law as Key/Unit). */
16
16
  get ofApplication(): string;
17
17
  /** The key, NOT the unit — revisions can exist before any unit does. */
@@ -8,7 +8,7 @@ import { TranslationKeyVersion } from './TranslationKeyVersion.js';
8
8
  * provenance — that a bare literal cannot hold.
9
9
  */
10
10
  export declare class TranslationUnit extends Shape {
11
- static targetClass: import("@_linked/core/utils/NodeReference").NodeReferenceValue;
11
+ static targetClass: import("@_linked/core/utils/NodeReference.js").NodeReferenceValue;
12
12
  /** IRI of the owning Application (explicitly app-scoped per Plan 014 AD-I). */
13
13
  get ofApplication(): string;
14
14
  get ofKey(): TranslationKey;
package/package.json CHANGED
@@ -1,8 +1,8 @@
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.8.0",
5
- "packageManager": "npm@12.1.0",
4
+ "version": "0.8.2",
5
+ "packageManager": "npm@12.2.0",
6
6
  "linkedPackage": true,
7
7
  "type": "module",
8
8
  "main": "lib/esm/index.js",
@@ -90,11 +90,11 @@
90
90
  }
91
91
  },
92
92
  "dependencies": {
93
- "@_linked/core": "^2.22.8",
94
- "@_linked/server-utils": "^1.9.0",
95
- "@formatjs/icu-messageformat-parser": "3.5.20",
93
+ "@_linked/core": "^2.36.1",
94
+ "@_linked/server-utils": "^1.14.0",
95
+ "@formatjs/icu-messageformat-parser": "3.5.21",
96
96
  "fflate": "^0.8.2",
97
- "intl-messageformat": "12.1.2"
97
+ "intl-messageformat": "12.1.3"
98
98
  },
99
99
  "peerDependencies": {
100
100
  "react": "*",
@@ -121,6 +121,6 @@
121
121
  "react": "^19.3.0",
122
122
  "react-dom": "^19.3.0",
123
123
  "typescript": "5.9.3",
124
- "vitest": "^3.2.4"
124
+ "vitest": "^5.0.0"
125
125
  }
126
126
  }
@@ -1,3 +1,5 @@
1
+ import { sharedState } from '@_linked/core/utils/sharedState';
2
+
1
3
  export type TranslationAuthoringAction =
2
4
  | 'manage'
3
5
  | 'review'
@@ -16,7 +18,11 @@ export type TranslationAuthorizationResolver = (
16
18
  request: TranslationAuthorizationRequest,
17
19
  ) => Promise<boolean>;
18
20
 
19
- let resolver: TranslationAuthorizationResolver | undefined;
21
+ // On globalThis so a second copy of this module sees the resolver the host configured on the
22
+ // first; a copy that cannot see it would deny every author.
23
+ const state = sharedState('@_linked/translation', 'authorization', () => ({
24
+ resolver: undefined as TranslationAuthorizationResolver | undefined,
25
+ }));
20
26
 
21
27
  /**
22
28
  * Host hook: the host decides who may read and author an app's translations
@@ -29,7 +35,7 @@ let resolver: TranslationAuthorizationResolver | undefined;
29
35
  export function configureTranslationAuthorization(
30
36
  next?: TranslationAuthorizationResolver,
31
37
  ): void {
32
- resolver = next;
38
+ state.resolver = next;
33
39
  }
34
40
 
35
41
  /**
@@ -39,6 +45,7 @@ export function configureTranslationAuthorization(
39
45
  export async function canAuthorTranslation(
40
46
  request: TranslationAuthorizationRequest,
41
47
  ): Promise<boolean> {
48
+ const resolver = state.resolver;
42
49
  if (!resolver) return false;
43
50
  try {
44
51
  return (await resolver(request)) === true;
package/src/exchange.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { sharedState } from '@_linked/core/utils/sharedState';
1
2
  import type { MessageFormat } from './core/messages.js';
2
3
  import { canonicalLanguageTag } from './key-version.js';
3
4
  import type { TranslationState } from './records.js';
@@ -131,7 +132,11 @@ const EXCHANGE_STATES = new Set<TranslationState>([
131
132
  'reviewed',
132
133
  'stale',
133
134
  ]);
134
- const adapters = new Map<TranslationFormat, TranslationFormatAdapter>();
135
+ // On globalThis so an adapter the host registers on one copy of this module is found by
136
+ // another copy.
137
+ const { adapters } = sharedState('@_linked/translation', 'exchange', () => ({
138
+ adapters: new Map<TranslationFormat, TranslationFormatAdapter>(),
139
+ }));
135
140
 
136
141
  function record(value: unknown, label: string): Record<string, unknown> {
137
142
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -402,7 +407,11 @@ export function isConfirmedBuildDeclaration(
402
407
  export function registerTranslationFormatAdapter(
403
408
  adapter: TranslationFormatAdapter,
404
409
  ): void {
405
- if (adapters.has(adapter.format)) {
410
+ const registered = adapters.get(adapter.format);
411
+ // The registry is process-wide, so the same adapter registered again (by a second copy of
412
+ // the host module that registers it) is not a conflict; a different adapter for the format is.
413
+ if (registered === adapter) return;
414
+ if (registered) {
406
415
  throw new Error(
407
416
  `Translation format adapter "${adapter.format}" is already registered.`,
408
417
  );
package/src/mt.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * SDK imports — concrete providers live with the host application and are
9
9
  * registered at boot.
10
10
  */
11
+ import { sharedState } from '@_linked/core/utils/sharedState';
11
12
  import {
12
13
  analyzeMessage,
13
14
  compareMessageArguments,
@@ -72,7 +73,11 @@ export interface TranslationMtProvider {
72
73
  translateBatch(request: MtBatchRequest): Promise<MtBatchResult[]>;
73
74
  }
74
75
 
75
- const providers = new Map<string, TranslationMtProvider>();
76
+ // On globalThis so a provider the host registers on one copy of this module is found by
77
+ // another copy.
78
+ const { providers } = sharedState('@_linked/translation', 'mt', () => ({
79
+ providers: new Map<string, TranslationMtProvider>(),
80
+ }));
76
81
 
77
82
  export function registerMtProvider(provider: TranslationMtProvider): void {
78
83
  providers.set(provider.key, provider);
package/src/react.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import { sharedState } from '@_linked/core/utils/sharedState';
1
2
  import {
2
3
  languageFallbacks,
3
4
  type TranslationLanguageDefinition,
@@ -100,7 +101,11 @@ interface TranslationContextValue {
100
101
  dir: 'ltr' | 'rtl';
101
102
  }
102
103
 
103
- const TranslationContext = createContext<TranslationContextValue | null>(null);
104
+ // The context object is kept on globalThis: a context created per copy of this module would
105
+ // leave a consumer in one copy blind to a provider from the other (it gets the null default).
106
+ const { TranslationContext } = sharedState('@_linked/translation', 'react.contexts', () => ({
107
+ TranslationContext: createContext<TranslationContextValue | null>(null),
108
+ }));
104
109
 
105
110
  export interface TranslationProviderProps {
106
111
  children: React.ReactNode;
@@ -1602,21 +1602,21 @@ export class TranslationProvider extends ShapeProvider {
1602
1602
  }
1603
1603
  }
1604
1604
 
1605
- @callable('user')
1605
+ @callable('signed-in')
1606
1606
  async listKeys(data: { appId: string }) {
1607
1607
  const actor = this.requireActor();
1608
1608
  await this.requireAccess(actor, { appId: data?.appId }, 'read');
1609
1609
  return listTranslationKeys(data);
1610
1610
  }
1611
1611
 
1612
- @callable('user')
1612
+ @callable('signed-in')
1613
1613
  async listEntries(data: { appId: string }): Promise<TranslationEntryRecord[]> {
1614
1614
  const actor = this.requireActor();
1615
1615
  await this.requireAccess(actor, { appId: data?.appId }, 'read');
1616
1616
  return listTranslationEntries(data);
1617
1617
  }
1618
1618
 
1619
- @callable('user')
1619
+ @callable('signed-in')
1620
1620
  async listKeyVersions(data: {
1621
1621
  appId: string;
1622
1622
  key?: string;
@@ -1626,7 +1626,7 @@ export class TranslationProvider extends ShapeProvider {
1626
1626
  return listTranslationKeyVersions(data);
1627
1627
  }
1628
1628
 
1629
- @callable('user')
1629
+ @callable('signed-in')
1630
1630
  async listMemoryMatches(data: {
1631
1631
  appId: string;
1632
1632
  key: string;
@@ -1637,7 +1637,7 @@ export class TranslationProvider extends ShapeProvider {
1637
1637
  return listTranslationMemoryMatches(data);
1638
1638
  }
1639
1639
 
1640
- @callable('user')
1640
+ @callable('signed-in')
1641
1641
  async proposeRevision(data: {
1642
1642
  appId: string;
1643
1643
  key: string;
@@ -1661,7 +1661,7 @@ export class TranslationProvider extends ShapeProvider {
1661
1661
  });
1662
1662
  }
1663
1663
 
1664
- @callable('user')
1664
+ @callable('signed-in')
1665
1665
  async listProposals(data: {
1666
1666
  appId: string;
1667
1667
  language?: string;
@@ -1682,7 +1682,7 @@ export class TranslationProvider extends ShapeProvider {
1682
1682
  return proposals.filter((_, index) => allowed[index]);
1683
1683
  }
1684
1684
 
1685
- @callable('user')
1685
+ @callable('signed-in')
1686
1686
  async decideProposal(data: {
1687
1687
  appId: string;
1688
1688
  revisionId: string;
@@ -1706,7 +1706,7 @@ export class TranslationProvider extends ShapeProvider {
1706
1706
  return decideTranslationProposal(data, actor);
1707
1707
  }
1708
1708
 
1709
- @callable('user')
1709
+ @callable('signed-in')
1710
1710
  async listMemory(data: {
1711
1711
  appId: string;
1712
1712
  }): Promise<TranslationMemoryRecord[]> {
@@ -1715,7 +1715,7 @@ export class TranslationProvider extends ShapeProvider {
1715
1715
  return listTranslationMemory(data);
1716
1716
  }
1717
1717
 
1718
- @callable('user')
1718
+ @callable('signed-in')
1719
1719
  async pretranslateMemory(data: {
1720
1720
  appId: string;
1721
1721
  language: string;
@@ -1726,7 +1726,7 @@ export class TranslationProvider extends ShapeProvider {
1726
1726
  return pretranslateFromMemory(data, { author: actor });
1727
1727
  }
1728
1728
 
1729
- @callable('user')
1729
+ @callable('signed-in')
1730
1730
  async applyMemoryMatch(data: {
1731
1731
  appId: string;
1732
1732
  key: string;
@@ -1738,7 +1738,7 @@ export class TranslationProvider extends ShapeProvider {
1738
1738
  return applyTranslationMemoryMatch(data, { author: actor });
1739
1739
  }
1740
1740
 
1741
- @callable('user')
1741
+ @callable('signed-in')
1742
1742
  async listReleases(data: {
1743
1743
  appId: string;
1744
1744
  }): Promise<TranslationReleaseRecord[]> {
@@ -1747,7 +1747,7 @@ export class TranslationProvider extends ShapeProvider {
1747
1747
  return listTranslationReleases(data);
1748
1748
  }
1749
1749
 
1750
- @callable('user')
1750
+ @callable('signed-in')
1751
1751
  async createRelease(
1752
1752
  data: Omit<TranslationReleaseRecord, 'id' | 'createdAt'> & {
1753
1753
  createdAt?: string;
@@ -1758,7 +1758,7 @@ export class TranslationProvider extends ShapeProvider {
1758
1758
  return createTranslationRelease(data);
1759
1759
  }
1760
1760
 
1761
- @callable('user')
1761
+ @callable('signed-in')
1762
1762
  async advanceReleaseHotfix(data: {
1763
1763
  id: string;
1764
1764
  expectedSequence: number;
@@ -1772,14 +1772,14 @@ export class TranslationProvider extends ShapeProvider {
1772
1772
  return advanceTranslationReleaseHotfix({ ...data, appId });
1773
1773
  }
1774
1774
 
1775
- @callable('user')
1775
+ @callable('signed-in')
1776
1776
  async upsertKey(data: TranslationKeyInput): Promise<{ id: string }> {
1777
1777
  const actor = this.requireActor();
1778
1778
  await this.requireAccess(actor, data, 'manage');
1779
1779
  return upsertTranslationKey(data, { createdBy: actor });
1780
1780
  }
1781
1781
 
1782
- @callable('user')
1782
+ @callable('signed-in')
1783
1783
  async upsertUnit(data: TranslationUnitInput): Promise<{ id: string }> {
1784
1784
  const actor = this.requireActor();
1785
1785
  await this.requireAccess(actor, data, 'review');
@@ -1795,14 +1795,14 @@ export class TranslationProvider extends ShapeProvider {
1795
1795
  * anchors the IRIs. Uniqueness = (term, language) — upsert is
1796
1796
  * delete-then-create on that identity.
1797
1797
  */
1798
- @callable('user')
1798
+ @callable('signed-in')
1799
1799
  async listGlossary(data: { appId: string }): Promise<GlossaryTermRecord[]> {
1800
1800
  const actor = this.requireActor();
1801
1801
  await this.requireAccess(actor, { appId: data?.appId }, 'read');
1802
1802
  return listGlossaryTerms(data);
1803
1803
  }
1804
1804
 
1805
- @callable('user')
1805
+ @callable('signed-in')
1806
1806
  async upsertGlossaryTerm(data: {
1807
1807
  appId: string;
1808
1808
  term: string;
@@ -1845,7 +1845,7 @@ export class TranslationProvider extends ShapeProvider {
1845
1845
  return { id };
1846
1846
  }
1847
1847
 
1848
- @callable('user')
1848
+ @callable('signed-in')
1849
1849
  async deleteGlossaryTerm(data: { appId: string; id: string }): Promise<{ deleted: boolean }> {
1850
1850
  const actor = this.requireActor();
1851
1851
  await this.requireAccess(actor, data, 'manage');
@@ -1860,7 +1860,7 @@ export class TranslationProvider extends ShapeProvider {
1860
1860
  }
1861
1861
 
1862
1862
  /** History for one (key, language) cell — newest first (Plan 017 AD-P). */
1863
- @callable('user')
1863
+ @callable('signed-in')
1864
1864
  async listRevisions(data: {
1865
1865
  appId: string;
1866
1866
  key: string;