@i18n-micro/utils 1.0.6 → 1.0.8

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.
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class s{constructor(t){this.cache=new Map,this.expiry=new Map,this.maxSize=0,this.ttlMs=0,t&&this.configure(t)}configure(t){this.maxSize=t.maxSize??0,this.ttlMs=(t.ttl??0)*1e3,this.ttlMs===0&&this.expiry.clear()}get(t){const e=this.cache.get(t);if(e!==void 0){if(this.ttlMs>0){const i=this.expiry.get(t);if(i&&Date.now()>i){this.delete(t);return}this.expiry.set(t,Date.now()+this.ttlMs)}return this.cache.delete(t),this.cache.set(t,e),e}}has(t){if(!this.cache.has(t))return!1;if(this.ttlMs>0){const e=this.expiry.get(t);if(e&&Date.now()>e)return this.delete(t),!1}return!0}set(t,e){if(this.cache.has(t))this.cache.delete(t);else if(this.maxSize>0&&this.cache.size>=this.maxSize){const i=this.cache.keys().next().value;i!==void 0&&this.delete(i)}this.cache.set(t,e),this.ttlMs>0&&this.expiry.set(t,Date.now()+this.ttlMs)}delete(t){return this.expiry.delete(t),this.cache.delete(t)}clear(){this.cache.clear(),this.expiry.clear()}keys(){return this.cache.keys()}get size(){return this.cache.size}}exports.CacheControl=s;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class s{constructor(t){this.cache=new Map,this.expiry=new Map,this.maxSize=0,this.ttlMs=0,t&&this.configure(t)}configure(t){this.maxSize=t.maxSize??0,this.ttlMs=(t.ttl??0)*1e3,this.ttlMs===0&&this.expiry.clear()}get(t){const e=this.cache.get(t);if(e!==void 0){if(this.ttlMs>0){const i=this.expiry.get(t);if(i&&Date.now()>i){this.delete(t);return}this.expiry.set(t,Date.now()+this.ttlMs)}return this.maxSize>0&&(this.cache.delete(t),this.cache.set(t,e)),e}}has(t){if(!this.cache.has(t))return!1;if(this.ttlMs>0){const e=this.expiry.get(t);if(e&&Date.now()>e)return this.delete(t),!1}return!0}set(t,e){if(this.cache.has(t))this.cache.delete(t);else if(this.maxSize>0&&this.cache.size>=this.maxSize){const i=this.cache.keys().next().value;i!==void 0&&this.delete(i)}this.cache.set(t,e),this.ttlMs>0&&this.expiry.set(t,Date.now()+this.ttlMs)}delete(t){return this.expiry.delete(t),this.cache.delete(t)}clear(){this.cache.clear(),this.expiry.clear()}keys(){return this.cache.keys()}get size(){return this.cache.size}}exports.CacheControl=s;
@@ -17,7 +17,7 @@ class h {
17
17
  }
18
18
  this.expiry.set(t, Date.now() + this.ttlMs);
19
19
  }
20
- return this.cache.delete(t), this.cache.set(t, e), e;
20
+ return this.maxSize > 0 && (this.cache.delete(t), this.cache.set(t, e)), e;
21
21
  }
22
22
  }
23
23
  has(t) {
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function i(s,o){const t={...s};for(const e in o){if(e==="__proto__"||e==="constructor")continue;const r=o[e],n=t[e];r!==null&&typeof r=="object"&&!Array.isArray(r)&&n!==null&&typeof n=="object"&&!Array.isArray(n)?t[e]={...n,...r}:t[e]=r}return t}function c(s,o){if(!s||Object.keys(s).length===0)return{...o};const t={...s};for(const e in o){if(e==="__proto__"||e==="constructor")continue;const r=o[e],n=t[e];r&&typeof r=="object"&&!Array.isArray(r)&&n&&typeof n=="object"&&!Array.isArray(n)?t[e]=c(n,r):t[e]=r}return t}exports.deepMergeTranslations=i;exports.deepMergeTranslationsRecursive=c;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function i(s,n){const r={...s};for(const e of Object.keys(n)){if(e==="__proto__")continue;const t=n[e],o=r[e];t!==null&&typeof t=="object"&&!Array.isArray(t)&&o!==null&&typeof o=="object"&&!Array.isArray(o)?r[e]={...o,...t}:r[e]=t}return r}function c(s,n){if(!s||Object.keys(s).length===0)return{...n};const r={...s};for(const e of Object.keys(n)){if(e==="__proto__")continue;const t=n[e],o=r[e];t&&typeof t=="object"&&!Array.isArray(t)&&o&&typeof o=="object"&&!Array.isArray(o)?r[e]=c(o,t):r[e]=t}return r}exports.deepMergeTranslations=i;exports.deepMergeTranslationsRecursive=c;
@@ -1,23 +1,23 @@
1
- function i(s, e) {
2
- const o = { ...s };
3
- for (const t in e) {
4
- if (t === "__proto__" || t === "constructor") continue;
5
- const r = e[t], n = o[t];
6
- r !== null && typeof r == "object" && !Array.isArray(r) && n !== null && typeof n == "object" && !Array.isArray(n) ? o[t] = { ...n, ...r } : o[t] = r;
1
+ function y(s, n) {
2
+ const r = { ...s };
3
+ for (const e of Object.keys(n)) {
4
+ if (e === "__proto__") continue;
5
+ const t = n[e], o = r[e];
6
+ t !== null && typeof t == "object" && !Array.isArray(t) && o !== null && typeof o == "object" && !Array.isArray(o) ? r[e] = { ...o, ...t } : r[e] = t;
7
7
  }
8
- return o;
8
+ return r;
9
9
  }
10
- function c(s, e) {
11
- if (!s || Object.keys(s).length === 0) return { ...e };
12
- const o = { ...s };
13
- for (const t in e) {
14
- if (t === "__proto__" || t === "constructor") continue;
15
- const r = e[t], n = o[t];
16
- r && typeof r == "object" && !Array.isArray(r) && n && typeof n == "object" && !Array.isArray(n) ? o[t] = c(n, r) : o[t] = r;
10
+ function c(s, n) {
11
+ if (!s || Object.keys(s).length === 0) return { ...n };
12
+ const r = { ...s };
13
+ for (const e of Object.keys(n)) {
14
+ if (e === "__proto__") continue;
15
+ const t = n[e], o = r[e];
16
+ t && typeof t == "object" && !Array.isArray(t) && o && typeof o == "object" && !Array.isArray(o) ? r[e] = c(o, t) : r[e] = t;
17
17
  }
18
- return o;
18
+ return r;
19
19
  }
20
20
  export {
21
- i as deepMergeTranslations,
21
+ y as deepMergeTranslations,
22
22
  c as deepMergeTranslationsRecursive
23
23
  };
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("node:fs"),s=require("node:path");function r(n,t){for(const e of o.readdirSync(n,{withFileTypes:!0})){const i=s.join(n,e.name);if(e.isDirectory()){r(i,t);continue}e.name.endsWith(".json")&&(t.fileCount+=1,t.totalBytes+=o.statSync(i).size)}}function a(n){const t={fileCount:0,totalBytes:0};return o.existsSync(n)&&r(n,t),t}exports.scanTranslationPayloadDirectory=a;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("node:crypto"),o=require("node:fs"),l=require("node:zlib"),u=require("node:path");function f(n,t){for(const e of o.readdirSync(n,{withFileTypes:!0})){const s=u.join(n,e.name);e.isDirectory()?f(s,t):e.name.endsWith(".json")&&t.push(s)}}function S(n){const t={fileCount:0,totalBytes:0};if(!o.existsSync(n))return t;const e=[];f(n,e);for(const s of e)t.fileCount+=1,t.totalBytes+=o.statSync(s).size;return t}function d(n,t){const e=y.createHash("sha256");let s=0;for(const c of n){const a=u.join(c,t);if(!o.existsSync(a))continue;const r=[];f(a,r),r.sort();for(const i of r)e.update(u.relative(a,i)),e.update("\0"),e.update(o.readFileSync(i)),s+=1}return s>0?e.digest("hex").slice(0,16):null}const h=1024;function T(n,t){if(!t||!o.existsSync(n))return 0;const e=t===!0||t.gzip!==!1,s=t===!0||t.brotli!==!1;if(!e&&!s)return 0;const c=[];f(n,c);let a=0;for(const r of c){const i=o.readFileSync(r);i.length<h||(e&&o.writeFileSync(`${r}.gz`,l.gzipSync(i,{level:9})),s&&o.writeFileSync(`${r}.br`,l.brotliCompressSync(i,{params:{[l.constants.BROTLI_PARAM_QUALITY]:11,[l.constants.BROTLI_PARAM_SIZE_HINT]:i.length}})),a+=1)}return a}exports.compressTranslationPayloads=T;exports.hashTranslationSources=d;exports.scanTranslationPayloadDirectory=S;
@@ -1,2 +1,32 @@
1
1
  import { TranslationPayloadStats } from './payload-config';
2
2
  export declare function scanTranslationPayloadDirectory(dir: string): TranslationPayloadStats;
3
+ /**
4
+ * Fingerprint of the translation sources, used to cache-bust `_locales` requests.
5
+ *
6
+ * The alternative — a build timestamp — changes on every deploy, so browsers and CDNs
7
+ * throw away a dictionary that may not have changed at all. Hashing the sources means
8
+ * the URL only moves when the translations actually move.
9
+ *
10
+ * Layer order matters (later layers override earlier ones), so paths are hashed in the
11
+ * order given and relative to their own root: an identical tree checked out elsewhere
12
+ * must produce the same value.
13
+ *
14
+ * Returns `null` when there is nothing to hash, leaving the caller to fall back.
15
+ */
16
+ export declare function hashTranslationSources(rootDirs: string[], translationDirName: string): string | null;
17
+ /** Nitro's `compressPublicAssets`: `true` means both encodings, an object selects them. */
18
+ export type PublicAssetCompression = boolean | {
19
+ gzip?: boolean;
20
+ brotli?: boolean;
21
+ } | undefined;
22
+ /**
23
+ * Gzip/brotli the translation payloads copied into the public directory.
24
+ *
25
+ * Nitro compresses public assets before the `nitro:build:public-assets` hook where the
26
+ * payloads are copied, so `compressPublicAssets` never reaches them — a static host
27
+ * would serve the single largest part of the output uncompressed. This applies the
28
+ * user's setting to those files; it does not turn compression on by itself.
29
+ *
30
+ * Returns the number of files actually compressed.
31
+ */
32
+ export declare function compressTranslationPayloads(dir: string, compression: PublicAssetCompression): number;
@@ -1,2 +1,32 @@
1
1
  import { TranslationPayloadStats } from './payload-config';
2
2
  export declare function scanTranslationPayloadDirectory(dir: string): TranslationPayloadStats;
3
+ /**
4
+ * Fingerprint of the translation sources, used to cache-bust `_locales` requests.
5
+ *
6
+ * The alternative — a build timestamp — changes on every deploy, so browsers and CDNs
7
+ * throw away a dictionary that may not have changed at all. Hashing the sources means
8
+ * the URL only moves when the translations actually move.
9
+ *
10
+ * Layer order matters (later layers override earlier ones), so paths are hashed in the
11
+ * order given and relative to their own root: an identical tree checked out elsewhere
12
+ * must produce the same value.
13
+ *
14
+ * Returns `null` when there is nothing to hash, leaving the caller to fall back.
15
+ */
16
+ export declare function hashTranslationSources(rootDirs: string[], translationDirName: string): string | null;
17
+ /** Nitro's `compressPublicAssets`: `true` means both encodings, an object selects them. */
18
+ export type PublicAssetCompression = boolean | {
19
+ gzip?: boolean;
20
+ brotli?: boolean;
21
+ } | undefined;
22
+ /**
23
+ * Gzip/brotli the translation payloads copied into the public directory.
24
+ *
25
+ * Nitro compresses public assets before the `nitro:build:public-assets` hook where the
26
+ * payloads are copied, so `compressPublicAssets` never reaches them — a static host
27
+ * would serve the single largest part of the output uncompressed. This applies the
28
+ * user's setting to those files; it does not turn compression on by itself.
29
+ *
30
+ * Returns the number of files actually compressed.
31
+ */
32
+ export declare function compressTranslationPayloads(dir: string, compression: PublicAssetCompression): number;
@@ -1,19 +1,54 @@
1
- import { existsSync as r, readdirSync as s, statSync as c } from "node:fs";
2
- import { join as a } from "node:path";
3
- function i(n, t) {
4
- for (const o of s(n, { withFileTypes: !0 })) {
5
- const e = a(n, o.name);
6
- if (o.isDirectory()) {
7
- i(e, t);
8
- continue;
9
- }
10
- o.name.endsWith(".json") && (t.fileCount += 1, t.totalBytes += c(e).size);
1
+ import { createHash as p } from "node:crypto";
2
+ import { existsSync as c, readFileSync as y, writeFileSync as a, statSync as S, readdirSync as m } from "node:fs";
3
+ import { gzipSync as T, brotliCompressSync as d, constants as u } from "node:zlib";
4
+ import { join as h, relative as _ } from "node:path";
5
+ function f(n, t) {
6
+ for (const e of m(n, { withFileTypes: !0 })) {
7
+ const o = h(n, e.name);
8
+ e.isDirectory() ? f(o, t) : e.name.endsWith(".json") && t.push(o);
11
9
  }
12
10
  }
13
- function u(n) {
11
+ function A(n) {
14
12
  const t = { fileCount: 0, totalBytes: 0 };
15
- return r(n) && i(n, t), t;
13
+ if (!c(n)) return t;
14
+ const e = [];
15
+ f(n, e);
16
+ for (const o of e)
17
+ t.fileCount += 1, t.totalBytes += S(o).size;
18
+ return t;
19
+ }
20
+ function B(n, t) {
21
+ const e = p("sha256");
22
+ let o = 0;
23
+ for (const l of n) {
24
+ const i = h(l, t);
25
+ if (!c(i)) continue;
26
+ const s = [];
27
+ f(i, s), s.sort();
28
+ for (const r of s)
29
+ e.update(_(i, r)), e.update("\0"), e.update(y(r)), o += 1;
30
+ }
31
+ return o > 0 ? e.digest("hex").slice(0, 16) : null;
32
+ }
33
+ const g = 1024;
34
+ function b(n, t) {
35
+ if (!t || !c(n)) return 0;
36
+ const e = t === !0 || t.gzip !== !1, o = t === !0 || t.brotli !== !1;
37
+ if (!e && !o) return 0;
38
+ const l = [];
39
+ f(n, l);
40
+ let i = 0;
41
+ for (const s of l) {
42
+ const r = y(s);
43
+ r.length < g || (e && a(`${s}.gz`, T(r, { level: 9 })), o && a(
44
+ `${s}.br`,
45
+ d(r, { params: { [u.BROTLI_PARAM_QUALITY]: 11, [u.BROTLI_PARAM_SIZE_HINT]: r.length } })
46
+ ), i += 1);
47
+ }
48
+ return i;
16
49
  }
17
50
  export {
18
- u as scanTranslationPayloadDirectory
51
+ b as compressTranslationPayloads,
52
+ B as hashTranslationSources,
53
+ A as scanTranslationPayloadDirectory
19
54
  };
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function r(a,e){const l=a||"index";return e?.[l]??l}function o(a,e,l){return`/${a.replace(/^\/+|\/+$/g,"")}/${e}/${l}/data.json`.replace(/\/{2,}/g,"/")}function n(a){return(a.isServer?a.apiBaseServerHost:a.apiBaseClientHost)??a.baseURL}function t(a){const e=r(a.routeName,a.routesLocaleLinks);return{path:o(a.apiBaseUrl,e,a.locale),baseURL:n(a),params:a.dateBuild?{v:a.dateBuild}:void 0}}exports.buildTranslationPayloadFetchRequest=t;exports.buildTranslationPayloadPath=o;exports.resolveTranslationPayloadBaseURL=n;exports.resolveTranslationPayloadPage=r;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function r(a,e){const l=a||"index";return e?.[l]??l}function t(a,e,l){return`/${a.replace(/^\/+|\/+$/g,"")}/${e}/${l}/data.json`.replace(/\/{2,}/g,"/")}function n(a){return(a.isServer?a.apiBaseServerHost:a.apiBaseClientHost)??a.baseURL}function o(a){const e=r(a.routeName,a.routesLocaleLinks);return{path:t(a.apiBaseUrl,e,a.locale),baseURL:n(a),params:a.dateBuild?{v:a.dateBuild}:void 0}}function s(a,e=!0){const l=Math.floor(a??0);return!Number.isSafeInteger(l)||l<=0?null:e?`public, max-age=${l}, immutable`:"public, max-age=0, must-revalidate"}exports.buildTranslationPayloadCacheControl=s;exports.buildTranslationPayloadFetchRequest=o;exports.buildTranslationPayloadPath=t;exports.resolveTranslationPayloadBaseURL=n;exports.resolveTranslationPayloadPage=r;
@@ -22,3 +22,15 @@ export declare function buildTranslationPayloadFetchRequest(input: TranslationPa
22
22
  baseURL: string;
23
23
  params?: Record<string, string | number>;
24
24
  };
25
+ /**
26
+ * Builds `Cache-Control` for `/{apiBaseUrl}/:page/:locale/data.json`.
27
+ *
28
+ * `immutable` is only emitted when a cache-buster is active (`hasCacheBuster`),
29
+ * i.e. requests carry `?v=<dateBuild>`. Without a cache-buster (`dateBuild` set
30
+ * to `0`/`''`) the URL is stable across deploys, so we intentionally avoid
31
+ * long-lived caching by returning a conservative `max-age=0` policy.
32
+ *
33
+ * Returns `null` when caching should be skipped — non-positive or non-finite
34
+ * `duration` (guards `NaN`/`Infinity` from producing a malformed header).
35
+ */
36
+ export declare function buildTranslationPayloadCacheControl(durationSeconds: number | undefined, hasCacheBuster?: boolean): string | null;
@@ -22,3 +22,15 @@ export declare function buildTranslationPayloadFetchRequest(input: TranslationPa
22
22
  baseURL: string;
23
23
  params?: Record<string, string | number>;
24
24
  };
25
+ /**
26
+ * Builds `Cache-Control` for `/{apiBaseUrl}/:page/:locale/data.json`.
27
+ *
28
+ * `immutable` is only emitted when a cache-buster is active (`hasCacheBuster`),
29
+ * i.e. requests carry `?v=<dateBuild>`. Without a cache-buster (`dateBuild` set
30
+ * to `0`/`''`) the URL is stable across deploys, so we intentionally avoid
31
+ * long-lived caching by returning a conservative `max-age=0` policy.
32
+ *
33
+ * Returns `null` when caching should be skipped — non-positive or non-finite
34
+ * `duration` (guards `NaN`/`Infinity` from producing a malformed header).
35
+ */
36
+ export declare function buildTranslationPayloadCacheControl(durationSeconds: number | undefined, hasCacheBuster?: boolean): string | null;
@@ -5,20 +5,25 @@ function l(a, e) {
5
5
  function n(a, e, r) {
6
6
  return `/${a.replace(/^\/+|\/+$/g, "")}/${e}/${r}/data.json`.replace(/\/{2,}/g, "/");
7
7
  }
8
- function o(a) {
8
+ function t(a) {
9
9
  return (a.isServer ? a.apiBaseServerHost : a.apiBaseClientHost) ?? a.baseURL;
10
10
  }
11
- function t(a) {
11
+ function s(a) {
12
12
  const e = l(a.routeName, a.routesLocaleLinks);
13
13
  return {
14
14
  path: n(a.apiBaseUrl, e, a.locale),
15
- baseURL: o(a),
15
+ baseURL: t(a),
16
16
  params: a.dateBuild ? { v: a.dateBuild } : void 0
17
17
  };
18
18
  }
19
+ function i(a, e = !0) {
20
+ const r = Math.floor(a ?? 0);
21
+ return !Number.isSafeInteger(r) || r <= 0 ? null : e ? `public, max-age=${r}, immutable` : "public, max-age=0, must-revalidate";
22
+ }
19
23
  export {
20
- t as buildTranslationPayloadFetchRequest,
24
+ i as buildTranslationPayloadCacheControl,
25
+ s as buildTranslationPayloadFetchRequest,
21
26
  n as buildTranslationPayloadPath,
22
- o as resolveTranslationPayloadBaseURL,
27
+ t as resolveTranslationPayloadBaseURL,
23
28
  l as resolveTranslationPayloadPage
24
29
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i18n-micro/utils",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Framework-agnostic utilities for Nuxt I18n Micro (granular subpath imports).",
5
5
  "keywords": [
6
6
  "i18n",
@@ -353,14 +353,14 @@
353
353
  },
354
354
  "dependencies": {
355
355
  "globby": "^14.1.0",
356
- "@i18n-micro/types": "1.2.6"
356
+ "@i18n-micro/types": "1.2.7"
357
357
  },
358
358
  "devDependencies": {
359
359
  "publint": "^0.3.17",
360
- "vite": "^7.3.1",
360
+ "vite": "^7.3.6",
361
361
  "vite-plugin-dts": "^4.5.4",
362
- "vitest": "^3.2.4",
363
- "@i18n-micro/test-utils": "1.2.2"
362
+ "vitest": "^4.1.10",
363
+ "@i18n-micro/test-utils": "1.2.6"
364
364
  },
365
365
  "engines": {
366
366
  "node": ">=18"
@@ -368,7 +368,7 @@
368
368
  "scripts": {
369
369
  "build": "vite build",
370
370
  "check:package": "publint",
371
- "test": "jest",
371
+ "test": "vitest run",
372
372
  "test:dist": "vitest run --config vitest.dist.config.ts"
373
373
  }
374
374
  }