@_linked/translation 0.9.4 → 0.9.5

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.
Files changed (41) hide show
  1. package/package.json +2 -3
  2. package/src/archive.ts +0 -914
  3. package/src/authorization.ts +0 -56
  4. package/src/backend.ts +0 -2
  5. package/src/cdn-loader.ts +0 -264
  6. package/src/checks.ts +0 -413
  7. package/src/compile.ts +0 -168
  8. package/src/core/messages.ts +0 -139
  9. package/src/data/translation.ttl +0 -489
  10. package/src/discovery.ts +0 -594
  11. package/src/exchange.ts +0 -437
  12. package/src/export.ts +0 -138
  13. package/src/formats/csv.ts +0 -467
  14. package/src/formats/json.ts +0 -596
  15. package/src/formats/xliff.ts +0 -733
  16. package/src/import.ts +0 -648
  17. package/src/index.ts +0 -201
  18. package/src/key-sync-node.ts +0 -269
  19. package/src/key-sync.ts +0 -208
  20. package/src/key-version.ts +0 -207
  21. package/src/languages.ts +0 -102
  22. package/src/linked-discovery.ts +0 -385
  23. package/src/message-format.ts +0 -214
  24. package/src/mt.ts +0 -253
  25. package/src/ontologies/translation.register.ts +0 -17
  26. package/src/ontologies/translation.ts +0 -239
  27. package/src/package.ts +0 -14
  28. package/src/react.tsx +0 -378
  29. package/src/records.ts +0 -181
  30. package/src/release.ts +0 -754
  31. package/src/shape-catalog.ts +0 -198
  32. package/src/shapes/GlossaryTerm.ts +0 -61
  33. package/src/shapes/TranslationInventoryOrigin.ts +0 -131
  34. package/src/shapes/TranslationKey.ts +0 -183
  35. package/src/shapes/TranslationKeyVersion.ts +0 -77
  36. package/src/shapes/TranslationLanguage.ts +0 -51
  37. package/src/shapes/TranslationProvider.ts +0 -1927
  38. package/src/shapes/TranslationRelease.ts +0 -114
  39. package/src/shapes/TranslationRevision.ts +0 -105
  40. package/src/shapes/TranslationUnit.ts +0 -72
  41. 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
- }