@vahidkaargar/customized-api-client 0.3.0 → 0.4.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 CHANGED
@@ -229,7 +229,8 @@ Client-level options for `createApiClient({ … })`:
229
229
  | `defaultHeaders` | — | Merged into every request |
230
230
  | `retry` | see [Retries](#retries) | `maxAttempts`, backoff, jitter |
231
231
  | `generateIdempotencyKey` | ULID | Factory for mutation keys |
232
- | `getAcceptLanguage` | — | Sets `Accept-Language` when non-empty |
232
+ | `locale` | — | `getLocale`, `defaultLocale`, `onLocaleMismatch` see [Locale](#locale-accept-language--content-language) |
233
+ | `getAcceptLanguage` | — | **Deprecated** — use `locale.getLocale`; sets `Accept-Language` when non-empty |
233
234
  | `onIdempotencyReplay` | — | Fired when `Idempotent-Replayed: true` |
234
235
  | `onUnauthorized` | — | Fired on normalized **401** |
235
236
  | `onDeprecated` | — | Deprecation / sunset headers |
@@ -247,6 +248,30 @@ auth: { type: 'partner-bearer', getSecret: () => process.env.PARTNER_SECRET }
247
248
 
248
249
  If `getToken` / `getSecret` returns `null` or `undefined`, no `Authorization` header is sent.
249
250
 
251
+ ### Locale (Accept-Language / Content-Language)
252
+
253
+ Configure locale once on the client (e.g. for backends with `SetLocaleMiddleware`). This package only sets HTTP headers and optional mismatch reporting — not vue-i18n, `GET /locales`, or `GET /translations`.
254
+
255
+ ```typescript
256
+ const client = createApiClient({
257
+ baseURL: 'https://api.example.com/api/v1',
258
+ locale: {
259
+ getLocale: () => getStoredLocale(), // 'en' | 'fr' | 'fa'
260
+ defaultLocale: 'en', // omit Accept-Language when UI locale is English (server default)
261
+ onLocaleMismatch: import.meta.env.DEV ? 'warn' : undefined,
262
+ },
263
+ });
264
+ ```
265
+
266
+ | Behavior | Detail |
267
+ |----------|--------|
268
+ | **Accept-Language** | From `locale.getLocale()` on every request via this client |
269
+ | **Omit for default** | When resolved locale matches `defaultLocale` (primary subtag), header is not sent |
270
+ | **Content-Language** | Exposed on success as `res.headers.contentLanguage` |
271
+ | **Mismatch** | If response `Content-Language` differs from requested locale (base tag: `fr` vs `fr-FR` match), `'warn'` or your callback runs — UI locale is never changed |
272
+
273
+ Legacy `getAcceptLanguage` still works; `locale.getLocale` takes precedence when both are set.
274
+
250
275
  ---
251
276
 
252
277
  ## Making requests
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
36
36
  IDEMPOTENCY_MAX_LENGTH: () => IDEMPOTENCY_MAX_LENGTH,
37
37
  PACKAGE_VERSION: () => PACKAGE_VERSION,
38
+ acceptLanguageForRequest: () => acceptLanguageForRequest,
38
39
  applyJsonApiHeaders: () => applyJsonApiHeaders,
39
40
  applyTransformKeys: () => applyTransformKeys,
40
41
  assertValidIdempotencyKey: () => assertValidIdempotencyKey,
@@ -67,8 +68,12 @@ __export(index_exports, {
67
68
  isPreconditionRequiredError: () => isPreconditionRequiredError,
68
69
  isRetryablePerPolicy: () => isRetryablePerPolicy,
69
70
  isValidationError: () => isValidationError,
71
+ localesMatch: () => localesMatch,
70
72
  normalizeAxiosResponse: () => normalizeAxiosResponse,
71
73
  normalizeHttpUrl: () => normalizeHttpUrl,
74
+ normalizeLocaleCode: () => normalizeLocaleCode,
75
+ notifyLocaleMismatch: () => notifyLocaleMismatch,
76
+ parseContentLanguage: () => parseContentLanguage,
72
77
  parseDeprecationHeaders: () => parseDeprecationHeaders,
73
78
  parseJsonApiDocument: () => parseJsonApiDocument,
74
79
  parseJsonApiErrorBody: () => parseJsonApiErrorBody,
@@ -77,11 +82,14 @@ __export(index_exports, {
77
82
  parseRetryAfterSeconds: () => parseRetryAfterSeconds,
78
83
  pollAsyncResult: () => pollAsyncResult,
79
84
  readResourceVersion: () => readResourceVersion,
85
+ readResponseContentLanguage: () => readResponseContentLanguage,
80
86
  redactHeaderRecord: () => redactHeaderRecord,
81
87
  resolveAcceptLanguage: () => resolveAcceptLanguage,
82
88
  resolveAcceptedLocation: () => resolveAcceptedLocation,
83
89
  resolveAuthorizationHeader: () => resolveAuthorizationHeader,
84
90
  resolveIncluded: () => resolveIncluded,
91
+ resolveLocaleProvider: () => resolveLocaleProvider,
92
+ resolveRequestLocale: () => resolveRequestLocale,
85
93
  resolveResourcePath: () => resolveResourcePath,
86
94
  retryAllowed: () => retryAllowed,
87
95
  truncateForLog: () => truncateForLog
@@ -91,7 +99,7 @@ module.exports = __toCommonJS(index_exports);
91
99
  // package.json
92
100
  var package_default = {
93
101
  name: "@vahidkaargar/customized-api-client",
94
- version: "0.3.0",
102
+ version: "0.4.0",
95
103
  description: "TypeScript Axios client for JSON:API v1.1 with idempotency, retries, and normalized results",
96
104
  type: "module",
97
105
  engines: {
@@ -213,11 +221,85 @@ async function resolveAuthorizationHeader(auth) {
213
221
  return `Bearer ${s}`;
214
222
  }
215
223
 
224
+ // src/http/header-utils.ts
225
+ function flattenAxiosHeaders(headers) {
226
+ if (!headers) return {};
227
+ if (typeof headers.forEach === "function") {
228
+ const out2 = {};
229
+ headers.forEach((value, key) => {
230
+ out2[key.toLowerCase()] = value;
231
+ });
232
+ return out2;
233
+ }
234
+ const o = headers;
235
+ const out = {};
236
+ for (const [k, v] of Object.entries(o)) {
237
+ if (typeof v === "string") out[k.toLowerCase()] = v;
238
+ else if (Array.isArray(v) && v[0]) out[k.toLowerCase()] = v[0];
239
+ }
240
+ return out;
241
+ }
242
+ function getHeader(headers, name) {
243
+ return headers[name.toLowerCase()];
244
+ }
245
+
216
246
  // src/headers/locale.ts
247
+ function normalizeLocaleCode(tag) {
248
+ if (tag === void 0) return void 0;
249
+ const trimmed = tag.trim();
250
+ if (!trimmed) return void 0;
251
+ const base = trimmed.split(/[-_]/)[0]?.trim();
252
+ return base ? base.toLowerCase() : void 0;
253
+ }
254
+ function parseContentLanguage(header) {
255
+ if (header === void 0) return void 0;
256
+ const first = header.split(",")[0]?.trim();
257
+ if (!first) return void 0;
258
+ const tag = first.split(";")[0]?.trim();
259
+ return tag && tag.length > 0 ? tag : void 0;
260
+ }
261
+ function localesMatch(a, b) {
262
+ const na = normalizeLocaleCode(a);
263
+ const nb = normalizeLocaleCode(b);
264
+ if (na === void 0 || nb === void 0) return false;
265
+ return na === nb;
266
+ }
267
+ function resolveLocaleProvider(getAcceptLanguage, locale) {
268
+ return locale?.getLocale ?? getAcceptLanguage;
269
+ }
270
+ async function resolveRequestLocale(getAcceptLanguage, locale) {
271
+ return resolveAcceptLanguage(resolveLocaleProvider(getAcceptLanguage, locale));
272
+ }
273
+ function acceptLanguageForRequest(resolved, defaultLocale) {
274
+ if (resolved === void 0) return void 0;
275
+ if (defaultLocale !== void 0 && localesMatch(resolved, defaultLocale)) {
276
+ return void 0;
277
+ }
278
+ return resolved;
279
+ }
217
280
  async function resolveAcceptLanguage(provider) {
218
281
  if (!provider) return void 0;
219
282
  const v = await provider();
220
- return v ?? void 0;
283
+ if (v === null || v === void 0) return void 0;
284
+ const trimmed = v.trim();
285
+ return trimmed.length > 0 ? trimmed : void 0;
286
+ }
287
+ function readResponseContentLanguage(flatHeaders) {
288
+ return parseContentLanguage(getHeader(flatHeaders, "content-language"));
289
+ }
290
+ function notifyLocaleMismatch(locale, ctx) {
291
+ const handler = locale?.onLocaleMismatch;
292
+ if (!handler) return;
293
+ if (ctx.requested === void 0) return;
294
+ if (localesMatch(ctx.requested, ctx.resolved)) return;
295
+ if (handler === "warn") {
296
+ console.warn(
297
+ "[@vahidkaargar/customized-api-client] Content-Language mismatch",
298
+ ctx
299
+ );
300
+ return;
301
+ }
302
+ handler(ctx);
221
303
  }
222
304
 
223
305
  // src/headers/idempotency.ts
@@ -305,28 +387,6 @@ function parseRetryAfterSeconds(value) {
305
387
  return void 0;
306
388
  }
307
389
 
308
- // src/http/header-utils.ts
309
- function flattenAxiosHeaders(headers) {
310
- if (!headers) return {};
311
- if (typeof headers.forEach === "function") {
312
- const out2 = {};
313
- headers.forEach((value, key) => {
314
- out2[key.toLowerCase()] = value;
315
- });
316
- return out2;
317
- }
318
- const o = headers;
319
- const out = {};
320
- for (const [k, v] of Object.entries(o)) {
321
- if (typeof v === "string") out[k.toLowerCase()] = v;
322
- else if (Array.isArray(v) && v[0]) out[k.toLowerCase()] = v[0];
323
- }
324
- return out;
325
- }
326
- function getHeader(headers, name) {
327
- return headers[name.toLowerCase()];
328
- }
329
-
330
390
  // src/retry/execute-with-retry.ts
331
391
  var DEFAULT_MAX_ATTEMPTS = 4;
332
392
  var DEFAULT_BASE_MS = 200;
@@ -750,6 +810,7 @@ function createApiClient(config) {
750
810
  const mode = config.baseUrlMode ?? "modeB";
751
811
  const genKey = config.generateIdempotencyKey ?? defaultIdempotencyKey;
752
812
  warnInsecureBaseUrl(config.baseURL);
813
+ const localeByRequest = /* @__PURE__ */ new WeakMap();
753
814
  const instance = import_axios.default.create({
754
815
  baseURL: config.baseURL,
755
816
  timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
@@ -766,9 +827,15 @@ function createApiClient(config) {
766
827
  if (authHeader) {
767
828
  next.headers.Authorization = authHeader;
768
829
  }
769
- const lang = await resolveAcceptLanguage(config.getAcceptLanguage);
770
- if (lang) {
771
- next.headers["Accept-Language"] = lang;
830
+ const resolved = await resolveRequestLocale(
831
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- legacy `getAcceptLanguage` fallback
832
+ config.getAcceptLanguage,
833
+ config.locale
834
+ );
835
+ localeByRequest.set(next, resolved);
836
+ const toSend = acceptLanguageForRequest(resolved, config.locale?.defaultLocale);
837
+ if (toSend) {
838
+ next.headers["Accept-Language"] = toSend;
772
839
  }
773
840
  if (isMutationMethod(method)) {
774
841
  const h = next.headers;
@@ -794,6 +861,17 @@ function createApiClient(config) {
794
861
  if (dep && config.onDeprecated) {
795
862
  config.onDeprecated(dep);
796
863
  }
864
+ const contentLang = readResponseContentLanguage(flat);
865
+ if (contentLang) {
866
+ notifyLocaleMismatch(config.locale, {
867
+ requested: localeByRequest.get(res.config),
868
+ resolved: contentLang,
869
+ /* v8 ignore start -- @preserve axios config url/method are strings */
870
+ url: typeof res.config.url === "string" ? res.config.url : void 0,
871
+ method: typeof res.config.method === "string" ? res.config.method : void 0
872
+ /* v8 ignore stop -- @preserve */
873
+ });
874
+ }
797
875
  return res;
798
876
  },
799
877
  (err) => Promise.reject(
@@ -1178,6 +1256,7 @@ var PACKAGE_VERSION = package_default.version;
1178
1256
  DEFAULT_TIMEOUT_MS,
1179
1257
  IDEMPOTENCY_MAX_LENGTH,
1180
1258
  PACKAGE_VERSION,
1259
+ acceptLanguageForRequest,
1181
1260
  applyJsonApiHeaders,
1182
1261
  applyTransformKeys,
1183
1262
  assertValidIdempotencyKey,
@@ -1210,8 +1289,12 @@ var PACKAGE_VERSION = package_default.version;
1210
1289
  isPreconditionRequiredError,
1211
1290
  isRetryablePerPolicy,
1212
1291
  isValidationError,
1292
+ localesMatch,
1213
1293
  normalizeAxiosResponse,
1214
1294
  normalizeHttpUrl,
1295
+ normalizeLocaleCode,
1296
+ notifyLocaleMismatch,
1297
+ parseContentLanguage,
1215
1298
  parseDeprecationHeaders,
1216
1299
  parseJsonApiDocument,
1217
1300
  parseJsonApiErrorBody,
@@ -1220,11 +1303,14 @@ var PACKAGE_VERSION = package_default.version;
1220
1303
  parseRetryAfterSeconds,
1221
1304
  pollAsyncResult,
1222
1305
  readResourceVersion,
1306
+ readResponseContentLanguage,
1223
1307
  redactHeaderRecord,
1224
1308
  resolveAcceptLanguage,
1225
1309
  resolveAcceptedLocation,
1226
1310
  resolveAuthorizationHeader,
1227
1311
  resolveIncluded,
1312
+ resolveLocaleProvider,
1313
+ resolveRequestLocale,
1228
1314
  resolveResourcePath,
1229
1315
  retryAllowed,
1230
1316
  truncateForLog