@_linked/translation 0.9.4 → 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 +2 -3
- package/src/archive.ts +0 -914
- package/src/authorization.ts +0 -56
- package/src/backend.ts +0 -2
- package/src/cdn-loader.ts +0 -264
- package/src/checks.ts +0 -413
- package/src/compile.ts +0 -168
- package/src/core/messages.ts +0 -139
- package/src/data/translation.ttl +0 -489
- package/src/discovery.ts +0 -594
- package/src/exchange.ts +0 -437
- package/src/export.ts +0 -138
- package/src/formats/csv.ts +0 -467
- package/src/formats/json.ts +0 -596
- package/src/formats/xliff.ts +0 -733
- package/src/import.ts +0 -648
- package/src/index.ts +0 -201
- package/src/key-sync-node.ts +0 -269
- package/src/key-sync.ts +0 -208
- package/src/key-version.ts +0 -207
- package/src/languages.ts +0 -102
- package/src/linked-discovery.ts +0 -385
- package/src/message-format.ts +0 -214
- package/src/mt.ts +0 -253
- package/src/ontologies/translation.register.ts +0 -17
- package/src/ontologies/translation.ts +0 -239
- package/src/package.ts +0 -14
- package/src/react.tsx +0 -378
- package/src/records.ts +0 -181
- package/src/release.ts +0 -754
- package/src/shape-catalog.ts +0 -198
- package/src/shapes/GlossaryTerm.ts +0 -61
- package/src/shapes/TranslationInventoryOrigin.ts +0 -131
- package/src/shapes/TranslationKey.ts +0 -183
- package/src/shapes/TranslationKeyVersion.ts +0 -77
- package/src/shapes/TranslationLanguage.ts +0 -51
- package/src/shapes/TranslationProvider.ts +0 -1927
- package/src/shapes/TranslationRelease.ts +0 -114
- package/src/shapes/TranslationRevision.ts +0 -105
- package/src/shapes/TranslationUnit.ts +0 -72
- package/src/shapes/index.ts +0 -13
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { createNameSpace } from '@_linked/core/utils/NameSpace';
|
|
2
|
-
import { loadOntologyFiles } from '@_linked/core/ontology/loadOntologyFiles';
|
|
3
|
-
|
|
4
|
-
/** The package's ontology data, relative to this module (Turtle, copied into `lib/` by the build). */
|
|
5
|
-
export const dataFiles = ['../data/translation.ttl'];
|
|
6
|
-
|
|
7
|
-
/** Reads the package's ontology data (server only). */
|
|
8
|
-
export const loadData = () => loadOntologyFiles(import.meta.url, dataFiles);
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Public ontology namespace for the translation package. First-party Linked
|
|
12
|
-
* ontologies live at `https://linked.cm/ont/{ontologySlug}/{localName}`, where
|
|
13
|
-
* the ontology slug — and the prefix label — is the package's public slug
|
|
14
|
-
* (the LINKED first-party ontology namespace rule). So terms are
|
|
15
|
-
* `https://linked.cm/ont/translation/TranslationKey` and the prefix is
|
|
16
|
-
* `translation:`. Before 0.5.0 this was `https://id.linked.cm/translation/vocab#`.
|
|
17
|
-
*/
|
|
18
|
-
export var ns = createNameSpace('https://linked.cm/ont/translation/');
|
|
19
|
-
export var _self = ns('');
|
|
20
|
-
|
|
21
|
-
// ── Classes ─────────────────────────────────────────────────────────────
|
|
22
|
-
export var TranslationKey = ns('TranslationKey');
|
|
23
|
-
export var TranslationKeyVersion = ns('TranslationKeyVersion');
|
|
24
|
-
export var TranslationRelease = ns('TranslationRelease');
|
|
25
|
-
export var TranslationUnit = ns('TranslationUnit');
|
|
26
|
-
export var TranslationRevision = ns('TranslationRevision');
|
|
27
|
-
export var GlossaryTerm = ns('GlossaryTerm');
|
|
28
|
-
export var TranslationInventoryOrigin = ns('TranslationInventoryOrigin');
|
|
29
|
-
|
|
30
|
-
// Open language resources; BCP-47 is a string identifier, not an enum.
|
|
31
|
-
export var TranslationLanguage = ns('TranslationLanguage');
|
|
32
|
-
export var languageIdentifier = ns('languageIdentifier');
|
|
33
|
-
export var languageCode = ns('languageCode');
|
|
34
|
-
export var nativeName = ns('nativeName');
|
|
35
|
-
export var englishName = ns('englishName');
|
|
36
|
-
export var textDirection = ns('textDirection');
|
|
37
|
-
export var parentLanguage = ns('parentLanguage');
|
|
38
|
-
export var fallbackLanguage = ns('fallbackLanguage');
|
|
39
|
-
export var languageEnabled = ns('languageEnabled');
|
|
40
|
-
export var languageSupported = ns('languageSupported');
|
|
41
|
-
|
|
42
|
-
// ── TranslationKey properties ───────────────────────────────────────────
|
|
43
|
-
export var namespace = ns('namespace');
|
|
44
|
-
export var key = ns('key');
|
|
45
|
-
export var sourceText = ns('sourceText'); // canonical English (Plan 014 AD-J/R7)
|
|
46
|
-
export var description = ns('description');
|
|
47
|
-
export var kind = ns('kind'); // 'ui' | 'content' (AD-I)
|
|
48
|
-
export var ofApplication = ns('ofApplication'); // owning app IRI (AD-H/R2)
|
|
49
|
-
export var ofNode = ns('ofNode'); // content-translation subject (kind:'content')
|
|
50
|
-
export var ofField = ns('ofField');
|
|
51
|
-
export var ofShape = ns('ofShape'); // semantic SHACL shape context for package keys
|
|
52
|
-
export var ofProperty = ns('ofProperty'); // semantic predicate context for package keys
|
|
53
|
-
export var ofResource = ns('ofResource'); // arbitrary semantic resource (action, group, condition, port)
|
|
54
|
-
export var fromPackage = ns('fromPackage'); // package that supplied a seed catalog
|
|
55
|
-
export var overridden = ns('overridden'); // app diverged this shape key from canonical → excluded from shape propagation (AD-N)
|
|
56
|
-
export var shapeSource = ns('shapeSource'); // canonical source snapshot at override time → the value "clear override" reverts to (AD-N)
|
|
57
|
-
export var format = ns('format'); // 'simple' | 'icu' (AD-J/R8)
|
|
58
|
-
export var currentVersion = ns('currentVersion'); // → TranslationKeyVersion
|
|
59
|
-
|
|
60
|
-
// ── TranslationKeyVersion properties (Plan 018) ────────────────────────
|
|
61
|
-
export var versionId = ns('versionId');
|
|
62
|
-
export var sourceLanguage = ns('sourceLanguage');
|
|
63
|
-
export var argumentSignature = ns('argumentSignature');
|
|
64
|
-
export var contractHash = ns('contractHash');
|
|
65
|
-
export var sourceHash = ns('sourceHash');
|
|
66
|
-
export var supersedes = ns('supersedes');
|
|
67
|
-
export var createdBy = ns('createdBy');
|
|
68
|
-
|
|
69
|
-
// ── TranslationRelease properties (Plan 018) ───────────────────────────
|
|
70
|
-
export var branch = ns('branch');
|
|
71
|
-
export var releaseId = ns('releaseId');
|
|
72
|
-
export var buildId = ns('buildId');
|
|
73
|
-
export var channel = ns('channel');
|
|
74
|
-
export var contractSetHash = ns('contractSetHash');
|
|
75
|
-
export var manifestHash = ns('manifestHash');
|
|
76
|
-
export var hotfixSequence = ns('hotfixSequence');
|
|
77
|
-
export var publishedAt = ns('publishedAt');
|
|
78
|
-
export var supportedUntil = ns('supportedUntil');
|
|
79
|
-
export var qualitySummary = ns('qualitySummary');
|
|
80
|
-
|
|
81
|
-
// ── CapabilityAppConfig properties (stored in the host's app configuration) ────
|
|
82
|
-
export var languages = ns('languages'); // active BCP-47 tags, including source
|
|
83
|
-
export var defaultLanguage = ns('defaultLanguage'); // authored/source language
|
|
84
|
-
export var cdnTarget = ns('cdnTarget'); // delivery base URL / bucket prefix — the "repoint" knob (AD-K, P2)
|
|
85
|
-
export var autoPublish = ns('autoPublish'); // boolean — overridable auto-publish-on-edit toggle (P2, default off)
|
|
86
|
-
export var reviewedOnlyLanguage = ns('reviewedOnlyLanguage'); // BCP-47 tags published reviewed-only; absence ⇒ machine-ok (AD-K/R9, P2)
|
|
87
|
-
export var mtDefault = ns('mtDefault'); // default MT provider key (AD-L, unused until P4)
|
|
88
|
-
export var mtFormality = ns('mtFormality'); // multi-value: "formal" (app default) or "de:informal" (per-language) — Plan 017 §7.4b
|
|
89
|
-
export var mtStyleNote = ns('mtStyleNote'); // free-text tone guidance fed to LLM MT providers (Plan 017 §7.4b)
|
|
90
|
-
|
|
91
|
-
// ── TranslationUnit properties ──────────────────────────────────────────
|
|
92
|
-
export var ofKey = ns('ofKey'); // → TranslationKey
|
|
93
|
-
export var ofKeyVersion = ns('ofKeyVersion'); // → TranslationKeyVersion
|
|
94
|
-
export var language = ns('language'); // BCP-47 tag
|
|
95
|
-
export var text = ns('text');
|
|
96
|
-
export var state = ns('state'); // untranslated | machine | reviewed | stale
|
|
97
|
-
export var updatedAt = ns('updatedAt');
|
|
98
|
-
export var updatedBy = ns('updatedBy');
|
|
99
|
-
export var origin = ns('origin'); // current-value provenance, e.g. `tm`
|
|
100
|
-
|
|
101
|
-
// ── TranslationRevision properties (Plan 017 AD-P) ─────────────────────
|
|
102
|
-
// One append-only node per authored value: history (`applied`), translator
|
|
103
|
-
// proposals (`proposed`), and MT suggestions (`suggested`) share the shape.
|
|
104
|
-
export var status = ns('status'); // applied | proposed | suggested | accepted | rejected | superseded
|
|
105
|
-
export var author = ns('author'); // WebID or MT provider IRI
|
|
106
|
-
export var authorKind = ns('authorKind'); // 'human' | 'machine'
|
|
107
|
-
export var mtProvider = ns('mtProvider'); // provider key when authorKind=machine
|
|
108
|
-
export var basedOnText = ns('basedOnText'); // unit text at authoring time (conflict detection)
|
|
109
|
-
export var note = ns('note'); // translator/reviewer comment
|
|
110
|
-
export var createdAt = ns('createdAt');
|
|
111
|
-
export var decidedAt = ns('decidedAt'); // when a proposal/suggestion was accepted/rejected
|
|
112
|
-
export var decidedBy = ns('decidedBy'); // WebID of the reviewer
|
|
113
|
-
|
|
114
|
-
// ── GlossaryTerm properties ─────────────────────────────────────────────
|
|
115
|
-
export var term = ns('term');
|
|
116
|
-
export var glossaryTranslation = ns('glossaryTranslation');
|
|
117
|
-
export var glossaryLanguage = ns('glossaryLanguage');
|
|
118
|
-
export var termType = ns('termType'); // prefer | require | keep | forbid (Plan 017 B2-1 termbase)
|
|
119
|
-
export var useInstead = ns('useInstead'); // replacement for a forbidden term/spelling
|
|
120
|
-
export var caseSensitive = ns('caseSensitive'); // boolean — exact-casing matching (proper nouns)
|
|
121
|
-
|
|
122
|
-
// ── Branch translation inventory metadata (Plan 017 Phase 8b) ─────────
|
|
123
|
-
// These records are materialized into the active branch metadata dataset,
|
|
124
|
-
// never app data. Confirmed TranslationKey/TranslationUnit records retain
|
|
125
|
-
// their existing app-data placement.
|
|
126
|
-
export var inventoryBranch = ns('inventoryBranch');
|
|
127
|
-
export var inventoryIdentity = ns('inventoryIdentity');
|
|
128
|
-
export var inventoryStatus = ns('inventoryStatus');
|
|
129
|
-
export var inventorySource = ns('inventorySource');
|
|
130
|
-
export var inventorySourceId = ns('inventorySourceId');
|
|
131
|
-
export var inventoryAuthoritative = ns('inventoryAuthoritative');
|
|
132
|
-
export var inventoryFile = ns('inventoryFile');
|
|
133
|
-
export var inventoryLine = ns('inventoryLine');
|
|
134
|
-
export var inventoryOwner = ns('inventoryOwner');
|
|
135
|
-
export var inventoryShape = ns('inventoryShape');
|
|
136
|
-
export var inventoryProperty = ns('inventoryProperty');
|
|
137
|
-
export var inventoryNode = ns('inventoryNode');
|
|
138
|
-
export var inventoryTargetClass = ns('inventoryTargetClass');
|
|
139
|
-
export var inventoryObservedAt = ns('inventoryObservedAt');
|
|
140
|
-
|
|
141
|
-
/** Grouped export for prefix/name access. */
|
|
142
|
-
export const tr = {
|
|
143
|
-
TranslationLanguage,
|
|
144
|
-
languageIdentifier,
|
|
145
|
-
languageCode,
|
|
146
|
-
nativeName,
|
|
147
|
-
englishName,
|
|
148
|
-
textDirection,
|
|
149
|
-
parentLanguage,
|
|
150
|
-
fallbackLanguage,
|
|
151
|
-
languageEnabled,
|
|
152
|
-
languageSupported,
|
|
153
|
-
TranslationKey,
|
|
154
|
-
TranslationKeyVersion,
|
|
155
|
-
TranslationRelease,
|
|
156
|
-
TranslationUnit,
|
|
157
|
-
TranslationRevision,
|
|
158
|
-
GlossaryTerm,
|
|
159
|
-
TranslationInventoryOrigin,
|
|
160
|
-
namespace,
|
|
161
|
-
key,
|
|
162
|
-
sourceText,
|
|
163
|
-
description,
|
|
164
|
-
kind,
|
|
165
|
-
ofApplication,
|
|
166
|
-
ofNode,
|
|
167
|
-
ofField,
|
|
168
|
-
ofShape,
|
|
169
|
-
ofProperty,
|
|
170
|
-
ofResource,
|
|
171
|
-
fromPackage,
|
|
172
|
-
overridden,
|
|
173
|
-
shapeSource,
|
|
174
|
-
format,
|
|
175
|
-
currentVersion,
|
|
176
|
-
versionId,
|
|
177
|
-
sourceLanguage,
|
|
178
|
-
argumentSignature,
|
|
179
|
-
contractHash,
|
|
180
|
-
sourceHash,
|
|
181
|
-
supersedes,
|
|
182
|
-
createdBy,
|
|
183
|
-
branch,
|
|
184
|
-
releaseId,
|
|
185
|
-
buildId,
|
|
186
|
-
channel,
|
|
187
|
-
contractSetHash,
|
|
188
|
-
manifestHash,
|
|
189
|
-
hotfixSequence,
|
|
190
|
-
publishedAt,
|
|
191
|
-
supportedUntil,
|
|
192
|
-
qualitySummary,
|
|
193
|
-
languages,
|
|
194
|
-
defaultLanguage,
|
|
195
|
-
cdnTarget,
|
|
196
|
-
autoPublish,
|
|
197
|
-
reviewedOnlyLanguage,
|
|
198
|
-
mtDefault,
|
|
199
|
-
mtFormality,
|
|
200
|
-
mtStyleNote,
|
|
201
|
-
ofKey,
|
|
202
|
-
ofKeyVersion,
|
|
203
|
-
language,
|
|
204
|
-
text,
|
|
205
|
-
state,
|
|
206
|
-
updatedAt,
|
|
207
|
-
updatedBy,
|
|
208
|
-
origin,
|
|
209
|
-
status,
|
|
210
|
-
author,
|
|
211
|
-
authorKind,
|
|
212
|
-
mtProvider,
|
|
213
|
-
basedOnText,
|
|
214
|
-
note,
|
|
215
|
-
createdAt,
|
|
216
|
-
decidedAt,
|
|
217
|
-
decidedBy,
|
|
218
|
-
term,
|
|
219
|
-
glossaryTranslation,
|
|
220
|
-
glossaryLanguage,
|
|
221
|
-
termType,
|
|
222
|
-
useInstead,
|
|
223
|
-
caseSensitive,
|
|
224
|
-
inventoryBranch,
|
|
225
|
-
inventoryIdentity,
|
|
226
|
-
inventoryStatus,
|
|
227
|
-
inventorySource,
|
|
228
|
-
inventorySourceId,
|
|
229
|
-
inventoryAuthoritative,
|
|
230
|
-
inventoryFile,
|
|
231
|
-
inventoryLine,
|
|
232
|
-
inventoryOwner,
|
|
233
|
-
inventoryShape,
|
|
234
|
-
inventoryProperty,
|
|
235
|
-
inventoryNode,
|
|
236
|
-
inventoryTargetClass,
|
|
237
|
-
inventoryObservedAt,
|
|
238
|
-
};
|
|
239
|
-
|
package/src/package.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { linkedPackage } from '@_linked/core/utils/Package';
|
|
2
|
-
|
|
3
|
-
// First-party LINKED package: no `baseUri`, so core's default root applies and
|
|
4
|
-
// IRIs follow the public rule — shapes `https://linked.cm/shape/translation/{ShapeName}`,
|
|
5
|
-
// package `https://linked.cm/pkg/translation` (arch-02 "Public community").
|
|
6
|
-
export const {
|
|
7
|
-
linkedShape,
|
|
8
|
-
linkedUtil,
|
|
9
|
-
linkedOntology,
|
|
10
|
-
registerPackageExport,
|
|
11
|
-
packageExports,
|
|
12
|
-
packageName,
|
|
13
|
-
getPackageShape,
|
|
14
|
-
} = linkedPackage('@_linked/translation');
|
package/src/react.tsx
DELETED
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
import { sharedState } from '@_linked/core/utils/sharedState';
|
|
2
|
-
import {
|
|
3
|
-
languageFallbacks,
|
|
4
|
-
type TranslationLanguageDefinition,
|
|
5
|
-
} from './languages.js';
|
|
6
|
-
import React, {
|
|
7
|
-
createContext,
|
|
8
|
-
startTransition,
|
|
9
|
-
useCallback,
|
|
10
|
-
useContext,
|
|
11
|
-
useEffect,
|
|
12
|
-
useMemo,
|
|
13
|
-
useState,
|
|
14
|
-
useSyncExternalStore,
|
|
15
|
-
} from 'react';
|
|
16
|
-
import {
|
|
17
|
-
directionFor,
|
|
18
|
-
type MessageFormat,
|
|
19
|
-
pseudoExpand,
|
|
20
|
-
pseudoLocalize,
|
|
21
|
-
translate,
|
|
22
|
-
type TranslationMessages,
|
|
23
|
-
} from './core/messages.js';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Window event that previews a language, text direction or pseudo-locale in
|
|
27
|
-
* every mounted `TranslationProvider`, without changing the user's language.
|
|
28
|
-
*
|
|
29
|
-
* Dispatch it with a {@link TranslationPreviewDetail}:
|
|
30
|
-
*
|
|
31
|
-
* ```ts
|
|
32
|
-
* window.dispatchEvent(
|
|
33
|
-
* new CustomEvent(TRANSLATION_PREVIEW_EVENT, {
|
|
34
|
-
* detail: { language: 'ar', direction: 'ltr', pseudoLocale: 'expanded' },
|
|
35
|
-
* })
|
|
36
|
-
* );
|
|
37
|
-
* ```
|
|
38
|
-
*
|
|
39
|
-
* Each event replaces the whole preview: a field that is missing or invalid
|
|
40
|
-
* falls back to its default (no preview language, `'auto'`, `'off'`), so an
|
|
41
|
-
* empty detail ends the preview. The preview language is never written to
|
|
42
|
-
* storage, and a preview direction wins over the language's own direction.
|
|
43
|
-
*/
|
|
44
|
-
export const TRANSLATION_PREVIEW_EVENT = 'linked:translation-preview';
|
|
45
|
-
|
|
46
|
-
export type TranslationPreviewDirection = 'auto' | 'ltr' | 'rtl';
|
|
47
|
-
export type TranslationPreviewPseudoLocale = 'off' | 'accented' | 'expanded';
|
|
48
|
-
|
|
49
|
-
/** The `detail` of a {@link TRANSLATION_PREVIEW_EVENT}. */
|
|
50
|
-
export interface TranslationPreviewDetail {
|
|
51
|
-
/** BCP-47 tag to render in instead of the selected language. */
|
|
52
|
-
language?: string;
|
|
53
|
-
/** Force a text direction; `'auto'` uses the language's own. */
|
|
54
|
-
direction?: TranslationPreviewDirection;
|
|
55
|
-
/**
|
|
56
|
-
* `'accented'` accents every message; `'expanded'` pads every message by
|
|
57
|
-
* about a third to show controls that only fit their English copy.
|
|
58
|
-
*/
|
|
59
|
-
pseudoLocale?: TranslationPreviewPseudoLocale;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const EMPTY_MESSAGES: Record<string, TranslationMessages> = {};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* React binding for the translation core (Plan 014 P1.5). Kept in a `/react`
|
|
66
|
-
* subpath so non-React consumers use `./core/messages` without pulling React.
|
|
67
|
-
* Message LOADING is caller-injected (`loadMessages`) so this stays transport-
|
|
68
|
-
* agnostic: dev wires it to the host's API, prod to the bundled `{lang}.json` with a
|
|
69
|
-
* CDN overlay (AD-J/AD-K) — the provider doesn't care which.
|
|
70
|
-
*
|
|
71
|
-
* The hook shape mirrors Tolgee (`useTranslate().t(key, default, params)`,
|
|
72
|
-
* `useLanguage()`), so swapping serve's provider touches only its i18n file.
|
|
73
|
-
*/
|
|
74
|
-
|
|
75
|
-
export interface TranslationLanguage {
|
|
76
|
-
tag: string; // BCP-47
|
|
77
|
-
label: string;
|
|
78
|
-
direction?: 'ltr' | 'rtl';
|
|
79
|
-
fallback?: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface TranslationCallOptions {
|
|
83
|
-
/** Explicit syntax for an inline default before per-key catalog metadata loads. */
|
|
84
|
-
format?: MessageFormat;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
type TranslateFn = (
|
|
88
|
-
key: string,
|
|
89
|
-
defaultValue?: string,
|
|
90
|
-
params?: Record<string, unknown>,
|
|
91
|
-
options?: TranslationCallOptions
|
|
92
|
-
) => string;
|
|
93
|
-
|
|
94
|
-
interface TranslationContextValue {
|
|
95
|
-
t: TranslateFn;
|
|
96
|
-
/** `t` over no catalog: exactly what the server rendered. */
|
|
97
|
-
tInline: TranslateFn;
|
|
98
|
-
language: string;
|
|
99
|
-
setLanguage: (tag: string) => void;
|
|
100
|
-
languages: TranslationLanguage[];
|
|
101
|
-
dir: 'ltr' | 'rtl';
|
|
102
|
-
}
|
|
103
|
-
|
|
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
|
-
}));
|
|
109
|
-
|
|
110
|
-
export interface TranslationProviderProps {
|
|
111
|
-
children: React.ReactNode;
|
|
112
|
-
languages: TranslationLanguage[];
|
|
113
|
-
/** Optional live catalog metadata, refreshed independently of an app build. */
|
|
114
|
-
loadLanguages?: () => Promise<TranslationLanguageDefinition[]>;
|
|
115
|
-
defaultLanguage?: string;
|
|
116
|
-
/** Global message format. 'simple' = FormatSimple (Tolgee-compatible default). */
|
|
117
|
-
format?: MessageFormat;
|
|
118
|
-
/** Caller-injected loader: dev→API, prod→bundled file then CDN overlay. */
|
|
119
|
-
loadMessages: (language: string) => Promise<TranslationMessages>;
|
|
120
|
-
/** localStorage key persisting the selected language. */
|
|
121
|
-
storageKey?: string;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function TranslationProvider({
|
|
125
|
-
children,
|
|
126
|
-
languages: initialLanguages,
|
|
127
|
-
loadLanguages,
|
|
128
|
-
defaultLanguage = 'en',
|
|
129
|
-
format = 'simple',
|
|
130
|
-
loadMessages,
|
|
131
|
-
storageKey = 'linked.lang',
|
|
132
|
-
}: TranslationProviderProps) {
|
|
133
|
-
const [remoteLanguages, setRemoteLanguages] =
|
|
134
|
-
useState<TranslationLanguageDefinition[]>();
|
|
135
|
-
const languages = remoteLanguages
|
|
136
|
-
? remoteLanguages
|
|
137
|
-
.filter((item) => item.enabled && item.supported)
|
|
138
|
-
.map((item) => ({
|
|
139
|
-
tag: item.code,
|
|
140
|
-
label: item.nativeName,
|
|
141
|
-
direction: item.direction,
|
|
142
|
-
fallback: item.fallback,
|
|
143
|
-
}))
|
|
144
|
-
: initialLanguages;
|
|
145
|
-
const [language, setLanguageState] = useState<string>(() => {
|
|
146
|
-
if (typeof window !== 'undefined') {
|
|
147
|
-
const stored = window.localStorage?.getItem(storageKey);
|
|
148
|
-
if (stored && languages.some((l) => l.tag === stored)) return stored;
|
|
149
|
-
}
|
|
150
|
-
return defaultLanguage;
|
|
151
|
-
});
|
|
152
|
-
const [previewLanguage, setPreviewLanguage] = useState<string | null>(null);
|
|
153
|
-
const [previewPseudoLocale, setPreviewPseudoLocale] =
|
|
154
|
-
useState<TranslationPreviewPseudoLocale>('off');
|
|
155
|
-
const [previewDirection, setPreviewDirection] =
|
|
156
|
-
useState<TranslationPreviewDirection>('auto');
|
|
157
|
-
const effectiveLanguage = previewLanguage ?? language;
|
|
158
|
-
// A preview direction is an editor coordinate and wins; otherwise the
|
|
159
|
-
// configured language's own direction, then the tag's script default.
|
|
160
|
-
const dir =
|
|
161
|
-
previewDirection === 'ltr' || previewDirection === 'rtl'
|
|
162
|
-
? previewDirection
|
|
163
|
-
: (languages.find((item) => item.tag === effectiveLanguage)?.direction ??
|
|
164
|
-
directionFor(effectiveLanguage));
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (!loadLanguages) return;
|
|
167
|
-
let cancelled = false;
|
|
168
|
-
loadLanguages()
|
|
169
|
-
.then((items) => {
|
|
170
|
-
if (!cancelled) {
|
|
171
|
-
setRemoteLanguages(items);
|
|
172
|
-
setMessagesByLang({});
|
|
173
|
-
const stored =
|
|
174
|
-
typeof window !== 'undefined'
|
|
175
|
-
? window.localStorage?.getItem(storageKey)
|
|
176
|
-
: null;
|
|
177
|
-
if (
|
|
178
|
-
stored &&
|
|
179
|
-
items.some(
|
|
180
|
-
(item) => item.code === stored && item.enabled && item.supported
|
|
181
|
-
)
|
|
182
|
-
)
|
|
183
|
-
setLanguageState(stored);
|
|
184
|
-
}
|
|
185
|
-
})
|
|
186
|
-
.catch(() => {});
|
|
187
|
-
return () => {
|
|
188
|
-
cancelled = true;
|
|
189
|
-
};
|
|
190
|
-
}, [loadLanguages, storageKey]);
|
|
191
|
-
const [messagesByLang, setMessagesByLang] = useState<
|
|
192
|
-
Record<string, TranslationMessages>
|
|
193
|
-
>({});
|
|
194
|
-
|
|
195
|
-
// Load the active language's messages once. English renders from inline
|
|
196
|
-
// defaults until (and if) a payload arrives, so the app is never blank.
|
|
197
|
-
useEffect(() => {
|
|
198
|
-
if (messagesByLang[effectiveLanguage]) return;
|
|
199
|
-
let cancelled = false;
|
|
200
|
-
const chain = languageFallbacks(
|
|
201
|
-
effectiveLanguage,
|
|
202
|
-
defaultLanguage,
|
|
203
|
-
remoteLanguages
|
|
204
|
-
).reverse();
|
|
205
|
-
Promise.all(chain.map((code) => loadMessages(code).catch(() => ({}))))
|
|
206
|
-
.then((maps) => Object.assign({}, ...maps) as TranslationMessages)
|
|
207
|
-
.then((msgs) => {
|
|
208
|
-
if (!cancelled) {
|
|
209
|
-
// A descendant lazy route may still be hydrating when the catalog
|
|
210
|
-
// arrives. Overlay messages without replacing its server-rendered
|
|
211
|
-
// DOM; inline defaults remain usable until the transition commits.
|
|
212
|
-
startTransition(() => {
|
|
213
|
-
setMessagesByLang((prev) => ({
|
|
214
|
-
...prev,
|
|
215
|
-
[effectiveLanguage]: msgs,
|
|
216
|
-
}));
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
.catch(() => {
|
|
221
|
-
/* offline / missing → keep inline defaults */
|
|
222
|
-
});
|
|
223
|
-
return () => {
|
|
224
|
-
cancelled = true;
|
|
225
|
-
};
|
|
226
|
-
}, [
|
|
227
|
-
effectiveLanguage,
|
|
228
|
-
loadMessages,
|
|
229
|
-
messagesByLang,
|
|
230
|
-
defaultLanguage,
|
|
231
|
-
remoteLanguages,
|
|
232
|
-
]);
|
|
233
|
-
|
|
234
|
-
// Preview locale is an ephemeral editor coordinate. It intentionally does
|
|
235
|
-
// not call setLanguage(), because doing so would persist a test condition as
|
|
236
|
-
// the app user's real language preference.
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
if (typeof window === 'undefined') return;
|
|
239
|
-
const onPreview = (event: Event) => {
|
|
240
|
-
const detail = (event as CustomEvent<TranslationPreviewDetail | null>)
|
|
241
|
-
.detail;
|
|
242
|
-
const nextLanguage: unknown = detail?.language;
|
|
243
|
-
const nextPseudoLocale: unknown = detail?.pseudoLocale;
|
|
244
|
-
const nextDirection: unknown = detail?.direction;
|
|
245
|
-
setPreviewLanguage(
|
|
246
|
-
typeof nextLanguage === 'string' && nextLanguage ? nextLanguage : null
|
|
247
|
-
);
|
|
248
|
-
setPreviewPseudoLocale(
|
|
249
|
-
nextPseudoLocale === 'accented' || nextPseudoLocale === 'expanded'
|
|
250
|
-
? nextPseudoLocale
|
|
251
|
-
: 'off'
|
|
252
|
-
);
|
|
253
|
-
setPreviewDirection(
|
|
254
|
-
nextDirection === 'ltr' || nextDirection === 'rtl'
|
|
255
|
-
? nextDirection
|
|
256
|
-
: 'auto'
|
|
257
|
-
);
|
|
258
|
-
};
|
|
259
|
-
window.addEventListener(TRANSLATION_PREVIEW_EVENT, onPreview);
|
|
260
|
-
return () =>
|
|
261
|
-
window.removeEventListener(TRANSLATION_PREVIEW_EVENT, onPreview);
|
|
262
|
-
}, []);
|
|
263
|
-
|
|
264
|
-
// Reflect the effective language and text direction on <html>.
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
if (typeof document !== 'undefined') {
|
|
267
|
-
document.documentElement.dir = dir;
|
|
268
|
-
document.documentElement.lang = effectiveLanguage;
|
|
269
|
-
}
|
|
270
|
-
}, [effectiveLanguage, dir]);
|
|
271
|
-
|
|
272
|
-
const setLanguage = useCallback(
|
|
273
|
-
(tag: string) => {
|
|
274
|
-
setLanguageState(tag);
|
|
275
|
-
if (typeof window !== 'undefined') {
|
|
276
|
-
window.localStorage?.setItem(storageKey, tag);
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
[storageKey]
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
const translatorFor = useCallback(
|
|
283
|
-
(catalogs: Record<string, TranslationMessages>): TranslateFn =>
|
|
284
|
-
(key, defaultValue, params, options) => {
|
|
285
|
-
const translated = translate(
|
|
286
|
-
catalogs[effectiveLanguage] ?? {},
|
|
287
|
-
key,
|
|
288
|
-
defaultValue,
|
|
289
|
-
params,
|
|
290
|
-
options?.format ?? format,
|
|
291
|
-
effectiveLanguage
|
|
292
|
-
);
|
|
293
|
-
if (previewPseudoLocale === 'accented') {
|
|
294
|
-
return effectiveLanguage.toLowerCase() === 'en-xa'
|
|
295
|
-
? translated
|
|
296
|
-
: pseudoLocalize(translated);
|
|
297
|
-
}
|
|
298
|
-
return previewPseudoLocale === 'expanded'
|
|
299
|
-
? pseudoExpand(translated)
|
|
300
|
-
: translated;
|
|
301
|
-
},
|
|
302
|
-
[effectiveLanguage, format, previewPseudoLocale]
|
|
303
|
-
);
|
|
304
|
-
const t = useMemo(
|
|
305
|
-
() => translatorFor(messagesByLang),
|
|
306
|
-
[translatorFor, messagesByLang]
|
|
307
|
-
);
|
|
308
|
-
const tInline = useMemo(() => translatorFor(EMPTY_MESSAGES), [translatorFor]);
|
|
309
|
-
|
|
310
|
-
const value = useMemo<TranslationContextValue>(
|
|
311
|
-
() => ({
|
|
312
|
-
t,
|
|
313
|
-
tInline,
|
|
314
|
-
language: effectiveLanguage,
|
|
315
|
-
setLanguage,
|
|
316
|
-
languages,
|
|
317
|
-
dir,
|
|
318
|
-
}),
|
|
319
|
-
[t, tInline, effectiveLanguage, setLanguage, languages, dir]
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
return (
|
|
323
|
-
<TranslationContext.Provider value={value}>
|
|
324
|
-
{children}
|
|
325
|
-
</TranslationContext.Provider>
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function useTranslationContext(hook: string): TranslationContextValue {
|
|
330
|
-
const ctx = useContext(TranslationContext);
|
|
331
|
-
if (!ctx)
|
|
332
|
-
throw new Error(`${hook} must be used within a <TranslationProvider>`);
|
|
333
|
-
return ctx;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** Tolgee-compatible: `const { t } = useTranslate()`. */
|
|
337
|
-
export function useTranslate() {
|
|
338
|
-
const ctx = useTranslationContext('useTranslate');
|
|
339
|
-
// The server never has a catalog, and a lazy descendant can hydrate after
|
|
340
|
-
// the catalog has already reached context. Gating per consumer means every
|
|
341
|
-
// hydrating render replays the inline defaults, then re-renders with the
|
|
342
|
-
// catalog once that consumer commits.
|
|
343
|
-
const hydrated = useSyncExternalStore(
|
|
344
|
-
subscribeNever,
|
|
345
|
-
() => true,
|
|
346
|
-
() => false
|
|
347
|
-
);
|
|
348
|
-
return { t: hydrated ? ctx.t : ctx.tInline };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const subscribeNever = () => () => {};
|
|
352
|
-
|
|
353
|
-
/** Active language + switcher + direction + the available languages. */
|
|
354
|
-
export function useLanguage() {
|
|
355
|
-
const ctx = useTranslationContext('useLanguage');
|
|
356
|
-
return {
|
|
357
|
-
language: ctx.language,
|
|
358
|
-
setLanguage: ctx.setLanguage,
|
|
359
|
-
languages: ctx.languages,
|
|
360
|
-
dir: ctx.dir,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/** JSX convenience (Tolgee `<T keyName=… />`). */
|
|
365
|
-
export function T({
|
|
366
|
-
keyName,
|
|
367
|
-
defaultValue,
|
|
368
|
-
params,
|
|
369
|
-
format,
|
|
370
|
-
}: {
|
|
371
|
-
keyName: string;
|
|
372
|
-
defaultValue?: string;
|
|
373
|
-
params?: Record<string, unknown>;
|
|
374
|
-
format?: MessageFormat;
|
|
375
|
-
}) {
|
|
376
|
-
const { t } = useTranslate();
|
|
377
|
-
return <>{t(keyName, defaultValue, params, { format })}</>;
|
|
378
|
-
}
|