@voltro/i18n 0.50.0 → 0.51.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/CHANGELOG.md +160 -0
- package/dist/cookieNames-CqdYWVpp.js +11 -0
- package/dist/index.d.ts +155 -23
- package/dist/index.js +96 -81
- package/dist/server.d.ts +77 -2
- package/dist/server.js +37 -23
- package/package.json +1 -1
- package/dist/cookieNames-BlfAjNqu.js +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,166 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.51.0] — 2026-08-24
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/data-transfer, @voltro/cli** — The asset counts reported references as if they were blobs. `_voltro_storage_refs` holds one row per reference and several rows legitimately name one key, so a capture of 57 rows over 16 keys wrote `count: 57` into the stamp beside an `assets/` directory holding 16 files, and the restore reported "57 blob(s) restored" while 16 objects appeared. Nothing was lost; what was lost is the ability to check. Anyone answering "are all the blobs there?" after a restore compared the stamp's number against one they counted and found a 3.5x gap that was not one.
|
|
47
|
+
|
|
48
|
+
A key named by several references is now fetched once rather than downloaded, hashed and discarded once per row, and the three numbers are stated separately: `references` (rows enumerated), `count` (distinct keys), `objects` (distinct sha256 bodies), with `totalBytes` and `objectBytes` beside them. The stamp's existing fields keep their names and now mean what a reader always took them for; the new ones are optional, so a stamp written before them still parses.
|
|
49
|
+
|
|
50
|
+
**Breaking on one export.** `restoreAssetsFromCas` returns `{ count, objects }` instead of a bare `number` — one number could not answer both questions, which is the defect. `count` is what the old value was, so the migration that changes nothing is `.count`.
|
|
51
|
+
|
|
52
|
+
**`voltro update` carries you across this** — codemod `0.51.0/03_restore-assets-returns-counts`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.51.0).
|
|
53
|
+
- **@voltro/i18n** — `<I18nProvider>` takes `timeZone` as its own prop, and `intlConfig` no longer accepts one.
|
|
54
|
+
|
|
55
|
+
`intlConfig` exists to forward props to react-intl UNMODIFIED, and for every other member of `IntlConfig` that is the right shape. `timeZone` is the one member this package's own formatters read — and they did not read it: `useFormatDate` built `Intl.DateTimeFormat` itself and took only the locale from the provider. So a zone passed through `intlConfig` configured `<T>`'s ICU dates and NOT the `useFormatDate()` beside them. Measured under one provider, one instant (`2026-08-24T23:30:00Z`), `locale: 'de'`, `intlConfig: { timeZone: 'Europe/Berlin' }`, process zone UTC: react-intl rendered `25.08.26, 01:30` and the hook rendered `24.08.26, 23:30`. A different hour, and a different day.
|
|
56
|
+
|
|
57
|
+
The zone has to be a prop this package can see. It is validated once at the provider (an unusable zone — a stale cookie, a typo, a runtime with a trimmed ICU — is dropped, because `Intl` THROWS on an unknown zone and `useFormatDate` catches, which would degrade every timestamp in the app to a raw `Date` string). It is what the new `useTimeZone()` reports. And it is what the framework fills per request.
|
|
58
|
+
|
|
59
|
+
**Migration:** `intlConfig={{ timeZone: 'Europe/Berlin' }}` → `timeZone="Europe/Berlin"`. The codemod does it, including the case where the zone was the bag's only member. An `intlConfig` naming a variable is reported by file and line rather than guessed at — the property would otherwise stop being read with nothing red anywhere.
|
|
60
|
+
- **@voltro/cli** — A native restore whose bookkeeping store would not open ran anyway, with no in-progress marker and no word about it. `nativeBookkeeping` was `try { … } catch { return undefined }`, and that `undefined` guarded every branch below — including the refusal for a marker that could not be written. So the failure removed the precaution AND the sentence that would have reported it missing, and `markerState` was never computed, defaulting to "held".
|
|
61
|
+
|
|
62
|
+
What it produced was worse than silence: a failed restore printed "This database is now in an unknown state and the next boot will REFUSE, by design" over a database with zero marker rows. The next boot did not refuse, and `voltro data clear-replace-marker` had nothing to clear. The trigger is not exotic — wrong credentials, an unreachable database, a missing env var, no `app.config.ts` from here — and a restore is the operation you run against a target that is already unwell, so the guard fell away exactly when it was needed.
|
|
63
|
+
|
|
64
|
+
The reason now travels instead of being caught and dropped. A restore that cannot write the marker REFUSES and names which of the two reasons it was (the store would not open, or the table is not there); those were two separate refusals and are now one, because they are one decision for the operator. `--no-marker` is the deliberate way past it and warns every time. And the `recorded:` line no longer reports a local failure as a property of the target — it says where the failure was.
|
|
65
|
+
|
|
66
|
+
**This changes an exit code.** A restore that could not write the marker used to exit 0; it now exits 1. Two invocations are affected — the bookkeeping store will not open, or there is no `app.config.ts` from the working directory — and the second surprises people, because `restore` reads its target from the environment and so looks like it needs no project. It does not, for the dump; it needs one for the marker. `--no-marker` is the deliberate way through and warns every time. The note ships under `reach: 'beyond-source'` because the affected invocations live in cron entries, CI jobs and runbooks rather than in TypeScript.
|
|
67
|
+
|
|
68
|
+
**`voltro update` carries you across this** — codemod `0.51.0/02_restore-refuses-without-marker`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.51.0).
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **@voltro/i18n, @voltro/cli, @voltro/voltro** — `timeZone` in the web `app.config.ts` — the zone every date/time formatter renders in, resolved per request and published so the client agrees.
|
|
73
|
+
|
|
74
|
+
A formatter is deterministic given the value, the locale, the zone and the clock. The locale already came from the provider and was already agreed across the hydration boundary — the server publishes it as `<html lang>` and the client reads that attribute rather than `navigator.languages`, precisely because the browser's own answer can differ from what the server saw. The zone had no such source. `Intl` fell back to the zone of whichever runtime was formatting: the pod on the server (UTC on a container with no `TZ`), the viewer's machine in the browser. Every server-rendered timestamp was therefore a hydration mismatch waiting for a wide enough offset, and across midnight it was a different calendar day.
|
|
75
|
+
|
|
76
|
+
timeZone: 'Europe/Berlin' // one zone for every viewer timeZone: 'viewer' // per request, from the `voltro:tz` cookie defaultTimeZone: 'UTC' // before the viewer's zone is known
|
|
77
|
+
|
|
78
|
+
Whatever it resolves to is stamped on the document as `<html data-voltro-tz>`, and the generated client entry reads that attribute. Both sides then format against one value — which is the property that removes the mismatch, whether or not the value is the viewer's true zone: being wrong together is repairable after mount, being different is not.
|
|
79
|
+
|
|
80
|
+
Under `'viewer'` the framework injects a script that seeds `voltro:tz` from the browser when the cookie is absent, so the server renders in the viewer's zone from the second request with no login. It never overwrites an existing value — the APP is the authoritative writer, at login, from the zone it holds for the signed-in user (`TIMEZONE_COOKIE` and `isSupportedTimeZone` are exported for that). Unset, nothing changes: each runtime keeps using its own zone, and `useTimeZone()` returns `undefined` to say so.
|
|
81
|
+
|
|
82
|
+
This is the RENDER zone. The server-side compute zone — what `startOfDay` resolves against inside a handler — is still `@voltro/datetime/context`'s seam, unwired.
|
|
83
|
+
|
|
84
|
+
**`apiSurface: compatible`** covers the two golden lines that moved, and they are the same change twice: `makeSsgWrap`'s returned wrapper, and the `wraps` record on the SSG shell input, each gained an OPTIONAL second parameter (the per-request zone + render instant; the wrapper is built once per locale, so they cannot live in the factory). A function with an optional extra parameter is assignable wherever the one-parameter type was expected, so no call site that compiled stops compiling — and both are the framework's SSG bridge, documented as never imported by app code. Everything else this release adds to these packages is a pure addition; the one genuine break in `@voltro/i18n` is the `timeZone` prop, which has its own entry and its own codemod.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- **@voltro/cli** — A `BREAKING` entry's changelog footer now tells a reader who pins versions by hand how to print the codemod's note without upgrading anything:
|
|
89
|
+
|
|
90
|
+
voltro update --codemods-only --from <your current version> --dry-run
|
|
91
|
+
|
|
92
|
+
The footer used to stop at the codemod's id, which is enough for anyone who runs `voltro update` and nothing at all for anyone who does not. A deployment said so plainly: they pin every `@voltro/*` version from their own container scripts, have never run the command, and `CHANGELOG.md` out of the tarball is the only channel anything reaches them through. So a note deliberately filed under an unreached version — our one mechanism for correcting guidance that can no longer be corrected in place — reached that population not at all, and the id told them a fix existed without telling them what it was.
|
|
93
|
+
|
|
94
|
+
No new surface: every flag in that invocation is already parsed, which is what lets `check-message-apis.mjs` verify the line rather than trust it.
|
|
95
|
+
- **@voltro/cli** — Two fingerprint labels now say what they compare.
|
|
96
|
+
|
|
97
|
+
The restore's skew warning said the backup's schema "differs from what this code declares". It does not: the value it compares against is the TARGET database's live schema, read by introspection at restore time. Bringing a target to the backup's shape makes the warning disappear while the declared fingerprint is a third value entirely, which is how the mislabel was caught. The comparison is the useful one and is unchanged; the sentence sent readers looking for a code change where a database differed.
|
|
98
|
+
|
|
99
|
+
`voltro db plan` prints `fingerprint: live … · declared …` instead of `from … → to …`, plus a line saying the two are not meant to match. A hash of a live database never equals the hash of the declaration it came from — introspection cannot recover generated expressions, `maxLength` or sensitivity markers — which is why `db drift` keeps a separate live baseline. Printed as `from → to`, `0 operations` under two differing hashes read as a contradiction.
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- **@voltro/cli** — `voltro check` reported a reactivity CHANNEL as a missing table, at `error` severity — so it set the exit code:
|
|
104
|
+
|
|
105
|
+
✗ error reference/dangling-source query(presence.list) reads table 'channel:presence' which does not exist fix: declare a 'channel:presence.entity.ts' table or fix the query's source
|
|
106
|
+
|
|
107
|
+
A `source:` entry is a table name OR a channel's routing key (`channel:<name>`), and every rule resolved entries against the table set. The advice cannot be followed — a channel exists precisely because no table is meant — and because it is an error rather than a warning, `voltro check` could not be a CI gate for any app that uses a channel. That includes an app whose only channel comes from `@voltro/plugin-presence`, whose own `presence.list` declares one: a first-party feature meeting a rule that did not know about it, inside a first-party plugin.
|
|
108
|
+
|
|
109
|
+
Channels are filtered in the ONE helper every table rule reads, rather than at each rule, because a per-rule filter is how the next rule joins without one. The same cause was live one rule over: `observed/declared-but-unobserved` reported "declares source 'channel:presence' but never read it while running" for every exercised procedure that declares a channel. Both are covered, each with a negative control — a filter that dropped the whole source list would have silenced the rules instead of narrowing them.
|
|
110
|
+
- **@voltro/data-transfer** — A native dump no longer carries `_voltro_data_transfers`, for the same reason it stopped carrying the in-progress marker one release ago. The restore opens its own run row there BEFORE the tool runs; the dump then dropped the table mid-flight, and the update recording the outcome wrote into a table that no longer held the row. Measured downstream: after a deliberately failed native restore, `voltro data transfers` showed no restore at all — only the `backup` row the dump had carried over from the SOURCE database. The command that answers "did the restore finish" could not see the run asking the question.
|
|
111
|
+
|
|
112
|
+
Exactly two tables are excluded and the line is deliberate: a native restore into the same deployment should bring the migration ledger, the stored plans, the CDC offsets and the schedule claims, because they describe the data being restored. These two describe the RESTORE, and a record of an operation must not be overwritten by the operation it records. Covered per dialect against real servers and real vendor tools, including a non-vacuity check that a table which SHOULD travel still does.
|
|
113
|
+
- **@voltro/i18n, @voltro/cli** — `useRelativeTime` used `Date.now()` as its base, which under SSR is two different numbers. The server rendered at T and wrote "3 minutes ago" into the HTML; the browser hydrated at T+Δ and rendered "4 minutes ago" whenever a unit boundary fell in the gap. The gap is network latency, so it reproduced on a slow connection and never on the developer's machine, and it had nothing to do with timezones — a correctly zoned app hit it just the same.
|
|
114
|
+
|
|
115
|
+
The server states its render instant (`<html data-voltro-now>`, `renderedAt` on the provider), the first client render uses that same number, and the clock goes live once hydration commits. Server markup and hydration markup are therefore identical BY CONSTRUCTION — the property `await.tsx` and `deferred.ts` already hold, rather than `suppressHydrationWarning`, which would hide a real mismatch along with this one. An explicit `{ now }` still wins.
|
|
116
|
+
|
|
117
|
+
The mount state lives in the provider, not in the hook: a table of ten thousand rows would otherwise pay a state hook and a passive effect each to learn one fact that is true for the whole document. An app that never renders on the server publishes no stamp and takes no second render pass.
|
|
118
|
+
- **@voltro/cli** — `closeNativeRun` writes the transfer row BACK when the restore's own artefact dropped the table it lives in, instead of issuing an `UPDATE` that matches nothing and returning happily. Excluding `_voltro_data_transfers` from our own dumps shortens that window; it does nothing for a dump taken before that change, for a hand-made one, or for mssql and sqlite, whose restores have no per-table exclusion at all. The write-back covers every dialect and every artefact, which is why it is the rule and the exclusion is the optimisation.
|
|
119
|
+
|
|
120
|
+
The row is read back rather than trusted — an update that matched nothing is indistinguishable from one that matched — and a read that itself fails writes nothing, because a duplicate row invented on a guess is its own defect in a history somebody reads under pressure.
|
|
121
|
+
- **@voltro/cli** — A prerendered page shipped the shell's baked `lang="en"` whatever locale it was rendered in.
|
|
122
|
+
|
|
123
|
+
`voltro dev` and `voltro start` both set `<html lang>` per request; the prerender never did. So a `/de/...` artefact — rendered with the German catalog, handed `locale: 'de'` in its `meta` — served `<html lang="en">`. That attribute is what a screen reader pronounces in, what Chrome offers to translate FROM, and what hyphenation uses, so the failure was silent to whoever shipped it and loud only to the people it excluded. The same shape as the 0.30.0 cookie-name drift, one document path over.
|
|
124
|
+
|
|
125
|
+
It surfaced while giving the zone somewhere to travel: `<html lang>` was set by four hand-written copies of one `.replace(/<html…/)` and by nothing in the prerender, and adding a second attribute to that arrangement is how the next one reaches three paths out of five. There is one `applyDocumentAttrs` now, and the prerender is one of its callers — which fixes the locale as a side effect of having somewhere to put the zone.
|
|
126
|
+
|
|
127
|
+
### Internal (no consumer-facing effect)
|
|
128
|
+
|
|
129
|
+
- **@voltro/cli** — `voltro data backup --assets` / `restore --assets` are now driven against a REAL S3 API (MinIO in the test stack), over the network, with a real backup and a real restore into a second bucket and the bytes compared.
|
|
130
|
+
|
|
131
|
+
The asset half rests on one field: a provider must map "there is no object at that key" to `status === 404`, and nothing looser — a 403 from a rotated credential is also non-transient, and calling that "the object is gone" turns a recoverable outage into a backup that quietly contains nothing. That mapping was measured against memory, filesystem and database live, and against the s3/azure SDK error SHAPES constructed. A constructed shape is a claim about an SDK, not about a round trip: nothing in it exercises signing, path-style addressing, or what the SDK actually raises when a server answers `NoSuchKey`. Two deployments listed exactly this as the gap they could not close either.
|
|
132
|
+
|
|
133
|
+
Both directions are covered against the real server: a dangling reference is stepped over and reported, and a bad credential fails the capture rather than being read as a missing object.
|
|
134
|
+
|
|
135
|
+
The native dialect lane also stops being silent about mssql. It was absent from the array entirely — an absent lane and a covered one look identical from the outside — and it is now listed with a written reason for why it does not register here (`sqlpackage` is a separate Microsoft download on a .NET runtime, absent from `mcr.microsoft.com/mssql-tools`). It is registered rather than skipped-forever, because a skip present on every healthy run teaches readers to ignore skip lines; and an assertion fails if any lane drops out WITHOUT a written reason, or if a reason names a lane that is in fact running.
|
|
136
|
+
- **@voltro/cli, @voltro/plugin-storage** — Coverage for the data commands, at the level the defects actually live.
|
|
137
|
+
|
|
138
|
+
`dataDirectFlags.e2e.test.ts` drives every DIRECT-target flag of `voltro data export` / `import` through the real binary against a real sqlite database, and carries the same `DATA_FLAGS`-driven self-check the native suite has: a new direct flag has to be driven there or the file goes red. The api-only flags are listed explicitly with the reason they are not here, and that list is asserted against `DATA_FLAGS` so it cannot become a place to hide an untested flag.
|
|
139
|
+
|
|
140
|
+
`missingObjectIs404.test.ts` pins the contract the dangling-reference skip rests on: every provider maps "no object at that key" to `status === 404`, and nothing looser. Five providers, three of them live, s3 and azure through their SDK's real error shapes — which read DIFFERENT fields (`$metadata.httpStatusCode` vs a bare `statusCode`), so a mapping copied from one to the other would turn dangling references back into hard capture failures on that backend alone.
|
|
141
|
+
|
|
142
|
+
`codegenFeatureTables.integration.test.ts` measures the count a report was about: `voltro codegen` must carry the tables a `*.cron.tsx` contributes, which the entity walk cannot see. The structural guards beside it were TRUE while that count was wrong. `frameworkSourceTypo.integration.test.ts` measures what catches a misspelled `_voltro_*` source given that the type deliberately does not — `voltro check` reports it as a dangling source and exits 1, with a negative control so the check is not merely flagging every framework name.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## [0.50.1] — 2026-08-24
|
|
147
|
+
|
|
148
|
+
### Added
|
|
149
|
+
|
|
150
|
+
- **@voltro/cli** — A `0.51.0` upgrade note for the case 0.45.0's typed-`source:` note left open: a source that is COMPUTED but whose names are all known — a generic reader that takes its table from `input` or a registry. Both remedies the original offered are wrong there. There is no misspelled name to fix, and widening to `ReactivitySourceValue` (or casting) compiles instantly while taking that entire set of tables permanently out of the check the narrowing exists to provide.
|
|
151
|
+
|
|
152
|
+
The note carries the recipe that keeps the names — `as const satisfies`, with the key set recovered separately — and the trap under it: an ANNOTATION widens the keys back even when the literal carries `as const`, because it is checked against the literal and then replaces its type. Filed under an unreached version because codemod selection is `from < version <= to`: 0.45.0's note can never fire again for anyone already past it. Documented on the subscriptions page in both languages.
|
|
153
|
+
|
|
154
|
+
### Changed
|
|
155
|
+
|
|
156
|
+
- **@voltro/data-transfer** — The `mariadb-dump`-not-on-PATH hint now also warns that a MariaDB 12.x client requires TLS by default, so running it BY HAND against a server without TLS fails with `TLS/SSL error: SSL is required` (2026) and needs `--skip-ssl`. Our own invocation is unaffected. It is said here because this hint is what sends people to install that client package, and the next thing many of them do is run the tool by hand — where the failure reads like a broken install of the thing we just told them to install.
|
|
157
|
+
|
|
158
|
+
### Fixed
|
|
159
|
+
|
|
160
|
+
- **@voltro/cli, @voltro/mcp** — `voltro check`'s `rbac/unguarded-mutation` no longer fires on `internal: true` procedures. Those are in no rpc group and on no route, so the finding's own sentence — "any caller who can reach the rpc surface can invoke it" — named a surface that does not exist, and neither remedy it offered was available: a guard protects nothing there, and `openAccess` is refused outright on an internal descriptor. An app with 990 internal mutations got 990 warnings and a `FAILED` exit from the same tree on which `voltro doctor` reported zero procedures without an access decision, which made `check` unusable as the CI gate it exists to be.
|
|
161
|
+
|
|
162
|
+
The capability manifest now carries `internal: true` per procedure (present only when true), and the rule reads it through `isWireReachable` — the same predicate the boot access gate and all three rpc-group assemblies use, rather than a fourth spelling of the same flag. A manifest that does not carry the field is read as REACHABLE, so the rule stays loud on an api older than the field rather than going quiet on it.
|
|
163
|
+
- **@voltro/data-transfer, @voltro/cli** — `--assets` aborted the whole capture at the first blob reference the storage provider could not resolve. A dangling reference is a fact about the source data — a row pointing at an object that was deleted, or that never arrived because an earlier import ran without the flag — and no backup can put back bytes that are not there. Aborting made the flag unusable for exactly the deployment that needed it: 178 references, one resolvable, and the run stopped at the second.
|
|
164
|
+
|
|
165
|
+
The phase now records the key, steps over it, and the run reports how many were skipped. Only a genuine not-found (`status === 404`) is treated this way; a 403 from a rotated credential or a 5xx from a backend outage still fails the capture, because calling those "the object is gone" turns a recoverable outage into a backup that quietly contains nothing.
|
|
166
|
+
|
|
167
|
+
The same abort also cost the artefact its provenance: `voltro-backup-stamp.json` was written after the asset phase, so a backup taken WITH `--assets` had no stamp at all, and `restore` then greeted an artefact this tool had written minutes earlier as "an older/handmade backup. Cannot verify dialect or schema version." The stamp is now written on every path — it describes the dump, and the dump is on disk and correct by the time the assets run.
|
|
168
|
+
- **@voltro/cli** — `voltro doctor`'s eager-loaded-relation rule resolves nested relations against their PARENT's target table instead of against the query's roots, so every level is reported in one pass. It flattened the `.with({ … })` tree before resolving, which meant a level-2 relation stayed invisible until level 1's table had been added to `source:` — each fix revealed the next level, and a clean run after the first fix meant nothing. One app ran `doctor → fix → doctor` twice before the output stopped producing new findings.
|
|
169
|
+
|
|
170
|
+
The one case that is still genuinely under-reported is now stated instead of being silent: when the executor's base table is neither declared nor textually readable, nothing resolves, and doctor prints which relation names it could not check rather than reporting clean.
|
|
171
|
+
- **@voltro/data-transfer** — A native restore whose client exited before consuming its input took the whole CLI down with an unhandled `EPIPE`. `createReadStream(dump).pipe(child.stdin)` had no `error` listener, so Node threw on a write to a pipe with no reader:
|
|
172
|
+
|
|
173
|
+
Error: write EPIPE Emitted 'error' event on Socket instance at: at Socket.onerror (node:internal/streams/readable:1045:14)
|
|
174
|
+
|
|
175
|
+
What that cost is the point. The crash landed before `close`, so the step never resumed: the client's stderr — the one place the reason was written down — was discarded, the transfer row was never closed, the in-progress marker was never re-asserted, and the operator got a raw Node stack trace over a database that was now half replaced. A deployment met it on a 64 MB restore and could not diagnose it at all, because the only diagnosis had been thrown away.
|
|
176
|
+
|
|
177
|
+
Every stream in the step now carries a handler. `EPIPE` on the child's stdin is deliberately swallowed — a child closes the pipe by dying, and its exit code and stderr are the actual failure — while any other stream error is carried and reported. The failure line now always includes the tool's stderr (and says so explicitly when the tool printed none), plus how many bytes of the artefact had been fed before it stopped. A source error also ends the child's stdin, which turns a restore that used to hang forever on a vanished dump into one that fails, and a clean exit over a broken input stream is reported as a failure rather than as a truncated success.
|
|
178
|
+
- **@voltro/cli** — `voltro check`'s observed-vs-declared findings advised an op that does not exist. The recorder reports what the store did, and `store.upsert` is one call; a `target:` is `InsertTarget | UpdateTarget | DeleteTarget` and has no `upsert` member. So `observed/undeclared-write` said to add `{ table: '…', op: 'upsert' }` — a `TS2322`, from a line whose whole purpose is to be pasted. It now advises both halves, and the observed → declarable mapping is typed `Record<ObservedWriteOp, ReadonlyArray<TargetSpec['op']>>` so a new recorder op cannot ship without a declarable answer.
|
|
179
|
+
|
|
180
|
+
The remedy no longer produces a finding of its own either: declared targets are now collected per table as a SET. `new Map(targets.map(t => [t.table, t.op]))` kept only the last op, so the correct two-entry declaration read as "declares update" and the insert half came back as a `wrong-op`.
|
|
181
|
+
- **@voltro/data-transfer, @voltro/cli** — The in-progress restore marker did not survive a native restore, on the exact dialects that were documented as safe. The claim — postgres and the mysql family drop only the objects the dump names, so the marker row survives — had a correct argument and a wrong premise: a native dump names the WHOLE database, `_voltro_replace_in_progress` included, and a mysql-family restore writes `DROP TABLE IF EXISTS` in front of each table. The table sorts early, so the guard was removed near the START of the window it exists to cover. Measured downstream as one row before and zero after, twice, once by hand and once through the command — and then a restore died and left a database with a schema, some of its tables, no users and no blobs, with nothing to stop the next boot.
|
|
182
|
+
|
|
183
|
+
Two changes, covering different dumps. `voltro data backup` now excludes the marker table (`--exclude-table` / `--ignore-table`), so an artefact we produce cannot carry the thing that erases the guard on the way back in. And `restore` re-asserts the marker after the tool exits, on the failing path as well as the succeeding one, which covers dumps taken before this change and dumps made by hand: rewritten is a warning, and unwritable is an error, because the guard is then off for that run.
|
|
184
|
+
- **@voltro/plugin-storage, @voltro/cli** — `--assets` could not be refused when no storage was configured, and resolved the wrong backend when it was.
|
|
185
|
+
|
|
186
|
+
`resolveStorageProvider({})` is total: its final `default:` arm returns an in-process `memoryProvider`. So the three `if (!provider)` refusals in `voltro data backup` / `export` / `restore` — whose entire job is to refuse a flag the app cannot honour — were checking a condition that cannot hold. What they let through is worse than an unchecked flag: with no storage configured, `backup --assets` captured from a fresh memory provider, found nothing in it, and wrote an artefact stamped `assets: { count: 0 }` — a rollback story that says the blobs are in there.
|
|
187
|
+
|
|
188
|
+
It also ignored the provider the app REGISTERED. An app on `storagePlugin({ provider: s3(…) })` had its export/import/backup read and write the env-derived default instead, which surfaces later as "no object at key" — the exact drift `appStorageProvider()` exists to prevent, in call sites that never adopted it.
|
|
189
|
+
|
|
190
|
+
`configuredStorageProvider()` is the honest predicate: the app's registered provider, else the env-named one, else `undefined`. A memory provider nobody asked for is not a decision. The CLI now loads the app config BEFORE resolving — `storagePlugin(...)` registers at construction, so asking first answered with the default no matter what the app had configured.
|
|
191
|
+
|
|
192
|
+
### Internal (no consumer-facing effect)
|
|
193
|
+
|
|
194
|
+
- **@voltro/cli** — `voltro data backup` / `restore` / `clear-replace-marker` are now driven end to end, one case per declared option and one per refusal, as real CLI subprocesses against a real database.
|
|
195
|
+
|
|
196
|
+
Every defect this command has shipped lived in the SEQUENCE rather than in a function — an unhandled `EPIPE` that discarded the client's stderr before `close` fired, a marker erased by the restore's own artefact, an asset abort that skipped the provenance stamp, a refusal that could not fire — so none of them was reachable by testing a part. `nativeBackupRestore.e2e.test.ts` covers the whole surface against sqlite (a file copy: no vendor binary, runs anywhere) and `nativeBackupRestoreDialects.integration.test.ts` runs the vendor-tool half against postgres, mariadb and mysql, skipping BY NAME when the server or the client binary is absent — including when the client is present but too old for the server, which a "is it on PATH" check reports as ready.
|
|
197
|
+
|
|
198
|
+
The suite checks itself against `DATA_FLAGS`, the command's own declared option list, so an option cannot be added without a case that drives it.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
42
202
|
## [0.50.0] — 2026-08-23
|
|
43
203
|
|
|
44
204
|
### ⚠ BREAKING
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/timeZone.ts
|
|
2
|
+
var e = "viewer", t = "UTC", n = (e) => {
|
|
3
|
+
if (!e) return !1;
|
|
4
|
+
try {
|
|
5
|
+
return new Intl.DateTimeFormat("en-US", { timeZone: e }), !0;
|
|
6
|
+
} catch {
|
|
7
|
+
return !1;
|
|
8
|
+
}
|
|
9
|
+
}, r = "voltro:locale", i = "voltro:theme", a = "voltro:tz";
|
|
10
|
+
//#endregion
|
|
11
|
+
export { e as a, t as i, i as n, n as o, a as r, r as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ComponentProps } from 'react';
|
|
2
|
+
import { Context } from 'react';
|
|
2
3
|
import { FormattedMessage } from 'react-intl';
|
|
3
4
|
import { IntlConfig } from 'react-intl';
|
|
4
5
|
import { ReactNode } from 'react';
|
|
@@ -61,6 +62,10 @@ export declare const createTypedMessages: <C extends MessageCatalog>() => TypedM
|
|
|
61
62
|
|
|
62
63
|
export declare type DateInput = Date | number | string;
|
|
63
64
|
|
|
65
|
+
/** The zone used when nothing else resolves. Always valid, never surprising —
|
|
66
|
+
* and, unlike the process zone, the SAME on the server and in the browser. */
|
|
67
|
+
export declare const DEFAULT_TIME_ZONE = "UTC";
|
|
68
|
+
|
|
64
69
|
/**
|
|
65
70
|
* Identity function with type narrowing. Wrap your BASE catalog in
|
|
66
71
|
* this so other locales can `extends` its key set.
|
|
@@ -105,7 +110,7 @@ export declare interface I18nInstanceReport {
|
|
|
105
110
|
* never loaded the package (reports zeroes). */
|
|
106
111
|
export declare const i18nInstanceReport: () => I18nInstanceReport;
|
|
107
112
|
|
|
108
|
-
export declare const I18nProvider: ({ locale, messages, defaultLocale, silenceMissingTranslations, intlConfig, children, }: I18nProviderProps) => ReactNode;
|
|
113
|
+
export declare const I18nProvider: ({ locale, messages, defaultLocale, silenceMissingTranslations, timeZone, renderedAt, intlConfig, children, }: I18nProviderProps) => ReactNode;
|
|
109
114
|
|
|
110
115
|
declare interface I18nProviderProps {
|
|
111
116
|
readonly locale: string;
|
|
@@ -119,9 +124,41 @@ declare interface I18nProviderProps {
|
|
|
119
124
|
* rollout missing strings will fall back to the source-of-truth
|
|
120
125
|
* locale, no need to flood the console. */
|
|
121
126
|
readonly silenceMissingTranslations?: boolean;
|
|
122
|
-
/**
|
|
123
|
-
*
|
|
124
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The IANA zone every date/time formatter under this provider renders in.
|
|
129
|
+
*
|
|
130
|
+
* **Under SSR this is not a preference, it is the correctness knob.** Omit it
|
|
131
|
+
* and each side falls back to the zone of the runtime it happens to be in —
|
|
132
|
+
* the pod for the server (UTC on a container with no `TZ`), the viewer's
|
|
133
|
+
* machine for the browser. The two then disagree by an offset, the day can
|
|
134
|
+
* differ across midnight, and React reports it as a hydration mismatch.
|
|
135
|
+
*
|
|
136
|
+
* The framework resolves and passes this per request when the web app sets
|
|
137
|
+
* `timeZone` in `app.config.ts`; it also publishes the same value as
|
|
138
|
+
* `<html data-voltro-tz>` so the client's first render agrees by
|
|
139
|
+
* construction. Hand-built providers pass it themselves.
|
|
140
|
+
*
|
|
141
|
+
* An unusable zone (a stale cookie, a typo'd config, a runtime with a trimmed
|
|
142
|
+
* ICU) is DROPPED rather than forwarded — otherwise `Intl.DateTimeFormat`
|
|
143
|
+
* throws and every timestamp in the app degrades to a raw `Date` string.
|
|
144
|
+
*/
|
|
145
|
+
readonly timeZone?: string;
|
|
146
|
+
/**
|
|
147
|
+
* The instant this render reasons about, in epoch milliseconds — the server's
|
|
148
|
+
* render time, so that a relative formatter ("3 minutes ago") produces the
|
|
149
|
+
* SAME string on the server and in the hydration pass.
|
|
150
|
+
*
|
|
151
|
+
* Live again as soon as hydration commits, so a mounted page does not sit
|
|
152
|
+
* frozen at the server's clock. Omit it outside SSR: with no stamp the clock
|
|
153
|
+
* is simply live, which is what a client-only app wants and costs it no
|
|
154
|
+
* second render pass.
|
|
155
|
+
*/
|
|
156
|
+
readonly renderedAt?: number;
|
|
157
|
+
/** Forward any other IntlProvider prop unmodified (formats,
|
|
158
|
+
* textComponent, …). `timeZone` has its own prop above — it is
|
|
159
|
+
* read by this package's formatters as well as by react-intl, so
|
|
160
|
+
* it cannot be an opaque passthrough. */
|
|
161
|
+
readonly intlConfig?: Partial<Omit<IntlConfig, 'locale' | 'messages' | 'defaultLocale' | 'onError' | 'timeZone'>>;
|
|
125
162
|
readonly children: ReactNode;
|
|
126
163
|
}
|
|
127
164
|
|
|
@@ -143,6 +180,16 @@ export declare type ICUVars<S extends string> = string extends S ? never : S ext
|
|
|
143
180
|
* a plural/select BRANCH's text (`"# days total duration"`) is rejected. */
|
|
144
181
|
declare type IsICUIdentifier<S extends string> = S extends '' ? false : S extends `${infer C}${infer Rest}` ? C extends ICUIdentChar ? Rest extends '' ? true : IsICUIdentifier<Rest> : false : false;
|
|
145
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Is `name` an IANA zone THIS runtime can format in?
|
|
185
|
+
*
|
|
186
|
+
* Runtime-scoped on purpose: the question that matters is not whether the name
|
|
187
|
+
* is in the tzdata spec but whether `Intl.DateTimeFormat` here accepts it. A
|
|
188
|
+
* Node built with a trimmed ICU answers differently from a browser, and the
|
|
189
|
+
* one that has to render is the one that gets asked.
|
|
190
|
+
*/
|
|
191
|
+
export declare const isSupportedTimeZone: (name: string | null | undefined) => name is string;
|
|
192
|
+
|
|
146
193
|
export declare interface LazyCatalogs<L extends string = string> {
|
|
147
194
|
/** Locales this app declares, in declaration order. */
|
|
148
195
|
readonly locales: ReadonlyArray<L>;
|
|
@@ -206,21 +253,7 @@ export declare const LOCALE_COOKIE = "voltro:locale";
|
|
|
206
253
|
*/
|
|
207
254
|
export declare type LooseTFunction = (id: string, values?: Readonly<Record<string, MessageValue>>) => string;
|
|
208
255
|
|
|
209
|
-
|
|
210
|
-
* Build a children-to-tree wrapper that mounts `<I18nProvider>` with
|
|
211
|
-
* the supplied locale + catalog. The framework's SSG pipeline calls
|
|
212
|
-
* this once per build and passes the returned function as
|
|
213
|
-
* `renderPageToHtml({ outerWrap })`.
|
|
214
|
-
*
|
|
215
|
-
* For a multi-locale build the SSG emits per-locale HTML variants:
|
|
216
|
-
* `voltro build` calls this once per locale and, for each rendered
|
|
217
|
-
* page, picks `localeWraps.get(renderLocale)` — a `[locale]/…` route
|
|
218
|
-
* renders under its own `locale` param, a bare-path route under the
|
|
219
|
-
* `defaultLocale`. The client still swaps to the user's locale on
|
|
220
|
-
* hydration via the runtime's provider in `.framework/app.tsx`, but the
|
|
221
|
-
* emitted HTML is already locale-correct (right catalog + `meta({ locale })`).
|
|
222
|
-
*/
|
|
223
|
-
export declare const makeSsgWrap: (locale: string, messages: MessageCatalog, defaultLocale: string) => ((children: ReactNode) => ReactNode);
|
|
256
|
+
export declare const makeSsgWrap: (locale: string, messages: MessageCatalog, defaultLocale: string) => ((children: ReactNode, request?: SsgWrapRequest) => ReactNode);
|
|
224
257
|
|
|
225
258
|
/**
|
|
226
259
|
* The positional `values` a message key requires: none for a plain message, a
|
|
@@ -278,6 +311,42 @@ export declare type PluralForms = {
|
|
|
278
311
|
readonly other: string;
|
|
279
312
|
};
|
|
280
313
|
|
|
314
|
+
/**
|
|
315
|
+
* The instant the CURRENT render is reasoning about, in epoch milliseconds —
|
|
316
|
+
* or `undefined` for "the clock is live, read it yourself".
|
|
317
|
+
*
|
|
318
|
+
* `undefined` is the steady state. A number appears only between the server's
|
|
319
|
+
* render and the client's hydration commit, which is exactly the window in
|
|
320
|
+
* which the two sides must agree.
|
|
321
|
+
*/
|
|
322
|
+
export declare const RenderClockContext: Context<number | undefined>;
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Build a children-to-tree wrapper that mounts `<I18nProvider>` with
|
|
326
|
+
* the supplied locale + catalog. The framework's SSG pipeline calls
|
|
327
|
+
* this once per build and passes the returned function as
|
|
328
|
+
* `renderPageToHtml({ outerWrap })`.
|
|
329
|
+
*
|
|
330
|
+
* For a multi-locale build the SSG emits per-locale HTML variants:
|
|
331
|
+
* `voltro build` calls this once per locale and, for each rendered
|
|
332
|
+
* page, picks `localeWraps.get(renderLocale)` — a `[locale]/…` route
|
|
333
|
+
* renders under its own `locale` param, a bare-path route under the
|
|
334
|
+
* `defaultLocale`. The client still swaps to the user's locale on
|
|
335
|
+
* hydration via the runtime's provider in `.framework/app.tsx`, but the
|
|
336
|
+
* emitted HTML is already locale-correct (right catalog + `meta({ locale })`).
|
|
337
|
+
*/
|
|
338
|
+
/**
|
|
339
|
+
* Per-request facts the wrapper stamps onto the provider. Both vary per REQUEST
|
|
340
|
+
* while the wrapper itself is built once per LOCALE, which is why they arrive
|
|
341
|
+
* at call time rather than in the factory above.
|
|
342
|
+
*/
|
|
343
|
+
export declare interface SsgWrapRequest {
|
|
344
|
+
/** The IANA zone this request renders in — see `I18nProvider`'s `timeZone`. */
|
|
345
|
+
readonly timeZone?: string | undefined;
|
|
346
|
+
/** The server's render instant, so relative times survive hydration. */
|
|
347
|
+
readonly renderedAt?: number | undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
281
350
|
export declare const T: ({ id, ...rest }: TProps) => ReactNode;
|
|
282
351
|
|
|
283
352
|
/**
|
|
@@ -290,6 +359,28 @@ export declare const T: ({ id, ...rest }: TProps) => ReactNode;
|
|
|
290
359
|
*/
|
|
291
360
|
export declare const THEME_COOKIE = "voltro:theme";
|
|
292
361
|
|
|
362
|
+
/**
|
|
363
|
+
* The cookie carrying the IANA timezone to render in (`Europe/Berlin`).
|
|
364
|
+
*
|
|
365
|
+
* Read by `resolveTimeZone` when the app configures `timeZone: 'viewer'`, and
|
|
366
|
+
* therefore by every SSR path — it is what lets the SERVER render in the
|
|
367
|
+
* viewer's zone instead of the pod's. Two writers are expected and both are
|
|
368
|
+
* correct:
|
|
369
|
+
*
|
|
370
|
+
* - the framework's pre-paint script, which seeds it from
|
|
371
|
+
* `Intl.DateTimeFormat().resolvedOptions().timeZone` when it is absent, so
|
|
372
|
+
* the zone is known from the second request onward with no login; and
|
|
373
|
+
* - the APP, at login, overwriting it with the zone it holds for the signed-in
|
|
374
|
+
* user (a profile field, an identity provider's `timeZone` claim). That is
|
|
375
|
+
* the authoritative one — a user travelling with a laptop keeps the zone
|
|
376
|
+
* their account says, not the airport's.
|
|
377
|
+
*
|
|
378
|
+
* Absent means "no answer yet", never "UTC": the resolver falls through to the
|
|
379
|
+
* app's `defaultTimeZone` so the two sides still agree on a value neither of
|
|
380
|
+
* them guessed.
|
|
381
|
+
*/
|
|
382
|
+
export declare const TIMEZONE_COOKIE = "voltro:tz";
|
|
383
|
+
|
|
293
384
|
declare type TProps = Omit<ComponentProps<typeof FormattedMessage>, 'id'> & {
|
|
294
385
|
readonly id: string;
|
|
295
386
|
};
|
|
@@ -328,11 +419,20 @@ export declare type TypedTProps<C extends MessageCatalog> = Omit<ComponentProps<
|
|
|
328
419
|
export declare const useFormatCurrency: (currency: string) => ((value: number, options?: Intl.NumberFormatOptions) => string);
|
|
329
420
|
|
|
330
421
|
/**
|
|
331
|
-
* Locale-aware date/time formatting bound to the active locale
|
|
422
|
+
* Locale-aware date/time formatting bound to the active locale AND the active
|
|
423
|
+
* zone.
|
|
332
424
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
425
|
+
* The zone comes from the provider (`useTimeZone`), which the framework fills
|
|
426
|
+
* from `app.config.ts`'s `timeZone` — either a fixed IANA name for everyone, or
|
|
427
|
+
* `'viewer'` to resolve each request against the signed-in user's own zone.
|
|
428
|
+
* Passing `timeZone` in the options overrides it for one call, which is what a
|
|
429
|
+
* value that genuinely belongs to a fixed zone wants (a store's opening hours,
|
|
430
|
+
* a scheduled broadcast).
|
|
431
|
+
*
|
|
432
|
+
* **With no zone pinned anywhere, each runtime uses its own** — the pod's on the
|
|
433
|
+
* server, the viewer's in the browser. That is fine for a client-rendered page
|
|
434
|
+
* and is a hydration mismatch under SSR, because the two are not the same zone
|
|
435
|
+
* and across midnight not even the same day.
|
|
336
436
|
*/
|
|
337
437
|
export declare const useFormatDate: () => ((value: DateInput, options?: Intl.DateTimeFormatOptions) => string);
|
|
338
438
|
|
|
@@ -342,6 +442,9 @@ export declare const useFormatNumber: () => ((value: number, options?: Intl.Numb
|
|
|
342
442
|
/** All formatters at once, for components that need several. */
|
|
343
443
|
export declare const useFormatters: () => {
|
|
344
444
|
readonly locale: string;
|
|
445
|
+
/** The pinned IANA zone, or `undefined` when the ambient runtime zone is in
|
|
446
|
+
* use — see `useTimeZone`. */
|
|
447
|
+
readonly timeZone: string | undefined;
|
|
345
448
|
readonly formatDate: ReturnType<typeof useFormatDate>;
|
|
346
449
|
readonly relativeTime: ReturnType<typeof useRelativeTime>;
|
|
347
450
|
readonly formatNumber: ReturnType<typeof useFormatNumber>;
|
|
@@ -369,11 +472,26 @@ export declare const usePlural: () => ((count: number, forms: PluralForms, optio
|
|
|
369
472
|
* Picks the largest unit that fits, so a 90-minute delta reads "1 hour ago", not
|
|
370
473
|
* "90 minutes ago". Anything under a second is "now" (via the `second` unit at
|
|
371
474
|
* 0), which avoids the "0 seconds ago" flicker hand-rolled versions produce.
|
|
475
|
+
*
|
|
476
|
+
* **Hydration-safe by construction.** The base instant is the server's render
|
|
477
|
+
* time for the server render and the hydration pass that has to match it, then
|
|
478
|
+
* the live clock once hydration commits — so the string cannot change between
|
|
479
|
+
* the HTML and the first client render, and does not stay frozen afterwards.
|
|
480
|
+
* Pass `{ now }` to state the base yourself; that always wins.
|
|
372
481
|
*/
|
|
373
482
|
export declare const useRelativeTime: () => ((value: DateInput, options?: Intl.RelativeTimeFormatOptions & {
|
|
374
483
|
readonly now?: DateInput;
|
|
375
484
|
}) => string);
|
|
376
485
|
|
|
486
|
+
/**
|
|
487
|
+
* The pinned render instant, or `undefined` when the clock is live.
|
|
488
|
+
*
|
|
489
|
+
* Call sites treat `undefined` as "use `Date.now()`" rather than defaulting to
|
|
490
|
+
* one, so a component outside any provider behaves exactly as it did before
|
|
491
|
+
* this context existed.
|
|
492
|
+
*/
|
|
493
|
+
export declare const useRenderClock: () => number | undefined;
|
|
494
|
+
|
|
377
495
|
export declare const useT: (id: string, values?: Record<string, string | number | boolean | Date | null | undefined>) => string;
|
|
378
496
|
|
|
379
497
|
/**
|
|
@@ -392,10 +510,24 @@ export declare const useT: (id: string, values?: Record<string, string | number
|
|
|
392
510
|
*/
|
|
393
511
|
export declare const useTFn: () => (id: string, values?: Record<string, string | number | boolean | Date | null | undefined>) => string;
|
|
394
512
|
|
|
513
|
+
/**
|
|
514
|
+
* The IANA zone the active provider renders in, or `undefined` when none is
|
|
515
|
+
* pinned and the ambient runtime zone is in use.
|
|
516
|
+
*
|
|
517
|
+
* `undefined` is a real answer and worth branching on: it means the server and
|
|
518
|
+
* the browser are each using their OWN zone, so a server-rendered timestamp is
|
|
519
|
+
* not guaranteed to survive hydration. Set `timeZone` in the web
|
|
520
|
+
* `app.config.ts` to make it a value.
|
|
521
|
+
*/
|
|
522
|
+
export declare const useTimeZone: () => string | undefined;
|
|
523
|
+
|
|
395
524
|
/** A validated ICU arg name, or `never` when the candidate isn't a real
|
|
396
525
|
* identifier. This is the guard that keeps the type-level parse from emitting a
|
|
397
526
|
* plural/select branch's TEXT as a phantom required var — an ICU arg name can't
|
|
398
527
|
* contain spaces/`#`/`—`, so anything that does is dropped. */
|
|
399
528
|
declare type ValidICUName<S extends string> = IsICUIdentifier<S> extends true ? S : never;
|
|
400
529
|
|
|
530
|
+
/** The `app.config.ts` sentinel meaning "resolve per request from the viewer". */
|
|
531
|
+
export declare const VIEWER_TIME_ZONE = "viewer";
|
|
532
|
+
|
|
401
533
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { a as e, i as t, n, o as r, r as i, t as a } from "./cookieNames-CqdYWVpp.js";
|
|
2
|
+
import { createContext as o, createElement as s, useCallback as c, useContext as l, useEffect as u, useMemo as d, useState as f } from "react";
|
|
3
|
+
import { FormattedMessage as p, IntlProvider as m, useIntl as h } from "react-intl";
|
|
4
|
+
import { jsx as g } from "react/jsx-runtime";
|
|
5
5
|
//#region src/instanceRegistry.ts
|
|
6
|
-
var
|
|
7
|
-
let e = globalThis, t = e[
|
|
6
|
+
var _ = "__voltro_i18n_instances__", v = () => {
|
|
7
|
+
let e = globalThis, t = e[_];
|
|
8
8
|
if (t !== void 0) return t;
|
|
9
9
|
let n = { records: [] };
|
|
10
|
-
return e[
|
|
11
|
-
},
|
|
10
|
+
return e[_] = n, n;
|
|
11
|
+
}, y = () => {
|
|
12
12
|
try {
|
|
13
13
|
let e = String((/* @__PURE__ */ Error()).stack ?? "").split("\n");
|
|
14
14
|
for (let t of e.slice(3)) {
|
|
@@ -17,60 +17,71 @@ var d = "__voltro_i18n_instances__", f = () => {
|
|
|
17
17
|
}
|
|
18
18
|
} catch {}
|
|
19
19
|
return "unknown";
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
file:
|
|
20
|
+
}, b = (e) => {
|
|
21
|
+
v().records.push({
|
|
22
|
+
file: y(),
|
|
23
23
|
intlProvider: e
|
|
24
24
|
});
|
|
25
|
-
},
|
|
26
|
-
let e = globalThis[
|
|
25
|
+
}, x = () => {
|
|
26
|
+
let e = globalThis[_]?.records ?? [];
|
|
27
27
|
return {
|
|
28
28
|
instances: e.length,
|
|
29
29
|
reactIntlCopies: new Set(e.map((e) => e.intlProvider)).size,
|
|
30
30
|
files: [...new Set(e.map((e) => e.file))]
|
|
31
31
|
};
|
|
32
|
-
};
|
|
32
|
+
}, S = o(void 0), C = () => l(S);
|
|
33
33
|
//#endregion
|
|
34
34
|
//#region src/provider.tsx
|
|
35
|
-
m
|
|
36
|
-
var
|
|
37
|
-
let
|
|
35
|
+
b(m);
|
|
36
|
+
var w = ({ locale: e, messages: t, defaultLocale: n, silenceMissingTranslations: i = !0, timeZone: a, renderedAt: o, intlConfig: s, children: c }) => {
|
|
37
|
+
let l = i ? (e) => {
|
|
38
38
|
e.code !== "MISSING_TRANSLATION" && console.error(e);
|
|
39
|
-
} : void 0;
|
|
40
|
-
|
|
39
|
+
} : void 0, [d, p] = f(!1);
|
|
40
|
+
u(() => {
|
|
41
|
+
o !== void 0 && p(!0);
|
|
42
|
+
}, [o]);
|
|
43
|
+
let h = d ? void 0 : o, _ = r(a) ? a : void 0;
|
|
44
|
+
return /* @__PURE__ */ g(m, {
|
|
41
45
|
locale: e,
|
|
42
46
|
defaultLocale: n,
|
|
43
47
|
messages: t,
|
|
44
|
-
...
|
|
45
|
-
...
|
|
46
|
-
|
|
48
|
+
...l ? { onError: l } : {},
|
|
49
|
+
..._ ? { timeZone: _ } : {},
|
|
50
|
+
...s,
|
|
51
|
+
children: /* @__PURE__ */ g(S.Provider, {
|
|
52
|
+
value: h,
|
|
53
|
+
children: c
|
|
54
|
+
})
|
|
47
55
|
});
|
|
48
|
-
},
|
|
56
|
+
}, T = ({ id: e, ...t }) => /* @__PURE__ */ g(p, {
|
|
49
57
|
id: e,
|
|
50
58
|
...t
|
|
51
|
-
}),
|
|
52
|
-
let e =
|
|
53
|
-
return
|
|
54
|
-
},
|
|
59
|
+
}), E = (e, t) => h().formatMessage({ id: e }, t), D = () => h().locale, O = () => h().messages, k = () => {
|
|
60
|
+
let e = h();
|
|
61
|
+
return c((t, n) => e.formatMessage({ id: t }, n), [e]);
|
|
62
|
+
}, A = (e, t, n, r) => {
|
|
55
63
|
if (t === 0 && n.zero !== void 0) return n.zero.replace(/\{count\}/g, String(t));
|
|
56
64
|
let i = "other";
|
|
57
65
|
try {
|
|
58
66
|
i = new Intl.PluralRules(e, r).select(t);
|
|
59
67
|
} catch {}
|
|
60
68
|
return (n[i] ?? n.other).replace(/\{count\}/g, String(t));
|
|
61
|
-
},
|
|
62
|
-
let e =
|
|
63
|
-
return
|
|
64
|
-
},
|
|
65
|
-
let e =
|
|
66
|
-
return
|
|
69
|
+
}, j = () => {
|
|
70
|
+
let e = D();
|
|
71
|
+
return c((t, n, r) => A(e, t, n, r), [e]);
|
|
72
|
+
}, M = (e) => e instanceof Date ? e : new Date(e), N = () => h().timeZone, P = () => {
|
|
73
|
+
let e = D(), t = N();
|
|
74
|
+
return c((n, r) => {
|
|
67
75
|
try {
|
|
68
|
-
return new Intl.DateTimeFormat(e,
|
|
76
|
+
return new Intl.DateTimeFormat(e, {
|
|
77
|
+
...t ? { timeZone: t } : {},
|
|
78
|
+
...r
|
|
79
|
+
}).format(M(n));
|
|
69
80
|
} catch {
|
|
70
|
-
return String(
|
|
81
|
+
return String(n);
|
|
71
82
|
}
|
|
72
|
-
}, [e]);
|
|
73
|
-
},
|
|
83
|
+
}, [e, t]);
|
|
84
|
+
}, F = [
|
|
74
85
|
["year", 31536e6],
|
|
75
86
|
["month", 2592e6],
|
|
76
87
|
["week", 6048e5],
|
|
@@ -78,67 +89,69 @@ var g = ({ locale: e, messages: t, defaultLocale: n, silenceMissingTranslations:
|
|
|
78
89
|
["hour", 36e5],
|
|
79
90
|
["minute", 6e4],
|
|
80
91
|
["second", 1e3]
|
|
81
|
-
],
|
|
82
|
-
let e =
|
|
83
|
-
return
|
|
84
|
-
let { now:
|
|
92
|
+
], I = () => {
|
|
93
|
+
let e = D(), t = C();
|
|
94
|
+
return c((n, r) => {
|
|
95
|
+
let { now: i, ...a } = r ?? {}, o = M(n).getTime() - (i === void 0 ? t ?? Date.now() : M(i).getTime());
|
|
85
96
|
try {
|
|
86
97
|
let t = new Intl.RelativeTimeFormat(e, {
|
|
87
98
|
numeric: "auto",
|
|
88
|
-
...
|
|
99
|
+
...a
|
|
89
100
|
});
|
|
90
|
-
for (let [e, n] of
|
|
101
|
+
for (let [e, n] of F) if (Math.abs(o) >= n || e === "second") return t.format(Math.round(o / n), e);
|
|
91
102
|
return t.format(0, "second");
|
|
92
103
|
} catch {
|
|
93
|
-
return String(
|
|
104
|
+
return String(n);
|
|
94
105
|
}
|
|
95
|
-
}, [e]);
|
|
96
|
-
},
|
|
97
|
-
let e =
|
|
98
|
-
return
|
|
106
|
+
}, [e, t]);
|
|
107
|
+
}, L = () => {
|
|
108
|
+
let e = D();
|
|
109
|
+
return c((t, n) => {
|
|
99
110
|
try {
|
|
100
111
|
return new Intl.NumberFormat(e, n).format(t);
|
|
101
112
|
} catch {
|
|
102
113
|
return String(t);
|
|
103
114
|
}
|
|
104
115
|
}, [e]);
|
|
105
|
-
},
|
|
106
|
-
let t =
|
|
107
|
-
return
|
|
116
|
+
}, R = (e) => {
|
|
117
|
+
let t = L();
|
|
118
|
+
return c((n, r) => t(n, {
|
|
108
119
|
style: "currency",
|
|
109
120
|
currency: e,
|
|
110
121
|
...r
|
|
111
122
|
}), [t, e]);
|
|
112
|
-
},
|
|
113
|
-
let e =
|
|
114
|
-
return
|
|
123
|
+
}, z = () => {
|
|
124
|
+
let e = D(), t = N(), n = P(), r = I(), i = L(), a = j();
|
|
125
|
+
return d(() => ({
|
|
115
126
|
locale: e,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
timeZone: t,
|
|
128
|
+
formatDate: n,
|
|
129
|
+
relativeTime: r,
|
|
130
|
+
formatNumber: i,
|
|
131
|
+
plural: a
|
|
120
132
|
}), [
|
|
121
133
|
e,
|
|
122
134
|
t,
|
|
123
135
|
n,
|
|
124
136
|
r,
|
|
125
|
-
i
|
|
137
|
+
i,
|
|
138
|
+
a
|
|
126
139
|
]);
|
|
127
|
-
},
|
|
140
|
+
}, B = (e) => e, V = () => (e) => e, H = (e, t, n) => e[t ?? n] ?? e[n], U = /\{\s*([A-Za-z0-9_]+)/g, W = (e) => {
|
|
128
141
|
let t = /* @__PURE__ */ new Set();
|
|
129
|
-
for (let n of e.matchAll(
|
|
142
|
+
for (let n of e.matchAll(U)) t.add(n[1]);
|
|
130
143
|
return t;
|
|
131
|
-
},
|
|
144
|
+
}, G = (e, t) => {
|
|
132
145
|
let n = Object.keys(e);
|
|
133
146
|
if (n.length < 2) return;
|
|
134
147
|
let r = t?.baseLocale ?? n[0], i = e[r] ?? {}, a = [];
|
|
135
148
|
for (let [t, o] of Object.entries(i)) {
|
|
136
|
-
let i =
|
|
149
|
+
let i = W(o);
|
|
137
150
|
for (let o of n) {
|
|
138
151
|
if (o === r) continue;
|
|
139
152
|
let n = e[o]?.[t];
|
|
140
153
|
if (n === void 0) continue;
|
|
141
|
-
let s =
|
|
154
|
+
let s = W(n), c = [...i].filter((e) => !s.has(e)), l = [...s].filter((e) => !i.has(e));
|
|
142
155
|
if (c.length > 0 || l.length > 0) {
|
|
143
156
|
let e = [c.length > 0 ? `missing {${c.join("}, {")}}` : "", l.length > 0 ? `unexpected {${l.join("}, {")}}` : ""].filter(Boolean);
|
|
144
157
|
a.push(`${o} '${t}': ${e.join("; ")} (base ${r} has {${[...i].join("}, {")}})`);
|
|
@@ -152,21 +165,21 @@ var g = ({ locale: e, messages: t, defaultLocale: n, silenceMissingTranslations:
|
|
|
152
165
|
return;
|
|
153
166
|
}
|
|
154
167
|
throw Error(o);
|
|
155
|
-
},
|
|
168
|
+
}, K = (e) => {
|
|
156
169
|
let t = e;
|
|
157
170
|
return t.dynamic === void 0 && (t.dynamic = (t, n) => e(t, n)), t;
|
|
158
|
-
},
|
|
159
|
-
useT:
|
|
160
|
-
useTFn: (() =>
|
|
161
|
-
T
|
|
162
|
-
}),
|
|
171
|
+
}, q = () => ({
|
|
172
|
+
useT: K((e, t) => E(e, t)),
|
|
173
|
+
useTFn: (() => K(k())),
|
|
174
|
+
T
|
|
175
|
+
}), J = (e) => "default" in e && typeof e.default == "object" ? e.default : e, Y = (e, t) => {
|
|
163
176
|
let n = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), i = (n) => n && n in e ? n : t, a = (t) => {
|
|
164
177
|
let a = i(t), o = n.get(a);
|
|
165
178
|
if (o) return Promise.resolve(o);
|
|
166
179
|
let s = r.get(a);
|
|
167
180
|
if (s) return s;
|
|
168
181
|
let c = e[a], l = c().then((e) => {
|
|
169
|
-
let t =
|
|
182
|
+
let t = J(e);
|
|
170
183
|
return n.set(a, t), r.delete(a), t;
|
|
171
184
|
}).catch((e) => {
|
|
172
185
|
throw r.delete(a), e;
|
|
@@ -182,33 +195,35 @@ var g = ({ locale: e, messages: t, defaultLocale: n, silenceMissingTranslations:
|
|
|
182
195
|
a(e).catch(() => {});
|
|
183
196
|
}
|
|
184
197
|
};
|
|
185
|
-
},
|
|
186
|
-
let [
|
|
187
|
-
return
|
|
198
|
+
}, X = ({ catalogs: e, locale: t, fallback: n = null, silenceMissingTranslations: r, children: i }) => {
|
|
199
|
+
let [a, o] = f(() => e.peek(t));
|
|
200
|
+
return u(() => {
|
|
188
201
|
let n = e.peek(t);
|
|
189
202
|
if (n) {
|
|
190
|
-
|
|
203
|
+
o(n);
|
|
191
204
|
return;
|
|
192
205
|
}
|
|
193
206
|
let r = !0;
|
|
194
|
-
return
|
|
195
|
-
r &&
|
|
207
|
+
return o(void 0), e.load(t).then((e) => {
|
|
208
|
+
r && o(e);
|
|
196
209
|
}), () => {
|
|
197
210
|
r = !1;
|
|
198
211
|
};
|
|
199
|
-
}, [e, t]),
|
|
212
|
+
}, [e, t]), a ? /* @__PURE__ */ g(w, {
|
|
200
213
|
locale: t,
|
|
201
|
-
messages:
|
|
214
|
+
messages: a,
|
|
202
215
|
defaultLocale: e.defaultLocale,
|
|
203
216
|
...r === void 0 ? {} : { silenceMissingTranslations: r },
|
|
204
217
|
children: i
|
|
205
218
|
}) : n;
|
|
206
|
-
},
|
|
219
|
+
}, Z = (e, t, n) => (r, i) => s(w, {
|
|
207
220
|
locale: e,
|
|
208
221
|
messages: t,
|
|
209
222
|
defaultLocale: n,
|
|
210
223
|
silenceMissingTranslations: !0,
|
|
224
|
+
...i?.timeZone === void 0 ? {} : { timeZone: i.timeZone },
|
|
225
|
+
...i?.renderedAt === void 0 ? {} : { renderedAt: i.renderedAt },
|
|
211
226
|
children: r
|
|
212
227
|
});
|
|
213
228
|
//#endregion
|
|
214
|
-
export {
|
|
229
|
+
export { t as DEFAULT_TIME_ZONE, w as I18nProvider, a as LOCALE_COOKIE, X as LazyI18nProvider, S as RenderClockContext, T, n as THEME_COOKIE, i as TIMEZONE_COOKIE, e as VIEWER_TIME_ZONE, G as assertCatalogParity, q as createTypedMessages, B as defineCatalog, Y as defineCatalogs, V as defineLocale, x as i18nInstanceReport, r as isSupportedTimeZone, Z as makeSsgWrap, H as pickCatalog, A as plural, R as useFormatCurrency, P as useFormatDate, L as useFormatNumber, z as useFormatters, D as useLocale, O as useMessages, j as usePlural, I as useRelativeTime, C as useRenderClock, E as useT, k as useTFn, N as useTimeZone };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/** The zone used when nothing else resolves. Always valid, never surprising —
|
|
2
|
+
* and, unlike the process zone, the SAME on the server and in the browser. */
|
|
3
|
+
export declare const DEFAULT_TIME_ZONE = "UTC";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Is `name` an IANA zone THIS runtime can format in?
|
|
7
|
+
*
|
|
8
|
+
* Runtime-scoped on purpose: the question that matters is not whether the name
|
|
9
|
+
* is in the tzdata spec but whether `Intl.DateTimeFormat` here accepts it. A
|
|
10
|
+
* Node built with a trimmed ICU answers differently from a browser, and the
|
|
11
|
+
* one that has to render is the one that gets asked.
|
|
12
|
+
*/
|
|
13
|
+
export declare const isSupportedTimeZone: (name: string | null | undefined) => name is string;
|
|
14
|
+
|
|
1
15
|
/**
|
|
2
16
|
* Build the per-request resolver: cookie > Accept-Language > default, then the
|
|
3
17
|
* matching wrapper.
|
|
@@ -11,6 +25,13 @@ export declare const makeSsrI18nResolver: (args: SsrI18nResolverArgs) => ((cooki
|
|
|
11
25
|
/** What a renderer needs to render one request in the right locale. */
|
|
12
26
|
export declare interface ResolvedSsrI18n {
|
|
13
27
|
readonly locale: string;
|
|
28
|
+
/**
|
|
29
|
+
* The IANA zone this request renders in, or `undefined` when the app pins
|
|
30
|
+
* none. The renderer PUBLISHES it (`<html data-voltro-tz>`) so the client's
|
|
31
|
+
* first render formats against the same value the server did — the same
|
|
32
|
+
* contract `<html lang>` has for the locale.
|
|
33
|
+
*/
|
|
34
|
+
readonly timeZone: string | undefined;
|
|
14
35
|
/** Mounts `<I18nProvider>` around the tree — `renderPageToHtml`'s `outerWrap`. */
|
|
15
36
|
readonly outerWrap: (children: unknown) => unknown;
|
|
16
37
|
}
|
|
@@ -32,13 +53,67 @@ export declare interface ResolveLocaleArgs {
|
|
|
32
53
|
readonly defaultLocale: string;
|
|
33
54
|
}
|
|
34
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the IANA zone this request renders in, or `undefined` when the app
|
|
58
|
+
* pins none.
|
|
59
|
+
*
|
|
60
|
+
* Every candidate is validated before it is returned, so an unusable value
|
|
61
|
+
* (stale cookie naming a zone this runtime's ICU does not carry, a typo in
|
|
62
|
+
* `app.config.ts`) falls THROUGH to the next signal rather than reaching
|
|
63
|
+
* `Intl.DateTimeFormat` — which would throw and degrade every date in the app
|
|
64
|
+
* to a raw `Date` string. Same treatment `resolveLocale` gives a cookie naming
|
|
65
|
+
* a locale we no longer ship.
|
|
66
|
+
*/
|
|
67
|
+
export declare const resolveTimeZone: (args: ResolveTimeZoneArgs) => string | undefined;
|
|
68
|
+
|
|
69
|
+
export declare interface ResolveTimeZoneArgs {
|
|
70
|
+
/** Raw `Cookie:` request header, or null/undefined when absent. */
|
|
71
|
+
readonly cookieHeader?: string | null;
|
|
72
|
+
/**
|
|
73
|
+
* The web app's `timeZone` setting: an IANA name to pin one zone for every
|
|
74
|
+
* viewer, or `'viewer'` to resolve per request from the `voltro:tz` cookie.
|
|
75
|
+
* Absent means the app has not opted in — the result is `undefined` and each
|
|
76
|
+
* runtime keeps using its own zone, which is the pre-existing behaviour.
|
|
77
|
+
*/
|
|
78
|
+
readonly configured?: string | null;
|
|
79
|
+
/**
|
|
80
|
+
* The answer when `'viewer'` yields nothing: the very first request of a
|
|
81
|
+
* session (no cookie yet) and every prerendered page (one document, all
|
|
82
|
+
* viewers). Defaults to `UTC` — arbitrary, but identical on both sides, which
|
|
83
|
+
* is the property that matters.
|
|
84
|
+
*/
|
|
85
|
+
readonly defaultTimeZone?: string | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
35
88
|
export declare interface SsrI18nResolverArgs {
|
|
36
|
-
/**
|
|
37
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Per-locale `<I18nProvider>` wrappers, built by the caller with
|
|
91
|
+
* `makeSsgWrap`. The second parameter carries the per-REQUEST facts (zone,
|
|
92
|
+
* render instant); the wrapper itself is built once per LOCALE, which is why
|
|
93
|
+
* they cannot live in the factory.
|
|
94
|
+
*/
|
|
95
|
+
readonly wraps: Readonly<Record<string, (children: unknown, request?: {
|
|
96
|
+
readonly timeZone?: string | undefined;
|
|
97
|
+
readonly renderedAt?: number | undefined;
|
|
98
|
+
}) => unknown>>;
|
|
38
99
|
/** Locale codes the app ships catalogs for. */
|
|
39
100
|
readonly supported: ReadonlyArray<string>;
|
|
40
101
|
/** MUST be in `supported`. */
|
|
41
102
|
readonly defaultLocale: string;
|
|
103
|
+
/** The app's `timeZone` setting — an IANA name, or `'viewer'`. See
|
|
104
|
+
* `resolveTimeZone`. */
|
|
105
|
+
readonly timeZone?: string | null;
|
|
106
|
+
/** Fallback when `'viewer'` yields nothing. See `resolveTimeZone`. */
|
|
107
|
+
readonly defaultTimeZone?: string | null;
|
|
108
|
+
/**
|
|
109
|
+
* The render instant, injected for tests. Production leaves it unset and each
|
|
110
|
+
* request stamps its own `Date.now()` — the point of the stamp is that the
|
|
111
|
+
* CLIENT reuses the server's number, not that the number is fixed.
|
|
112
|
+
*/
|
|
113
|
+
readonly now?: () => number;
|
|
42
114
|
}
|
|
43
115
|
|
|
116
|
+
/** The `app.config.ts` sentinel meaning "resolve per request from the viewer". */
|
|
117
|
+
export declare const VIEWER_TIME_ZONE = "viewer";
|
|
118
|
+
|
|
44
119
|
export { }
|
package/dist/server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as e, i as t, o as n, r, t as i } from "./cookieNames-CqdYWVpp.js";
|
|
2
2
|
//#region src/server.ts
|
|
3
|
-
var
|
|
3
|
+
var a = i, o = (e) => RegExp(`(?:^|;\\s*)${e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;]*)`), s = (e, t) => {
|
|
4
4
|
if (!e) return;
|
|
5
|
-
let
|
|
6
|
-
return
|
|
7
|
-
},
|
|
5
|
+
let n = e.match(o(t));
|
|
6
|
+
return n ? decodeURIComponent(n[1]) : void 0;
|
|
7
|
+
}, c = (e, t) => {
|
|
8
8
|
let n = e.toLowerCase();
|
|
9
9
|
for (; n.length > 0;) {
|
|
10
10
|
let e = t.find((e) => e.toLowerCase() === n);
|
|
@@ -13,7 +13,7 @@ var t = e, n = (e) => RegExp(`(?:^|;\\s*)${e.replace(/[.*+?^${}()|[\]\\]/g, "\\$
|
|
|
13
13
|
if (r < 0) return;
|
|
14
14
|
n = n.slice(0, r);
|
|
15
15
|
}
|
|
16
|
-
},
|
|
16
|
+
}, l = (e) => e ? e.split(",").map((e) => {
|
|
17
17
|
let [t, ...n] = e.split(";").map((e) => e.trim());
|
|
18
18
|
if (!t || t === "*") return null;
|
|
19
19
|
let r = 1;
|
|
@@ -28,31 +28,45 @@ var t = e, n = (e) => RegExp(`(?:^|;\\s*)${e.replace(/[.*+?^${}()|[\]\\]/g, "\\$
|
|
|
28
28
|
tag: t,
|
|
29
29
|
q: r
|
|
30
30
|
};
|
|
31
|
-
}).filter((e) => e !== null).sort((e, t) => t.q - e.q) : [],
|
|
32
|
-
let { cookieHeader:
|
|
33
|
-
if (
|
|
34
|
-
let e =
|
|
31
|
+
}).filter((e) => e !== null).sort((e, t) => t.q - e.q) : [], u = (e) => {
|
|
32
|
+
let { cookieHeader: t, acceptLanguageHeader: n, supported: r, defaultLocale: i } = e, o = s(t, a);
|
|
33
|
+
if (o) {
|
|
34
|
+
let e = c(o, r);
|
|
35
35
|
if (e) return e;
|
|
36
36
|
}
|
|
37
|
-
for (let e of
|
|
38
|
-
let t =
|
|
37
|
+
for (let e of l(n)) {
|
|
38
|
+
let t = c(e.tag, r);
|
|
39
39
|
if (t) return t;
|
|
40
40
|
}
|
|
41
|
-
return
|
|
42
|
-
},
|
|
41
|
+
return i;
|
|
42
|
+
}, d = (e) => {
|
|
43
|
+
let { cookieHeader: t, configured: i, defaultTimeZone: a } = e;
|
|
44
|
+
if (!i) return;
|
|
45
|
+
if (i !== "viewer") return n(i) ? i : n(a) ? a : "UTC";
|
|
46
|
+
let o = s(t, r);
|
|
47
|
+
return n(o) ? o : n(a) ? a : "UTC";
|
|
48
|
+
}, f = (e) => {
|
|
43
49
|
let { wraps: t, supported: n, defaultLocale: r } = e;
|
|
44
|
-
return Object.keys(t).length === 0 ? () => void 0 : (
|
|
45
|
-
let
|
|
46
|
-
cookieHeader:
|
|
47
|
-
acceptLanguageHeader:
|
|
50
|
+
return Object.keys(t).length === 0 ? () => void 0 : (i, a) => {
|
|
51
|
+
let o = u({
|
|
52
|
+
cookieHeader: i ?? null,
|
|
53
|
+
acceptLanguageHeader: a ?? null,
|
|
48
54
|
supported: n,
|
|
49
55
|
defaultLocale: r
|
|
50
|
-
}), s =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
}), s = d({
|
|
57
|
+
cookieHeader: i ?? null,
|
|
58
|
+
configured: e.timeZone ?? null,
|
|
59
|
+
defaultTimeZone: e.defaultTimeZone ?? null
|
|
60
|
+
}), c = (e.now ?? Date.now)(), l = t[o] ?? t[r];
|
|
61
|
+
if (l !== void 0) return {
|
|
62
|
+
locale: o,
|
|
63
|
+
timeZone: s,
|
|
64
|
+
outerWrap: (e) => l(e, {
|
|
65
|
+
timeZone: s,
|
|
66
|
+
renderedAt: c
|
|
67
|
+
})
|
|
54
68
|
};
|
|
55
69
|
};
|
|
56
70
|
};
|
|
57
71
|
//#endregion
|
|
58
|
-
export {
|
|
72
|
+
export { t as DEFAULT_TIME_ZONE, e as VIEWER_TIME_ZONE, n as isSupportedTimeZone, f as makeSsrI18nResolver, u as resolveLocale, d as resolveTimeZone };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "Voltro's i18n layer — thin opinionated wrap over react-intl. Provides the framework's locale-resolution conventions (voltro:locale cookie + Accept-Language fallback), a typed message-catalog helper, and a slim provider component. Power users can import directly from react-intl for advanced APIs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|