@nckdv/translation-sdk 0.1.0 → 0.2.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/dist/index.cjs +53 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +53 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -6,12 +6,22 @@ var translationCore = require('@nckdv/translation-core');
|
|
|
6
6
|
|
|
7
7
|
// src/types.ts
|
|
8
8
|
var TranslationApiError = class extends Error {
|
|
9
|
-
constructor(message, status) {
|
|
9
|
+
constructor(message, status, body) {
|
|
10
10
|
super(message);
|
|
11
11
|
this.status = status;
|
|
12
|
+
this.body = body;
|
|
12
13
|
this.name = "TranslationApiError";
|
|
13
14
|
}
|
|
14
15
|
status;
|
|
16
|
+
body;
|
|
17
|
+
};
|
|
18
|
+
var CatalogPushConflictError = class extends TranslationApiError {
|
|
19
|
+
constructor(message, conflicts, body) {
|
|
20
|
+
super(message, 409, body);
|
|
21
|
+
this.conflicts = conflicts;
|
|
22
|
+
this.name = "CatalogPushConflictError";
|
|
23
|
+
}
|
|
24
|
+
conflicts;
|
|
15
25
|
};
|
|
16
26
|
|
|
17
27
|
// src/client.ts
|
|
@@ -31,19 +41,38 @@ function createClient(options) {
|
|
|
31
41
|
const base = apiUrl.replace(/\/+$/, "");
|
|
32
42
|
const cache = /* @__PURE__ */ new Map();
|
|
33
43
|
const inFlight = /* @__PURE__ */ new Map();
|
|
34
|
-
async function request(path) {
|
|
44
|
+
async function request(path, options2 = {}) {
|
|
35
45
|
const init = {
|
|
36
|
-
|
|
46
|
+
...options2,
|
|
47
|
+
headers: {
|
|
48
|
+
...options2.headers,
|
|
49
|
+
"x-api-key": apiKey
|
|
50
|
+
},
|
|
37
51
|
cache: "no-store"
|
|
38
52
|
};
|
|
39
53
|
const response = await fetchImpl(`${base}${path}`, init);
|
|
54
|
+
let body;
|
|
55
|
+
try {
|
|
56
|
+
body = await response.json();
|
|
57
|
+
} catch {
|
|
58
|
+
body = void 0;
|
|
59
|
+
}
|
|
40
60
|
if (!response.ok) {
|
|
61
|
+
const errorMessage = body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : void 0;
|
|
62
|
+
if (response.status === 409 && body && typeof body === "object" && "conflicts" in body && Array.isArray(body.conflicts) && body.conflicts.every((key) => typeof key === "string")) {
|
|
63
|
+
throw new CatalogPushConflictError(
|
|
64
|
+
errorMessage ?? "Catalog conflicts with existing values.",
|
|
65
|
+
body.conflicts,
|
|
66
|
+
body
|
|
67
|
+
);
|
|
68
|
+
}
|
|
41
69
|
throw new TranslationApiError(
|
|
42
|
-
response.status === 401 ? "Invalid or revoked API key." : `Translation API returned ${response.status}.`,
|
|
43
|
-
response.status
|
|
70
|
+
response.status === 401 ? "Invalid or revoked API key." : errorMessage ?? `Translation API returned ${response.status}.`,
|
|
71
|
+
response.status,
|
|
72
|
+
body
|
|
44
73
|
);
|
|
45
74
|
}
|
|
46
|
-
return
|
|
75
|
+
return body;
|
|
47
76
|
}
|
|
48
77
|
function fresh(language) {
|
|
49
78
|
const entry = cache.get(language);
|
|
@@ -91,13 +120,30 @@ function createClient(options) {
|
|
|
91
120
|
}
|
|
92
121
|
await Promise.all(languages.map((language) => getCatalog(language)));
|
|
93
122
|
}
|
|
123
|
+
async function push(language, catalog, options2 = {}) {
|
|
124
|
+
const parameters = new URLSearchParams({
|
|
125
|
+
mode: options2.mode ?? "no-force"
|
|
126
|
+
});
|
|
127
|
+
if (options2.force) parameters.set("force", "true");
|
|
128
|
+
const result = await request(
|
|
129
|
+
`/api/catalog/${encodeURIComponent(language)}?${parameters}`,
|
|
130
|
+
{
|
|
131
|
+
method: "PUT",
|
|
132
|
+
headers: { "content-type": "application/json" },
|
|
133
|
+
body: JSON.stringify(catalog)
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
invalidate(language);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
94
139
|
function invalidate(language) {
|
|
95
140
|
if (language) cache.delete(language);
|
|
96
141
|
else cache.clear();
|
|
97
142
|
}
|
|
98
|
-
return { getCatalog, getCatalogs, getTranslator, prefetch, invalidate };
|
|
143
|
+
return { getCatalog, getCatalogs, getTranslator, prefetch, push, invalidate };
|
|
99
144
|
}
|
|
100
145
|
|
|
146
|
+
exports.CatalogPushConflictError = CatalogPushConflictError;
|
|
101
147
|
exports.TranslationApiError = TranslationApiError;
|
|
102
148
|
exports.createClient = createClient;
|
|
103
149
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/client.ts"],"names":["createTranslator"],"mappings":";;;;;;;AA2CO,IAAM,mBAAA,GAAN,cAAkC,KAAA,CAAM;AAAA,EAC7C,WAAA,CACE,SACS,MAAA,EACT;AACA,IAAA,KAAA,CAAM,OAAO,CAAA;AAFJ,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAGT,IAAA,IAAA,CAAK,IAAA,GAAO,qBAAA;AAAA,EACd;AAAA,EAJW,MAAA;AAKb;;;ACzCA,IAAM,gBAAA,GAAmB,IAAI,EAAA,GAAK,GAAA;AAe3B,SAAS,aAAa,OAAA,EAAgC;AAC3D,EAAA,MAAM;AAAA,IACJ,MAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA,GAAU,gBAAA;AAAA,IACV,KAAA,EAAO,YAAY,UAAA,CAAW;AAAA,GAChC,GAAI,OAAA;AAEJ,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,MAAM,mCAAmC,CAAA;AAChE,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,MAAM,mCAAmC,CAAA;AAChE,EAAA,IAAI,OAAO,cAAc,UAAA,EAAY;AACnC,IAAA,MAAM,IAAI,MAAM,wDAAwD,CAAA;AAAA,EAC1E;AAEA,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACtC,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAmB;AAUrC,EAAA,MAAM,QAAA,uBAAe,GAAA,EAA8B;AAEnD,EAAA,eAAe,QAAW,IAAA,EAA0B;AAMlD,IAAA,MAAM,IAAA,GAAyC;AAAA,MAC7C,OAAA,EAAS,EAAE,WAAA,EAAa,MAAA,EAAO;AAAA,MAC/B,KAAA,EAAO;AAAA,KACT;AAEA,IAAA,MAAM,QAAA,GAAW,MAAM,SAAA,CAAU,CAAA,EAAG,IAAI,CAAA,EAAG,IAAI,IAAI,IAAI,CAAA;AAEvD,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,IAAI,mBAAA;AAAA,QACR,SAAS,MAAA,KAAW,GAAA,GAChB,6BAAA,GACA,CAAA,yBAAA,EAA4B,SAAS,MAAM,CAAA,CAAA,CAAA;AAAA,QAC/C,QAAA,CAAS;AAAA,OACX;AAAA,IACF;AACA,IAAA,OAAQ,MAAM,SAAS,IAAA,EAAK;AAAA,EAC9B;AAEA,EAAA,SAAS,MAAM,QAAA,EAAuC;AACpD,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,GAAA,CAAI,QAAQ,CAAA;AAChC,IAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,IAAA,IAAI,KAAA,CAAM,SAAA,IAAa,IAAA,CAAK,GAAA,EAAI,EAAG;AACjC,MAAA,KAAA,CAAM,OAAO,QAAQ,CAAA;AACrB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,OAAO,KAAA,CAAM,OAAA;AAAA,EACf;AAEA,EAAA,SAAS,KAAA,CAAM,UAAkB,OAAA,EAAkB;AAGjD,IAAA,IAAI,UAAU,CAAA,EAAG;AACf,MAAA,KAAA,CAAM,GAAA,CAAI,UAAU,EAAE,OAAA,EAAS,WAAW,IAAA,CAAK,GAAA,EAAI,GAAI,OAAA,EAAS,CAAA;AAAA,IAClE;AAAA,EACF;AAEA,EAAA,eAAe,WAAW,QAAA,EAAoC;AAC5D,IAAA,MAAM,MAAA,GAAS,MAAM,QAAQ,CAAA;AAC7B,IAAA,IAAI,QAAQ,OAAO,MAAA;AAEnB,IAAA,MAAM,QAAA,GAAW,QAAA,CAAS,GAAA,CAAI,QAAQ,CAAA;AACtC,IAAA,IAAI,UAAU,OAAO,QAAA;AAErB,IAAA,MAAM,OAAA,GAAU,OAAA;AAAA,MACd,CAAA,aAAA,EAAgB,kBAAA,CAAmB,QAAQ,CAAC,CAAA;AAAA,KAC9C,CACG,IAAA,CAAK,CAAC,OAAA,KAAY;AACjB,MAAA,KAAA,CAAM,UAAU,OAAO,CAAA;AACvB,MAAA,OAAO,OAAA;AAAA,IACT,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM,QAAA,CAAS,MAAA,CAAO,QAAQ,CAAC,CAAA;AAE1C,IAAA,QAAA,CAAS,GAAA,CAAI,UAAU,OAAO,CAAA;AAC9B,IAAA,OAAO,OAAA;AAAA,EACT;AAEA,EAAA,eAAe,WAAA,GAAgD;AAC7D,IAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAiC,cAAc,CAAA;AACtE,IAAA,KAAA,MAAW,CAAC,QAAA,EAAU,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAC1D,MAAA,KAAA,CAAM,UAAU,OAAO,CAAA;AAAA,IACzB;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,eAAe,cAAc,QAAA,EAAsC;AACjE,IAAA,MAAM,QAAA,GAAW,MAAM,UAAA,CAAW,QAAQ,CAAA;AAC1C,IAAA,OAAOA,gCAAA,CAAiB,EAAE,QAAA,EAAU,QAAA,EAAU,CAAA;AAAA,EAChD;AAEA,EAAA,eAAe,SAAS,SAAA,EAAqC;AAC3D,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,MAAM,WAAA,EAAY;AAClB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,OAAA,CAAQ,IAAI,SAAA,CAAU,GAAA,CAAI,CAAC,QAAA,KAAa,UAAA,CAAW,QAAQ,CAAC,CAAC,CAAA;AAAA,EACrE;AAEA,EAAA,SAAS,WAAW,QAAA,EAAyB;AAC3C,IAAA,IAAI,QAAA,EAAU,KAAA,CAAM,MAAA,CAAO,QAAQ,CAAA;AAAA,eACxB,KAAA,EAAM;AAAA,EACnB;AAEA,EAAA,OAAO,EAAE,UAAA,EAAY,WAAA,EAAa,aAAA,EAAe,UAAU,UAAA,EAAW;AACxE","file":"index.cjs","sourcesContent":["import type { InterpolationValues } from \"@nckdv/translation-core\";\n\n/** A flat catalog for one language, exactly as the API serves it. */\nexport type Catalog = Record<string, string>;\n\nexport type ClientOptions = {\n /** Where the platform is served from, e.g. `https://translate.example.com`. */\n apiUrl: string;\n /**\n * A project API key. It names its own project, so nothing else identifies\n * the project — that is the whole point of the credential.\n *\n * **Server-side only.** A key is a bearer credential for every catalog in\n * the project; shipping one to a browser publishes all of them.\n */\n apiKey: string;\n /**\n * How long a fetched catalog is reused, in milliseconds. Default 5 minutes.\n *\n * `0` disables caching, which is what a long-lived editor preview wants and\n * what a request-scoped server render does not.\n */\n cacheMs?: number;\n /** Swappable for tests and for runtimes with their own fetch. */\n fetch?: typeof globalThis.fetch;\n};\n\nexport type Translate = (key: string, values?: InterpolationValues) => string;\n\nexport type Client = {\n /** One language's catalog, from cache when it is still fresh. */\n getCatalog(language: string): Promise<Catalog>;\n /** Every language the project has, in one request. */\n getCatalogs(): Promise<Record<string, Catalog>>;\n /** A translate function bound to one language. */\n getTranslator(language: string): Promise<Translate>;\n /** Warms the cache — call at start-up so the first render does not wait. */\n prefetch(languages?: string[]): Promise<void>;\n /** Drops cached catalogs, so the next read goes to the server. */\n invalidate(language?: string): void;\n};\n\n/** Thrown for any answer that is not a catalog. */\nexport class TranslationApiError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n this.name = \"TranslationApiError\";\n }\n}\n","import { createTranslator } from \"@nckdv/translation-core\";\nimport {\n TranslationApiError,\n type Catalog,\n type Client,\n type ClientOptions,\n type Translate,\n} from \"./types.js\";\n\n/** Five minutes: long enough to matter, short enough that a fix lands soon. */\nconst DEFAULT_CACHE_MS = 5 * 60 * 1000;\n\ntype Entry = {\n catalog: Catalog;\n /** When this entry stops being reusable. */\n expiresAt: number;\n};\n\n/**\n * A client for one project's translations.\n *\n * The key identifies the project, so nothing here takes a project id — a\n * deployment holds one credential rather than a credential and a uuid it\n * cannot verify by eye.\n */\nexport function createClient(options: ClientOptions): Client {\n const {\n apiUrl,\n apiKey,\n cacheMs = DEFAULT_CACHE_MS,\n fetch: fetchImpl = globalThis.fetch,\n } = options;\n\n if (!apiUrl) throw new Error(\"createClient: apiUrl is required.\");\n if (!apiKey) throw new Error(\"createClient: apiKey is required.\");\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\"createClient: no fetch available; pass one in options.\");\n }\n\n const base = apiUrl.replace(/\\/+$/, \"\");\n const cache = new Map<string, Entry>();\n\n /**\n * In-flight requests, keyed the same way as the cache.\n *\n * Without this, a cold start that renders ten pages at once makes ten\n * identical requests for the same catalog. They are shared instead, and the\n * entry is removed as soon as it settles so a failure is retried rather than\n * remembered.\n */\n const inFlight = new Map<string, Promise<Catalog>>();\n\n async function request<T>(path: string): Promise<T> {\n // `cache` is not in Node's own `RequestInit`, but Next.js patches global\n // fetch and caches by default — and a framework quietly holding on to a\n // credentialled response is a correctness bug, not an optimisation. So the\n // field is set through a widened type rather than dropped: this client's\n // own cache is the one with the policy.\n const init: RequestInit & { cache?: string } = {\n headers: { \"x-api-key\": apiKey },\n cache: \"no-store\",\n };\n\n const response = await fetchImpl(`${base}${path}`, init);\n\n if (!response.ok) {\n throw new TranslationApiError(\n response.status === 401\n ? \"Invalid or revoked API key.\"\n : `Translation API returned ${response.status}.`,\n response.status,\n );\n }\n return (await response.json()) as T;\n }\n\n function fresh(language: string): Catalog | undefined {\n const entry = cache.get(language);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n cache.delete(language);\n return undefined;\n }\n return entry.catalog;\n }\n\n function store(language: string, catalog: Catalog) {\n // A zero TTL means \"do not cache\" rather than \"expire immediately\", so the\n // entry is simply never written.\n if (cacheMs > 0) {\n cache.set(language, { catalog, expiresAt: Date.now() + cacheMs });\n }\n }\n\n async function getCatalog(language: string): Promise<Catalog> {\n const cached = fresh(language);\n if (cached) return cached;\n\n const existing = inFlight.get(language);\n if (existing) return existing;\n\n const pending = request<Catalog>(\n `/api/catalog/${encodeURIComponent(language)}`,\n )\n .then((catalog) => {\n store(language, catalog);\n return catalog;\n })\n .finally(() => inFlight.delete(language));\n\n inFlight.set(language, pending);\n return pending;\n }\n\n async function getCatalogs(): Promise<Record<string, Catalog>> {\n const catalogs = await request<Record<string, Catalog>>(\"/api/catalog\");\n for (const [language, catalog] of Object.entries(catalogs)) {\n store(language, catalog);\n }\n return catalogs;\n }\n\n async function getTranslator(language: string): Promise<Translate> {\n const messages = await getCatalog(language);\n return createTranslator({ language, messages });\n }\n\n async function prefetch(languages?: string[]): Promise<void> {\n if (!languages) {\n await getCatalogs();\n return;\n }\n await Promise.all(languages.map((language) => getCatalog(language)));\n }\n\n function invalidate(language?: string): void {\n if (language) cache.delete(language);\n else cache.clear();\n }\n\n return { getCatalog, getCatalogs, getTranslator, prefetch, invalidate };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/client.ts"],"names":["options","createTranslator"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { InterpolationValues } from \"@nckdv/translation-core\";\n\n/** A flat catalog for one language, exactly as the API serves it. */\nexport type Catalog = Record<string, string>;\n\nexport type ClientOptions = {\n /** Where the platform is served from, e.g. `https://translate.example.com`. */\n apiUrl: string;\n /**\n * A project API key. It names its own project, so nothing else identifies\n * the project — that is the whole point of the credential.\n *\n * **Server-side only.** A key is a bearer credential for every catalog in\n * the project; shipping one to a browser publishes all of them.\n */\n apiKey: string;\n /**\n * How long a fetched catalog is reused, in milliseconds. Default 5 minutes.\n *\n * `0` disables caching, which is what a long-lived editor preview wants and\n * what a request-scoped server render does not.\n */\n cacheMs?: number;\n /** Swappable for tests and for runtimes with their own fetch. */\n fetch?: typeof globalThis.fetch;\n};\n\nexport type Translate = (key: string, values?: InterpolationValues) => string;\n\nexport type CatalogPushMode = \"no-force\" | \"override\" | \"keep\";\n\nexport type CatalogPushOptions = {\n /** Existing-value policy. Defaults to the atomic `no-force` mode. */\n mode?: CatalogPushMode;\n /** Required before a non-source language can be written. */\n force?: boolean;\n};\n\nexport type CatalogPushResult = {\n created: number;\n updated: number;\n unchanged: number;\n skipped: number;\n /** Existing target translations newly marked as needing review. */\n outdated: number;\n};\n\nexport type Client = {\n /** One language's catalog, from cache when it is still fresh. */\n getCatalog(language: string): Promise<Catalog>;\n /** Every language the project has, in one request. */\n getCatalogs(): Promise<Record<string, Catalog>>;\n /** A translate function bound to one language. */\n getTranslator(language: string): Promise<Translate>;\n /** Warms the cache — call at start-up so the first render does not wait. */\n prefetch(languages?: string[]): Promise<void>;\n /** Atomically merges a local catalog into one server language. */\n push(\n language: string,\n catalog: Catalog,\n options?: CatalogPushOptions,\n ): Promise<CatalogPushResult>;\n /** Drops cached catalogs, so the next read goes to the server. */\n invalidate(language?: string): void;\n};\n\n/** Thrown for any answer that is not a catalog. */\nexport class TranslationApiError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly body?: unknown,\n ) {\n super(message);\n this.name = \"TranslationApiError\";\n }\n}\n\n/** A no-force push found existing keys with different values. */\nexport class CatalogPushConflictError extends TranslationApiError {\n constructor(\n message: string,\n readonly conflicts: string[],\n body?: unknown,\n ) {\n super(message, 409, body);\n this.name = \"CatalogPushConflictError\";\n }\n}\n","import { createTranslator } from \"@nckdv/translation-core\";\nimport {\n CatalogPushConflictError,\n TranslationApiError,\n type Catalog,\n type CatalogPushOptions,\n type CatalogPushResult,\n type Client,\n type ClientOptions,\n type Translate,\n} from \"./types.js\";\n\n/** Five minutes: long enough to matter, short enough that a fix lands soon. */\nconst DEFAULT_CACHE_MS = 5 * 60 * 1000;\n\ntype Entry = {\n catalog: Catalog;\n /** When this entry stops being reusable. */\n expiresAt: number;\n};\n\n/**\n * A client for one project's translations.\n *\n * The key identifies the project, so nothing here takes a project id — a\n * deployment holds one credential rather than a credential and a uuid it\n * cannot verify by eye.\n */\nexport function createClient(options: ClientOptions): Client {\n const {\n apiUrl,\n apiKey,\n cacheMs = DEFAULT_CACHE_MS,\n fetch: fetchImpl = globalThis.fetch,\n } = options;\n\n if (!apiUrl) throw new Error(\"createClient: apiUrl is required.\");\n if (!apiKey) throw new Error(\"createClient: apiKey is required.\");\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\"createClient: no fetch available; pass one in options.\");\n }\n\n const base = apiUrl.replace(/\\/+$/, \"\");\n const cache = new Map<string, Entry>();\n\n /**\n * In-flight requests, keyed the same way as the cache.\n *\n * Without this, a cold start that renders ten pages at once makes ten\n * identical requests for the same catalog. They are shared instead, and the\n * entry is removed as soon as it settles so a failure is retried rather than\n * remembered.\n */\n const inFlight = new Map<string, Promise<Catalog>>();\n\n async function request<T>(\n path: string,\n options: RequestInit = {},\n ): Promise<T> {\n // `cache` is not in Node's own `RequestInit`, but Next.js patches global\n // fetch and caches by default — and a framework quietly holding on to a\n // credentialled response is a correctness bug, not an optimisation. So the\n // field is set through a widened type rather than dropped: this client's\n // own cache is the one with the policy.\n const init: RequestInit & { cache?: string } = {\n ...options,\n headers: {\n ...(options.headers as Record<string, string> | undefined),\n \"x-api-key\": apiKey,\n },\n cache: \"no-store\",\n };\n\n const response = await fetchImpl(`${base}${path}`, init);\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = undefined;\n }\n\n if (!response.ok) {\n const errorMessage =\n body &&\n typeof body === \"object\" &&\n \"error\" in body &&\n typeof body.error === \"string\"\n ? body.error\n : undefined;\n if (\n response.status === 409 &&\n body &&\n typeof body === \"object\" &&\n \"conflicts\" in body &&\n Array.isArray(body.conflicts) &&\n body.conflicts.every((key) => typeof key === \"string\")\n ) {\n throw new CatalogPushConflictError(\n errorMessage ?? \"Catalog conflicts with existing values.\",\n body.conflicts,\n body,\n );\n }\n throw new TranslationApiError(\n response.status === 401\n ? \"Invalid or revoked API key.\"\n : (errorMessage ?? `Translation API returned ${response.status}.`),\n response.status,\n body,\n );\n }\n return body as T;\n }\n\n function fresh(language: string): Catalog | undefined {\n const entry = cache.get(language);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n cache.delete(language);\n return undefined;\n }\n return entry.catalog;\n }\n\n function store(language: string, catalog: Catalog) {\n // A zero TTL means \"do not cache\" rather than \"expire immediately\", so the\n // entry is simply never written.\n if (cacheMs > 0) {\n cache.set(language, { catalog, expiresAt: Date.now() + cacheMs });\n }\n }\n\n async function getCatalog(language: string): Promise<Catalog> {\n const cached = fresh(language);\n if (cached) return cached;\n\n const existing = inFlight.get(language);\n if (existing) return existing;\n\n const pending = request<Catalog>(\n `/api/catalog/${encodeURIComponent(language)}`,\n )\n .then((catalog) => {\n store(language, catalog);\n return catalog;\n })\n .finally(() => inFlight.delete(language));\n\n inFlight.set(language, pending);\n return pending;\n }\n\n async function getCatalogs(): Promise<Record<string, Catalog>> {\n const catalogs = await request<Record<string, Catalog>>(\"/api/catalog\");\n for (const [language, catalog] of Object.entries(catalogs)) {\n store(language, catalog);\n }\n return catalogs;\n }\n\n async function getTranslator(language: string): Promise<Translate> {\n const messages = await getCatalog(language);\n return createTranslator({ language, messages });\n }\n\n async function prefetch(languages?: string[]): Promise<void> {\n if (!languages) {\n await getCatalogs();\n return;\n }\n await Promise.all(languages.map((language) => getCatalog(language)));\n }\n\n async function push(\n language: string,\n catalog: Catalog,\n options: CatalogPushOptions = {},\n ): Promise<CatalogPushResult> {\n const parameters = new URLSearchParams({\n mode: options.mode ?? \"no-force\",\n });\n if (options.force) parameters.set(\"force\", \"true\");\n\n const result = await request<CatalogPushResult>(\n `/api/catalog/${encodeURIComponent(language)}?${parameters}`,\n {\n method: \"PUT\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(catalog),\n },\n );\n invalidate(language);\n return result;\n }\n\n function invalidate(language?: string): void {\n if (language) cache.delete(language);\n else cache.clear();\n }\n\n return { getCatalog, getCatalogs, getTranslator, prefetch, push, invalidate };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -24,6 +24,21 @@ type ClientOptions = {
|
|
|
24
24
|
fetch?: typeof globalThis.fetch;
|
|
25
25
|
};
|
|
26
26
|
type Translate = (key: string, values?: InterpolationValues) => string;
|
|
27
|
+
type CatalogPushMode = "no-force" | "override" | "keep";
|
|
28
|
+
type CatalogPushOptions = {
|
|
29
|
+
/** Existing-value policy. Defaults to the atomic `no-force` mode. */
|
|
30
|
+
mode?: CatalogPushMode;
|
|
31
|
+
/** Required before a non-source language can be written. */
|
|
32
|
+
force?: boolean;
|
|
33
|
+
};
|
|
34
|
+
type CatalogPushResult = {
|
|
35
|
+
created: number;
|
|
36
|
+
updated: number;
|
|
37
|
+
unchanged: number;
|
|
38
|
+
skipped: number;
|
|
39
|
+
/** Existing target translations newly marked as needing review. */
|
|
40
|
+
outdated: number;
|
|
41
|
+
};
|
|
27
42
|
type Client = {
|
|
28
43
|
/** One language's catalog, from cache when it is still fresh. */
|
|
29
44
|
getCatalog(language: string): Promise<Catalog>;
|
|
@@ -33,13 +48,21 @@ type Client = {
|
|
|
33
48
|
getTranslator(language: string): Promise<Translate>;
|
|
34
49
|
/** Warms the cache — call at start-up so the first render does not wait. */
|
|
35
50
|
prefetch(languages?: string[]): Promise<void>;
|
|
51
|
+
/** Atomically merges a local catalog into one server language. */
|
|
52
|
+
push(language: string, catalog: Catalog, options?: CatalogPushOptions): Promise<CatalogPushResult>;
|
|
36
53
|
/** Drops cached catalogs, so the next read goes to the server. */
|
|
37
54
|
invalidate(language?: string): void;
|
|
38
55
|
};
|
|
39
56
|
/** Thrown for any answer that is not a catalog. */
|
|
40
57
|
declare class TranslationApiError extends Error {
|
|
41
58
|
readonly status: number;
|
|
42
|
-
|
|
59
|
+
readonly body?: unknown | undefined;
|
|
60
|
+
constructor(message: string, status: number, body?: unknown | undefined);
|
|
61
|
+
}
|
|
62
|
+
/** A no-force push found existing keys with different values. */
|
|
63
|
+
declare class CatalogPushConflictError extends TranslationApiError {
|
|
64
|
+
readonly conflicts: string[];
|
|
65
|
+
constructor(message: string, conflicts: string[], body?: unknown);
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
/**
|
|
@@ -51,4 +74,4 @@ declare class TranslationApiError extends Error {
|
|
|
51
74
|
*/
|
|
52
75
|
declare function createClient(options: ClientOptions): Client;
|
|
53
76
|
|
|
54
|
-
export { type Catalog, type Client, type ClientOptions, type Translate, TranslationApiError, createClient };
|
|
77
|
+
export { type Catalog, CatalogPushConflictError, type CatalogPushMode, type CatalogPushOptions, type CatalogPushResult, type Client, type ClientOptions, type Translate, TranslationApiError, createClient };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,21 @@ type ClientOptions = {
|
|
|
24
24
|
fetch?: typeof globalThis.fetch;
|
|
25
25
|
};
|
|
26
26
|
type Translate = (key: string, values?: InterpolationValues) => string;
|
|
27
|
+
type CatalogPushMode = "no-force" | "override" | "keep";
|
|
28
|
+
type CatalogPushOptions = {
|
|
29
|
+
/** Existing-value policy. Defaults to the atomic `no-force` mode. */
|
|
30
|
+
mode?: CatalogPushMode;
|
|
31
|
+
/** Required before a non-source language can be written. */
|
|
32
|
+
force?: boolean;
|
|
33
|
+
};
|
|
34
|
+
type CatalogPushResult = {
|
|
35
|
+
created: number;
|
|
36
|
+
updated: number;
|
|
37
|
+
unchanged: number;
|
|
38
|
+
skipped: number;
|
|
39
|
+
/** Existing target translations newly marked as needing review. */
|
|
40
|
+
outdated: number;
|
|
41
|
+
};
|
|
27
42
|
type Client = {
|
|
28
43
|
/** One language's catalog, from cache when it is still fresh. */
|
|
29
44
|
getCatalog(language: string): Promise<Catalog>;
|
|
@@ -33,13 +48,21 @@ type Client = {
|
|
|
33
48
|
getTranslator(language: string): Promise<Translate>;
|
|
34
49
|
/** Warms the cache — call at start-up so the first render does not wait. */
|
|
35
50
|
prefetch(languages?: string[]): Promise<void>;
|
|
51
|
+
/** Atomically merges a local catalog into one server language. */
|
|
52
|
+
push(language: string, catalog: Catalog, options?: CatalogPushOptions): Promise<CatalogPushResult>;
|
|
36
53
|
/** Drops cached catalogs, so the next read goes to the server. */
|
|
37
54
|
invalidate(language?: string): void;
|
|
38
55
|
};
|
|
39
56
|
/** Thrown for any answer that is not a catalog. */
|
|
40
57
|
declare class TranslationApiError extends Error {
|
|
41
58
|
readonly status: number;
|
|
42
|
-
|
|
59
|
+
readonly body?: unknown | undefined;
|
|
60
|
+
constructor(message: string, status: number, body?: unknown | undefined);
|
|
61
|
+
}
|
|
62
|
+
/** A no-force push found existing keys with different values. */
|
|
63
|
+
declare class CatalogPushConflictError extends TranslationApiError {
|
|
64
|
+
readonly conflicts: string[];
|
|
65
|
+
constructor(message: string, conflicts: string[], body?: unknown);
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
/**
|
|
@@ -51,4 +74,4 @@ declare class TranslationApiError extends Error {
|
|
|
51
74
|
*/
|
|
52
75
|
declare function createClient(options: ClientOptions): Client;
|
|
53
76
|
|
|
54
|
-
export { type Catalog, type Client, type ClientOptions, type Translate, TranslationApiError, createClient };
|
|
77
|
+
export { type Catalog, CatalogPushConflictError, type CatalogPushMode, type CatalogPushOptions, type CatalogPushResult, type Client, type ClientOptions, type Translate, TranslationApiError, createClient };
|
package/dist/index.js
CHANGED
|
@@ -4,12 +4,22 @@ import { createTranslator } from '@nckdv/translation-core';
|
|
|
4
4
|
|
|
5
5
|
// src/types.ts
|
|
6
6
|
var TranslationApiError = class extends Error {
|
|
7
|
-
constructor(message, status) {
|
|
7
|
+
constructor(message, status, body) {
|
|
8
8
|
super(message);
|
|
9
9
|
this.status = status;
|
|
10
|
+
this.body = body;
|
|
10
11
|
this.name = "TranslationApiError";
|
|
11
12
|
}
|
|
12
13
|
status;
|
|
14
|
+
body;
|
|
15
|
+
};
|
|
16
|
+
var CatalogPushConflictError = class extends TranslationApiError {
|
|
17
|
+
constructor(message, conflicts, body) {
|
|
18
|
+
super(message, 409, body);
|
|
19
|
+
this.conflicts = conflicts;
|
|
20
|
+
this.name = "CatalogPushConflictError";
|
|
21
|
+
}
|
|
22
|
+
conflicts;
|
|
13
23
|
};
|
|
14
24
|
|
|
15
25
|
// src/client.ts
|
|
@@ -29,19 +39,38 @@ function createClient(options) {
|
|
|
29
39
|
const base = apiUrl.replace(/\/+$/, "");
|
|
30
40
|
const cache = /* @__PURE__ */ new Map();
|
|
31
41
|
const inFlight = /* @__PURE__ */ new Map();
|
|
32
|
-
async function request(path) {
|
|
42
|
+
async function request(path, options2 = {}) {
|
|
33
43
|
const init = {
|
|
34
|
-
|
|
44
|
+
...options2,
|
|
45
|
+
headers: {
|
|
46
|
+
...options2.headers,
|
|
47
|
+
"x-api-key": apiKey
|
|
48
|
+
},
|
|
35
49
|
cache: "no-store"
|
|
36
50
|
};
|
|
37
51
|
const response = await fetchImpl(`${base}${path}`, init);
|
|
52
|
+
let body;
|
|
53
|
+
try {
|
|
54
|
+
body = await response.json();
|
|
55
|
+
} catch {
|
|
56
|
+
body = void 0;
|
|
57
|
+
}
|
|
38
58
|
if (!response.ok) {
|
|
59
|
+
const errorMessage = body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : void 0;
|
|
60
|
+
if (response.status === 409 && body && typeof body === "object" && "conflicts" in body && Array.isArray(body.conflicts) && body.conflicts.every((key) => typeof key === "string")) {
|
|
61
|
+
throw new CatalogPushConflictError(
|
|
62
|
+
errorMessage ?? "Catalog conflicts with existing values.",
|
|
63
|
+
body.conflicts,
|
|
64
|
+
body
|
|
65
|
+
);
|
|
66
|
+
}
|
|
39
67
|
throw new TranslationApiError(
|
|
40
|
-
response.status === 401 ? "Invalid or revoked API key." : `Translation API returned ${response.status}.`,
|
|
41
|
-
response.status
|
|
68
|
+
response.status === 401 ? "Invalid or revoked API key." : errorMessage ?? `Translation API returned ${response.status}.`,
|
|
69
|
+
response.status,
|
|
70
|
+
body
|
|
42
71
|
);
|
|
43
72
|
}
|
|
44
|
-
return
|
|
73
|
+
return body;
|
|
45
74
|
}
|
|
46
75
|
function fresh(language) {
|
|
47
76
|
const entry = cache.get(language);
|
|
@@ -89,13 +118,29 @@ function createClient(options) {
|
|
|
89
118
|
}
|
|
90
119
|
await Promise.all(languages.map((language) => getCatalog(language)));
|
|
91
120
|
}
|
|
121
|
+
async function push(language, catalog, options2 = {}) {
|
|
122
|
+
const parameters = new URLSearchParams({
|
|
123
|
+
mode: options2.mode ?? "no-force"
|
|
124
|
+
});
|
|
125
|
+
if (options2.force) parameters.set("force", "true");
|
|
126
|
+
const result = await request(
|
|
127
|
+
`/api/catalog/${encodeURIComponent(language)}?${parameters}`,
|
|
128
|
+
{
|
|
129
|
+
method: "PUT",
|
|
130
|
+
headers: { "content-type": "application/json" },
|
|
131
|
+
body: JSON.stringify(catalog)
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
invalidate(language);
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
92
137
|
function invalidate(language) {
|
|
93
138
|
if (language) cache.delete(language);
|
|
94
139
|
else cache.clear();
|
|
95
140
|
}
|
|
96
|
-
return { getCatalog, getCatalogs, getTranslator, prefetch, invalidate };
|
|
141
|
+
return { getCatalog, getCatalogs, getTranslator, prefetch, push, invalidate };
|
|
97
142
|
}
|
|
98
143
|
|
|
99
|
-
export { TranslationApiError, createClient };
|
|
144
|
+
export { CatalogPushConflictError, TranslationApiError, createClient };
|
|
100
145
|
//# sourceMappingURL=index.js.map
|
|
101
146
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { InterpolationValues } from \"@nckdv/translation-core\";\n\n/** A flat catalog for one language, exactly as the API serves it. */\nexport type Catalog = Record<string, string>;\n\nexport type ClientOptions = {\n /** Where the platform is served from, e.g. `https://translate.example.com`. */\n apiUrl: string;\n /**\n * A project API key. It names its own project, so nothing else identifies\n * the project — that is the whole point of the credential.\n *\n * **Server-side only.** A key is a bearer credential for every catalog in\n * the project; shipping one to a browser publishes all of them.\n */\n apiKey: string;\n /**\n * How long a fetched catalog is reused, in milliseconds. Default 5 minutes.\n *\n * `0` disables caching, which is what a long-lived editor preview wants and\n * what a request-scoped server render does not.\n */\n cacheMs?: number;\n /** Swappable for tests and for runtimes with their own fetch. */\n fetch?: typeof globalThis.fetch;\n};\n\nexport type Translate = (key: string, values?: InterpolationValues) => string;\n\nexport type Client = {\n /** One language's catalog, from cache when it is still fresh. */\n getCatalog(language: string): Promise<Catalog>;\n /** Every language the project has, in one request. */\n getCatalogs(): Promise<Record<string, Catalog>>;\n /** A translate function bound to one language. */\n getTranslator(language: string): Promise<Translate>;\n /** Warms the cache — call at start-up so the first render does not wait. */\n prefetch(languages?: string[]): Promise<void>;\n /** Drops cached catalogs, so the next read goes to the server. */\n invalidate(language?: string): void;\n};\n\n/** Thrown for any answer that is not a catalog. */\nexport class TranslationApiError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n this.name = \"TranslationApiError\";\n }\n}\n","import { createTranslator } from \"@nckdv/translation-core\";\nimport {\n TranslationApiError,\n type Catalog,\n type Client,\n type ClientOptions,\n type Translate,\n} from \"./types.js\";\n\n/** Five minutes: long enough to matter, short enough that a fix lands soon. */\nconst DEFAULT_CACHE_MS = 5 * 60 * 1000;\n\ntype Entry = {\n catalog: Catalog;\n /** When this entry stops being reusable. */\n expiresAt: number;\n};\n\n/**\n * A client for one project's translations.\n *\n * The key identifies the project, so nothing here takes a project id — a\n * deployment holds one credential rather than a credential and a uuid it\n * cannot verify by eye.\n */\nexport function createClient(options: ClientOptions): Client {\n const {\n apiUrl,\n apiKey,\n cacheMs = DEFAULT_CACHE_MS,\n fetch: fetchImpl = globalThis.fetch,\n } = options;\n\n if (!apiUrl) throw new Error(\"createClient: apiUrl is required.\");\n if (!apiKey) throw new Error(\"createClient: apiKey is required.\");\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\"createClient: no fetch available; pass one in options.\");\n }\n\n const base = apiUrl.replace(/\\/+$/, \"\");\n const cache = new Map<string, Entry>();\n\n /**\n * In-flight requests, keyed the same way as the cache.\n *\n * Without this, a cold start that renders ten pages at once makes ten\n * identical requests for the same catalog. They are shared instead, and the\n * entry is removed as soon as it settles so a failure is retried rather than\n * remembered.\n */\n const inFlight = new Map<string, Promise<Catalog>>();\n\n async function request<T>(path: string): Promise<T> {\n // `cache` is not in Node's own `RequestInit`, but Next.js patches global\n // fetch and caches by default — and a framework quietly holding on to a\n // credentialled response is a correctness bug, not an optimisation. So the\n // field is set through a widened type rather than dropped: this client's\n // own cache is the one with the policy.\n const init: RequestInit & { cache?: string } = {\n headers: { \"x-api-key\": apiKey },\n cache: \"no-store\",\n };\n\n const response = await fetchImpl(`${base}${path}`, init);\n\n if (!response.ok) {\n throw new TranslationApiError(\n response.status === 401\n ? \"Invalid or revoked API key.\"\n : `Translation API returned ${response.status}.`,\n response.status,\n );\n }\n return (await response.json()) as T;\n }\n\n function fresh(language: string): Catalog | undefined {\n const entry = cache.get(language);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n cache.delete(language);\n return undefined;\n }\n return entry.catalog;\n }\n\n function store(language: string, catalog: Catalog) {\n // A zero TTL means \"do not cache\" rather than \"expire immediately\", so the\n // entry is simply never written.\n if (cacheMs > 0) {\n cache.set(language, { catalog, expiresAt: Date.now() + cacheMs });\n }\n }\n\n async function getCatalog(language: string): Promise<Catalog> {\n const cached = fresh(language);\n if (cached) return cached;\n\n const existing = inFlight.get(language);\n if (existing) return existing;\n\n const pending = request<Catalog>(\n `/api/catalog/${encodeURIComponent(language)}`,\n )\n .then((catalog) => {\n store(language, catalog);\n return catalog;\n })\n .finally(() => inFlight.delete(language));\n\n inFlight.set(language, pending);\n return pending;\n }\n\n async function getCatalogs(): Promise<Record<string, Catalog>> {\n const catalogs = await request<Record<string, Catalog>>(\"/api/catalog\");\n for (const [language, catalog] of Object.entries(catalogs)) {\n store(language, catalog);\n }\n return catalogs;\n }\n\n async function getTranslator(language: string): Promise<Translate> {\n const messages = await getCatalog(language);\n return createTranslator({ language, messages });\n }\n\n async function prefetch(languages?: string[]): Promise<void> {\n if (!languages) {\n await getCatalogs();\n return;\n }\n await Promise.all(languages.map((language) => getCatalog(language)));\n }\n\n function invalidate(language?: string): void {\n if (language) cache.delete(language);\n else cache.clear();\n }\n\n return { getCatalog, getCatalogs, getTranslator, prefetch, invalidate };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/client.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import type { InterpolationValues } from \"@nckdv/translation-core\";\n\n/** A flat catalog for one language, exactly as the API serves it. */\nexport type Catalog = Record<string, string>;\n\nexport type ClientOptions = {\n /** Where the platform is served from, e.g. `https://translate.example.com`. */\n apiUrl: string;\n /**\n * A project API key. It names its own project, so nothing else identifies\n * the project — that is the whole point of the credential.\n *\n * **Server-side only.** A key is a bearer credential for every catalog in\n * the project; shipping one to a browser publishes all of them.\n */\n apiKey: string;\n /**\n * How long a fetched catalog is reused, in milliseconds. Default 5 minutes.\n *\n * `0` disables caching, which is what a long-lived editor preview wants and\n * what a request-scoped server render does not.\n */\n cacheMs?: number;\n /** Swappable for tests and for runtimes with their own fetch. */\n fetch?: typeof globalThis.fetch;\n};\n\nexport type Translate = (key: string, values?: InterpolationValues) => string;\n\nexport type CatalogPushMode = \"no-force\" | \"override\" | \"keep\";\n\nexport type CatalogPushOptions = {\n /** Existing-value policy. Defaults to the atomic `no-force` mode. */\n mode?: CatalogPushMode;\n /** Required before a non-source language can be written. */\n force?: boolean;\n};\n\nexport type CatalogPushResult = {\n created: number;\n updated: number;\n unchanged: number;\n skipped: number;\n /** Existing target translations newly marked as needing review. */\n outdated: number;\n};\n\nexport type Client = {\n /** One language's catalog, from cache when it is still fresh. */\n getCatalog(language: string): Promise<Catalog>;\n /** Every language the project has, in one request. */\n getCatalogs(): Promise<Record<string, Catalog>>;\n /** A translate function bound to one language. */\n getTranslator(language: string): Promise<Translate>;\n /** Warms the cache — call at start-up so the first render does not wait. */\n prefetch(languages?: string[]): Promise<void>;\n /** Atomically merges a local catalog into one server language. */\n push(\n language: string,\n catalog: Catalog,\n options?: CatalogPushOptions,\n ): Promise<CatalogPushResult>;\n /** Drops cached catalogs, so the next read goes to the server. */\n invalidate(language?: string): void;\n};\n\n/** Thrown for any answer that is not a catalog. */\nexport class TranslationApiError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly body?: unknown,\n ) {\n super(message);\n this.name = \"TranslationApiError\";\n }\n}\n\n/** A no-force push found existing keys with different values. */\nexport class CatalogPushConflictError extends TranslationApiError {\n constructor(\n message: string,\n readonly conflicts: string[],\n body?: unknown,\n ) {\n super(message, 409, body);\n this.name = \"CatalogPushConflictError\";\n }\n}\n","import { createTranslator } from \"@nckdv/translation-core\";\nimport {\n CatalogPushConflictError,\n TranslationApiError,\n type Catalog,\n type CatalogPushOptions,\n type CatalogPushResult,\n type Client,\n type ClientOptions,\n type Translate,\n} from \"./types.js\";\n\n/** Five minutes: long enough to matter, short enough that a fix lands soon. */\nconst DEFAULT_CACHE_MS = 5 * 60 * 1000;\n\ntype Entry = {\n catalog: Catalog;\n /** When this entry stops being reusable. */\n expiresAt: number;\n};\n\n/**\n * A client for one project's translations.\n *\n * The key identifies the project, so nothing here takes a project id — a\n * deployment holds one credential rather than a credential and a uuid it\n * cannot verify by eye.\n */\nexport function createClient(options: ClientOptions): Client {\n const {\n apiUrl,\n apiKey,\n cacheMs = DEFAULT_CACHE_MS,\n fetch: fetchImpl = globalThis.fetch,\n } = options;\n\n if (!apiUrl) throw new Error(\"createClient: apiUrl is required.\");\n if (!apiKey) throw new Error(\"createClient: apiKey is required.\");\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\"createClient: no fetch available; pass one in options.\");\n }\n\n const base = apiUrl.replace(/\\/+$/, \"\");\n const cache = new Map<string, Entry>();\n\n /**\n * In-flight requests, keyed the same way as the cache.\n *\n * Without this, a cold start that renders ten pages at once makes ten\n * identical requests for the same catalog. They are shared instead, and the\n * entry is removed as soon as it settles so a failure is retried rather than\n * remembered.\n */\n const inFlight = new Map<string, Promise<Catalog>>();\n\n async function request<T>(\n path: string,\n options: RequestInit = {},\n ): Promise<T> {\n // `cache` is not in Node's own `RequestInit`, but Next.js patches global\n // fetch and caches by default — and a framework quietly holding on to a\n // credentialled response is a correctness bug, not an optimisation. So the\n // field is set through a widened type rather than dropped: this client's\n // own cache is the one with the policy.\n const init: RequestInit & { cache?: string } = {\n ...options,\n headers: {\n ...(options.headers as Record<string, string> | undefined),\n \"x-api-key\": apiKey,\n },\n cache: \"no-store\",\n };\n\n const response = await fetchImpl(`${base}${path}`, init);\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = undefined;\n }\n\n if (!response.ok) {\n const errorMessage =\n body &&\n typeof body === \"object\" &&\n \"error\" in body &&\n typeof body.error === \"string\"\n ? body.error\n : undefined;\n if (\n response.status === 409 &&\n body &&\n typeof body === \"object\" &&\n \"conflicts\" in body &&\n Array.isArray(body.conflicts) &&\n body.conflicts.every((key) => typeof key === \"string\")\n ) {\n throw new CatalogPushConflictError(\n errorMessage ?? \"Catalog conflicts with existing values.\",\n body.conflicts,\n body,\n );\n }\n throw new TranslationApiError(\n response.status === 401\n ? \"Invalid or revoked API key.\"\n : (errorMessage ?? `Translation API returned ${response.status}.`),\n response.status,\n body,\n );\n }\n return body as T;\n }\n\n function fresh(language: string): Catalog | undefined {\n const entry = cache.get(language);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n cache.delete(language);\n return undefined;\n }\n return entry.catalog;\n }\n\n function store(language: string, catalog: Catalog) {\n // A zero TTL means \"do not cache\" rather than \"expire immediately\", so the\n // entry is simply never written.\n if (cacheMs > 0) {\n cache.set(language, { catalog, expiresAt: Date.now() + cacheMs });\n }\n }\n\n async function getCatalog(language: string): Promise<Catalog> {\n const cached = fresh(language);\n if (cached) return cached;\n\n const existing = inFlight.get(language);\n if (existing) return existing;\n\n const pending = request<Catalog>(\n `/api/catalog/${encodeURIComponent(language)}`,\n )\n .then((catalog) => {\n store(language, catalog);\n return catalog;\n })\n .finally(() => inFlight.delete(language));\n\n inFlight.set(language, pending);\n return pending;\n }\n\n async function getCatalogs(): Promise<Record<string, Catalog>> {\n const catalogs = await request<Record<string, Catalog>>(\"/api/catalog\");\n for (const [language, catalog] of Object.entries(catalogs)) {\n store(language, catalog);\n }\n return catalogs;\n }\n\n async function getTranslator(language: string): Promise<Translate> {\n const messages = await getCatalog(language);\n return createTranslator({ language, messages });\n }\n\n async function prefetch(languages?: string[]): Promise<void> {\n if (!languages) {\n await getCatalogs();\n return;\n }\n await Promise.all(languages.map((language) => getCatalog(language)));\n }\n\n async function push(\n language: string,\n catalog: Catalog,\n options: CatalogPushOptions = {},\n ): Promise<CatalogPushResult> {\n const parameters = new URLSearchParams({\n mode: options.mode ?? \"no-force\",\n });\n if (options.force) parameters.set(\"force\", \"true\");\n\n const result = await request<CatalogPushResult>(\n `/api/catalog/${encodeURIComponent(language)}?${parameters}`,\n {\n method: \"PUT\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(catalog),\n },\n );\n invalidate(language);\n return result;\n }\n\n function invalidate(language?: string): void {\n if (language) cache.delete(language);\n else cache.clear();\n }\n\n return { getCatalog, getCatalogs, getTranslator, prefetch, push, invalidate };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nckdv/translation-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"typecheck": "tsc --noEmit"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@nckdv/translation-core": "^0.
|
|
24
|
+
"@nckdv/translation-core": "^0.2.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"tsup": "^8.3.5",
|