@stacksjs/i18n 0.70.45 → 0.70.54
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/LICENSE.md +21 -0
- package/README.md +394 -0
- package/dist/bootstrap.d.ts +14 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -1
- package/dist/locale-switch.d.ts +16 -0
- package/package.json +2 -2
- package/src/bootstrap.ts +107 -0
- package/src/index.ts +13 -0
- package/src/locale-switch.ts +81 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Open Web Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Rapid App & Library Development
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.com/package/stacks)
|
|
6
|
+
[](https://github.com/stacksjs/stacks/actions?query=workflow%3Aci)
|
|
7
|
+
[](http://commitizen.github.io/cz-cli/)
|
|
8
|
+
[](https://npmjs.com/package/stacks)
|
|
9
|
+
<!-- [![Codecov][codecov-src]][codecov-href] -->
|
|
10
|
+
|
|
11
|
+
> [!NOTE]
|
|
12
|
+
> Stacks is in active development and usable today — every section below
|
|
13
|
+
> is real, runnable functionality. Expect occasional breaking changes
|
|
14
|
+
> while we cut the official 1.0. Feedback and issue reports are welcome.
|
|
15
|
+
|
|
16
|
+
Stacks is a rapid development framework, where the goal is to _help you_ create & maintain frontends, backends, and clouds—without having to worry about the boilerplate. _An all-in-one toolkit that meets all your full stack needs._
|
|
17
|
+
|
|
18
|
+
- Web & Desktop Applications _(including system tray apps)_
|
|
19
|
+
- Serverless & Traditional APIs
|
|
20
|
+
- Cloud Infrastructure Creation & Maintenance
|
|
21
|
+
- Interactive CLIs
|
|
22
|
+
- Framework-agnostic Component & Function Libraries
|
|
23
|
+
- Deployment & Release Manager _(CI & CD)_
|
|
24
|
+
|
|
25
|
+
## Convention Over Configuration
|
|
26
|
+
|
|
27
|
+
As a developer, Stacks helps you every step along the way—in beginner & expert-friendly ways, allowing you to focus on the _what & why_ of your project, all while enabling you to stay in control & ownership of your _(& your users’)_ data.
|
|
28
|
+
|
|
29
|
+
> “It is the framework’s responsibility to remove patterns that lead to boilerplate code. And Stacks is really good at that.” _- Chris_
|
|
30
|
+
|
|
31
|
+
<!--  -->
|
|
32
|
+
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
Stacks runs on [Bun](https://bun.sh). You'll need:
|
|
36
|
+
|
|
37
|
+
- **Bun ≥ 1.3.11** — install with `curl -fsSL https://bun.sh/install | bash`, or upgrade an existing install with `bun upgrade`.
|
|
38
|
+
- **macOS, Linux, or WSL** — Windows-native is on the roadmap; today the toolchain assumes a POSIX shell.
|
|
39
|
+
- **Node.js is NOT required.** Bun handles the JS/TS runtime, package management, and bundling.
|
|
40
|
+
|
|
41
|
+
That's it for the framework itself. Individual features (Postgres, Redis, AWS deploys, etc.) bring their own prereqs — each is called out where it matters in the docs.
|
|
42
|
+
|
|
43
|
+
## Get Started
|
|
44
|
+
|
|
45
|
+
The fastest path, assuming Bun is installed:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bunx buddy new my-project
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A one-line installer (`curl -Ssf stacksjs.com/install | sh`) is also planned and will appear here once it lands; for now the `bunx` form above is the supported install path.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
Stacks ships with `buddy`, a single CLI for everything you'll do day to day. The handful below covers the common workflows; the full reference is collapsed underneath.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
buddy dev # start the dev server (frontend, API, dashboard, …)
|
|
59
|
+
buddy build # build for production (CLI prompts for what to build)
|
|
60
|
+
buddy test # run tests
|
|
61
|
+
buddy migrate # run database migrations
|
|
62
|
+
buddy make:action UpdateUser # scaffold a new Action (also: model, view, job, …)
|
|
63
|
+
buddy --help # show every available command
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For the full command reference, see the collapsible section below or the [Buddy CLI documentation](https://stacksjs.org/docs/cli).
|
|
67
|
+
|
|
68
|
+
<details>
|
|
69
|
+
<summary>View the complete Buddy Toolkit</summary>
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
buddy --version # get the Stacks version
|
|
73
|
+
buddy --help # view help menu
|
|
74
|
+
# please note: you may suffix any command with the
|
|
75
|
+
# `command --help` flag to review the help menu
|
|
76
|
+
|
|
77
|
+
buddy install # installs dependencies
|
|
78
|
+
buddy add # adds a stack or dependency
|
|
79
|
+
buddy fresh # fresh reinstall of all deps
|
|
80
|
+
buddy clean # removes all deps
|
|
81
|
+
buddy setup # sets up the project initially
|
|
82
|
+
buddy setup:oh-my-zsh # optional: sets up Oh My Zsh with auto-completions & "aliases"
|
|
83
|
+
|
|
84
|
+
buddy upgrade # upgrades all dependencies
|
|
85
|
+
buddy upgrade -i # prompts you to select which updates to apply (wip)
|
|
86
|
+
buddy upgrade:dependencies # auto-upgrades package.json deps
|
|
87
|
+
buddy upgrade:framework # auto-upgrades deps & the Stacks framework
|
|
88
|
+
buddy upgrade:search-engine # auto-upgrades configured search engine
|
|
89
|
+
buddy upgrade:shell # upgrades the shell integration
|
|
90
|
+
buddy upgrade:binary # upgrades the `stacks` binary
|
|
91
|
+
buddy upgrade:bun # upgrades to latest project-defined Bun version
|
|
92
|
+
buddy upgrade:all # auto-upgrades all of the above
|
|
93
|
+
|
|
94
|
+
# if you need any more info on any command listed here, you may suffix
|
|
95
|
+
# any of them via the "help option", i.e. `buddy ... --help`
|
|
96
|
+
|
|
97
|
+
buddy dev # starts the frontend dev server
|
|
98
|
+
buddy dev -i # prompts any of the dev servers (components, functions, views, docs, or api)
|
|
99
|
+
buddy dev:api # starts the API dev server
|
|
100
|
+
buddy dev:dashboard # starts the Admin/Dashboard dev server
|
|
101
|
+
buddy dev:desktop # starts the Desktop dev server
|
|
102
|
+
buddy dev:views # starts frontend dev server
|
|
103
|
+
buddy dev:components # starts component dev server
|
|
104
|
+
buddy dev:functions # stubs functions
|
|
105
|
+
buddy dev:docs # starts local docs dev server
|
|
106
|
+
buddy dev docs # also starts the local docs dev server (colon is optional for all commands)
|
|
107
|
+
buddy development # `buddy dev` alias
|
|
108
|
+
|
|
109
|
+
buddy share # creates a sharable link to your local project
|
|
110
|
+
|
|
111
|
+
# for Laravel folks, `serve` may ring more familiar than the `dev` name. Hence, we aliased it
|
|
112
|
+
buddy serve
|
|
113
|
+
buddy serve:components
|
|
114
|
+
buddy serve:desktop
|
|
115
|
+
buddy serve:views
|
|
116
|
+
buddy serve:functions
|
|
117
|
+
buddy serve:docs
|
|
118
|
+
|
|
119
|
+
# building for production (e.g. AWS, Google Cloud, npm, Vercel, Netlify, et al.)
|
|
120
|
+
buddy build # select a specific build (follow CLI prompts)
|
|
121
|
+
buddy build:views # builds SSG views
|
|
122
|
+
buddy build:desktop # builds Desktop application
|
|
123
|
+
buddy build:library # builds any or all libraries
|
|
124
|
+
buddy build:functions # builds function library
|
|
125
|
+
buddy build:components # builds Vue component library & Web Component library
|
|
126
|
+
buddy build:web-components # builds framework agnostic Web Component library (i.e. Custom Elements)
|
|
127
|
+
buddy build:all # builds all your code
|
|
128
|
+
|
|
129
|
+
# `buddy build` aliases
|
|
130
|
+
buddy prod
|
|
131
|
+
buddy prod:components
|
|
132
|
+
buddy prod:desktop
|
|
133
|
+
buddy prod:library
|
|
134
|
+
buddy prod:views
|
|
135
|
+
buddy prod:functions
|
|
136
|
+
buddy prod:web-components
|
|
137
|
+
buddy prod:all
|
|
138
|
+
buddy production # `buddy prod` alias
|
|
139
|
+
|
|
140
|
+
# sets your application key
|
|
141
|
+
buddy key:generate
|
|
142
|
+
|
|
143
|
+
buddy make:component HelloWorld # bootstraps a HelloWorld component
|
|
144
|
+
buddy make:function hello-world # bootstraps a hello-world function
|
|
145
|
+
buddy make:view hello-world # bootstraps a hello-word page
|
|
146
|
+
buddy make:model Car # bootstraps a Car model
|
|
147
|
+
buddy make:database cars # creates a cars database
|
|
148
|
+
buddy make:migration create_cars_table # creates a cars migration file
|
|
149
|
+
buddy make:factory cars # creates a Car factory file
|
|
150
|
+
buddy make:table cars # bootstraps a cars data table
|
|
151
|
+
buddy make:notification welcome-email # bootstraps a welcome-email notification
|
|
152
|
+
buddy make:lang de # bootstraps a lang/de.yml language file
|
|
153
|
+
buddy make:stack my-project # shares logic with `bunx --bun stacks new my-project`
|
|
154
|
+
|
|
155
|
+
buddy migrate # runs database migrations
|
|
156
|
+
buddy migrate:dns # sets the ./config/dns.ts file
|
|
157
|
+
|
|
158
|
+
buddy dns example.com # list all DNS records for example.com
|
|
159
|
+
buddy dns example.com --type MX # list MX records for example.com
|
|
160
|
+
|
|
161
|
+
buddy https example.com/api/hello
|
|
162
|
+
# http [flags] [METHOD] URL [ITEM [ITEM]]
|
|
163
|
+
buddy http --help
|
|
164
|
+
buddy http PUT example.com/api/put X-API-Token:123 name=John # Custom HTTP method, HTTP headers and JSON data
|
|
165
|
+
buddy http -v example.com/api/get # See the request that is being sent using one of the output options
|
|
166
|
+
buddy http POST example.com/api/post hello=World # submitting forms
|
|
167
|
+
buddy http -a USERNAME POST https://api.github.com/repos/user/repo/issues/83/comments body='Great feature! :heart:'
|
|
168
|
+
buddy http localhost:8000 Host:example.com
|
|
169
|
+
|
|
170
|
+
buddy lint # runs linter
|
|
171
|
+
buddy lint:fix # runs linter and fixes issues
|
|
172
|
+
|
|
173
|
+
buddy commit # follow CLI prompts for committing staged changes
|
|
174
|
+
buddy release # creates the releases for the stack & triggers the Release Action (workflow)
|
|
175
|
+
buddy changelog # generates CHANGELOG.md
|
|
176
|
+
|
|
177
|
+
# when deploying your app/s to a remote server or cloud provider
|
|
178
|
+
buddy deploy # select a specific deployment (follow CLI prompts)
|
|
179
|
+
# buddy deploy:docs # deploys docs to AWS (or other configured provider)
|
|
180
|
+
# buddy deploy:functions # deploys functions to AWS (or other configured provider)
|
|
181
|
+
# buddy deploy:views # deploys views to AWS (or other configured provider)
|
|
182
|
+
# buddy deploy:all # deploys all your code
|
|
183
|
+
buddy undeploy # be careful: "undeploys" removes/deletes your deployed resources
|
|
184
|
+
|
|
185
|
+
buddy cloud:remove # removes cloud setup
|
|
186
|
+
buddy cloud:cleanup # removes cloud setup & cleans up all potentially leftover resources
|
|
187
|
+
buddy cloud:add --jump-box # adds a jump box to your cloud setup
|
|
188
|
+
|
|
189
|
+
# select the example to run (follow CLI prompts)
|
|
190
|
+
buddy example # prompts you to select which example to run
|
|
191
|
+
buddy example:vue # runs the Vue example
|
|
192
|
+
buddy example:web-components # runs the Web Component example
|
|
193
|
+
|
|
194
|
+
# you likely won’t need to run these commands as they are auto-triggered, but they are available
|
|
195
|
+
buddy generate # prompts you to select which generator to run
|
|
196
|
+
buddy generate:types # generates types for your components, functions, & views
|
|
197
|
+
buddy generate:entries # generates entry files for components, functions, & views
|
|
198
|
+
buddy generate:web-types # generates Web Component types
|
|
199
|
+
buddy generate:vscode-custom-data # generates VSCode custom data
|
|
200
|
+
buddy generate:ide-helpers # generates IDE helpers
|
|
201
|
+
buddy generate:component-meta # generates component meta
|
|
202
|
+
buddy generate:all # runs all generators
|
|
203
|
+
|
|
204
|
+
# generates your application key
|
|
205
|
+
buddy key:generate # generates your application key
|
|
206
|
+
|
|
207
|
+
# manage your environment variables
|
|
208
|
+
buddy env:get # get an environment variable
|
|
209
|
+
buddy env:set # set an environment variable
|
|
210
|
+
buddy env:encrypt # encrypt an environment variable
|
|
211
|
+
buddy env:decrypt # decrypt an environment variable
|
|
212
|
+
buddy env:keypair # generate a keypair
|
|
213
|
+
buddy env:rotate # rotate a keypair
|
|
214
|
+
|
|
215
|
+
# generate your TypeScript declarations
|
|
216
|
+
buddy types:generate # generates types for your components, functions, & views
|
|
217
|
+
buddy types:fix # auto-fixes types for your components, functions, & views
|
|
218
|
+
|
|
219
|
+
buddy domains # alias for `buddy domains:list`
|
|
220
|
+
buddy domains:add stacksjs.com # adds a domain
|
|
221
|
+
buddy domains:remove stacksjs.com # removes a domain
|
|
222
|
+
buddy domains:list # lists all domains
|
|
223
|
+
buddy domains:update # apply ./config/dns.ts updates
|
|
224
|
+
buddy domains:purchase stacksjs.com # purchase a new domain
|
|
225
|
+
|
|
226
|
+
# test your stack
|
|
227
|
+
buddy test # runs test suite (unit & e2e)
|
|
228
|
+
buddy test:coverage # runs test coverage
|
|
229
|
+
buddy test:types # runs typecheck
|
|
230
|
+
|
|
231
|
+
# the CLI may be triggered in any
|
|
232
|
+
# of the following syntax
|
|
233
|
+
stx fresh
|
|
234
|
+
buddy fresh
|
|
235
|
+
bud fresh
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
</details>
|
|
239
|
+
|
|
240
|
+
Read more about the Buddy CLI in the [official docs](https://stacksjs.org/docs/cli) — every command, every flag, every prompt explained.
|
|
241
|
+
|
|
242
|
+
## Features
|
|
243
|
+
|
|
244
|
+
The Stacks framework is a harmony of several “engines” to build any web and/or desktop application, in highly scalable & privacy-friendly ways. It consists of the following engines:
|
|
245
|
+
|
|
246
|
+
### Frontend Development
|
|
247
|
+
|
|
248
|
+
_Develop dynamic UIs with helpers for atomic design, and much more._
|
|
249
|
+
|
|
250
|
+
- 🧩 **Components** _primitive to develop user interfaces_
|
|
251
|
+
- 🤖 **Functions** _primitive to develop business logic (and grant your UI superpowers)_
|
|
252
|
+
- 🎨 **UI Kit** _modern & deeply-integrated components_
|
|
253
|
+
- 🌐 **Web** _“a routing & templating engine that makes sense”_
|
|
254
|
+
- 🖥️ **Desktop** _transforms your web app into a desktop app, plus more_
|
|
255
|
+
- 📝 **Documentation** _markdown-based documentation, auto-generated_
|
|
256
|
+
- 📚 **Library** _auto-builds & manages component & function libraries_
|
|
257
|
+
- ⚡️ Powered by Bun, Craft, Headwind
|
|
258
|
+
|
|
259
|
+
### Backend Development
|
|
260
|
+
|
|
261
|
+
_Develop serverless (or server) functions with countless helpers to build scalable & fast APIs._
|
|
262
|
+
|
|
263
|
+
- 🪄 **AI** _deep AI integrations to simplify building agentic workflow_
|
|
264
|
+
- 🤖 **APIs** _scalability & maintainability built-in_
|
|
265
|
+
- 🏎️ **Cache** _unified caching for DynamoDB, Redis and more_
|
|
266
|
+
- ⚙️ **CLIs** _create beautiful CLIs for Linux, Windows, and Mac (dependency-free binaries)_
|
|
267
|
+
- 🛍️ **Commerce** _own & grow your own online business with ease (SaaS-optimized)_
|
|
268
|
+
- 📀 **Database** _DynamoDB, SQLite, MySQL, Postgres, and more_
|
|
269
|
+
- 👾 **Errors** _native type-safe error handling_
|
|
270
|
+
- 🗓️ **Events** _functional event (front & backend) communication_
|
|
271
|
+
- 📢 **Notifications** _emails, SMSs, direct, and push notifications & webhooks_
|
|
272
|
+
- 🗺️ **ORM** _automated schemas for scale & a pretty API_
|
|
273
|
+
- 💳 **Payments** _unified API for one-off & subscription billing methods for Stripe_
|
|
274
|
+
- ⚙️ **Queues** _run any heavy workload in the background_
|
|
275
|
+
- 🛠️ **Query Builder** _powerful, type-safe SQL query builder_
|
|
276
|
+
- 💬 **Realtime** _“everything you need to build dynamic real-time apps”_
|
|
277
|
+
- 🧭 **Router** _smart routing, file-based or Laravel-like_
|
|
278
|
+
- 🔎 **Search Engine** _smart searching, advanced filtering & sorting, pagination, headless UI_
|
|
279
|
+
- 💾 **Storage** _a secure-by-default File API that feels right_
|
|
280
|
+
- 🧪 **Tinker** _a powerful TypeScript REPL_
|
|
281
|
+
- 🌪️ **Validation** _e2e type-safety (true frontend & backend harmony)_
|
|
282
|
+
- 🎯 **X-Ray** _all you need to debug, log & analyze_
|
|
283
|
+
|
|
284
|
+
### Cloud Development
|
|
285
|
+
|
|
286
|
+
_Develop & maintain cloud infrastructure with ease. “Imagine Vercel, Vapor and Forge having been unified.”_
|
|
287
|
+
|
|
288
|
+
- ☁️ **Server** _local development server & production-ready servers out-of-the-box_
|
|
289
|
+
- ⛅️ **Serverless** _on-demand, auto-scaling, zero maintenance_
|
|
290
|
+
- ⏰ **Alarms** _built-in cloud infrastructure monitoring to avoid surprises_
|
|
291
|
+
- 🚏 **CDN** _zero-config, low-latency, request life-cycle hooks, optimized request compressions (Brotli & gzip)_
|
|
292
|
+
- 🔀 **Domain** _version-controlled & zero-config domain management (e.g. DNS management)_
|
|
293
|
+
- 🤖 **AI** _fine-tune a foundational model using your application data_
|
|
294
|
+
- 📧 **Email** _secure & zero-setup <easy-peasy@custom-domains.com> mailboxes_
|
|
295
|
+
- 🔐 **Firewall** _native web application firewall support_
|
|
296
|
+
- 📦 **Storage** _unlimited cloud storage & automatic backups_
|
|
297
|
+
- 🚜 **Maintenance** _maintain your cloud infrastructure with ease using Buddy & Stacks_
|
|
298
|
+
- 🚦 **Infrastructure as Code** _version-controlled cloud infrastructure (AWS, Google next?)_
|
|
299
|
+
|
|
300
|
+
### CI/CD
|
|
301
|
+
|
|
302
|
+
_Focus on coding, not publishing._
|
|
303
|
+
|
|
304
|
+
- 🚀 **Deployment Manager** _takes the sweat out of production deployments—zero-setup push-to-deploy_
|
|
305
|
+
- 0️⃣ **Zero Downtime** _deploy with confidence using a zero-downtime deployment strategy_
|
|
306
|
+
- 📫 **Release Manager** _libraries (component & function) auto-published to npm, git helpers, and more_
|
|
307
|
+
|
|
308
|
+
### Developer Experience (DX)
|
|
309
|
+
|
|
310
|
+
Convention over configuration, while staying wholly configurable. _No more boilerplate._
|
|
311
|
+
|
|
312
|
+
- 💎 **Automated Upgrades** _no need to worry about upgrading to the latest versions, Stacks upgrades you_
|
|
313
|
+
- 🦋 **Pretty Dev URLs** _your-project.localhost instead of localhost:3000_
|
|
314
|
+
- 💡 **IDE Integration** _auto-completions, inline docs & a powerful IDE setup_
|
|
315
|
+
- 🪄 **Zero-Config** _yet highly configurable—convention over configuration_
|
|
316
|
+
- 💅 **Linter & Formatter** _auto-configured & built into your IDE_
|
|
317
|
+
- 💪🏼 **Type Strong** _built-in e2e type-safety_
|
|
318
|
+
- ✨ **Git Workflows** _committing with ease_
|
|
319
|
+
- 🚗 **Auto Imports** _your components & functions, including date, string, array, & object helpers_
|
|
320
|
+
- ⏩ **Code Snippets** _goodbye to the boilerplate code—thank you Sarah Drasner_
|
|
321
|
+
- 🔤 **Spell Checker** _be notified once there are typos_
|
|
322
|
+
- 🛠️ **Essential Utilities** _powers at your fingertips. Collections, VueUse, and more_
|
|
323
|
+
- 👥 **Team Management** _manage your team & their permissions_
|
|
324
|
+
- 🧪 **Streamlined Testing** _unit & e2e tests powered by Bun, Vitest & Playwright_
|
|
325
|
+
|
|
326
|
+
No matter whether you are a beginner or an expert, the approachable Stacks design allows you to learn at your own pace, using our thorough documentation covering every aspect of the framework. Stacks is extremely beginner & expert-friendly.
|
|
327
|
+
|
|
328
|
+
Develop beautiful, reactive, composable UIs without learning a new set of languages. HTML, CSS, and minimal JavaScript—that’s all you need to dive in now! _Or TypeScript ✌🏼_
|
|
329
|
+
|
|
330
|
+
> _An actual rapid application development framework for all Full Stack needs. Next-level simplicity & DX._
|
|
331
|
+
|
|
332
|
+
## Testing
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
./buddy test
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Changelog
|
|
339
|
+
|
|
340
|
+
Please see our [releases](https://github.com/stacksjs/stacks/releases) page for more information on what has changed recently.
|
|
341
|
+
|
|
342
|
+
## Contributing
|
|
343
|
+
|
|
344
|
+
Please see the [Contributing Guide](https://github.com/stacksjs/contributing) for details.
|
|
345
|
+
|
|
346
|
+
## Community
|
|
347
|
+
|
|
348
|
+
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
|
349
|
+
|
|
350
|
+
[Discussions on GitHub](https://github.com/stacksjs/stacks/discussions)
|
|
351
|
+
|
|
352
|
+
For casual chit-chat with others using this package:
|
|
353
|
+
|
|
354
|
+
[Join the Stacks Discord Server](https://discord.gg/stacksjs)
|
|
355
|
+
|
|
356
|
+
## Postcardware
|
|
357
|
+
|
|
358
|
+
“Software that is free, but hopes for a postcard.” We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.
|
|
359
|
+
|
|
360
|
+
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
|
|
361
|
+
|
|
362
|
+
## Sponsors
|
|
363
|
+
|
|
364
|
+
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
|
|
365
|
+
|
|
366
|
+
- [JetBrains](https://www.jetbrains.com/)
|
|
367
|
+
- [The Solana Foundation](https://solana.com/)
|
|
368
|
+
|
|
369
|
+
## Credits
|
|
370
|
+
|
|
371
|
+
- [Laravel](https://laravel.com) _many thanks to their community_
|
|
372
|
+
- [Chris Breuer](https://github.com/chrisbbreuer)
|
|
373
|
+
- [All Contributors](../../contributors)
|
|
374
|
+
|
|
375
|
+
And a special thanks to [Dan Scanlon](https://twitter.com/danscan) for donating the `stacks` name on npm ✨
|
|
376
|
+
|
|
377
|
+
## License
|
|
378
|
+
|
|
379
|
+
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
|
|
380
|
+
|
|
381
|
+
Made with 💙
|
|
382
|
+
|
|
383
|
+
<!-- Badges -->
|
|
384
|
+
[npm-version-src]: https://img.shields.io/npm/v/stacks?style=flat-square
|
|
385
|
+
[npm-version-href]: https://npmjs.com/package/stacks
|
|
386
|
+
|
|
387
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/stacks?style=flat-square
|
|
388
|
+
[npm-downloads-href]: https://npmjs.com/package/stacks
|
|
389
|
+
|
|
390
|
+
[github-actions-src]: https://img.shields.io/github/actions/workflow/status/stacksjs/stacks/ci.yml?style=flat-square&branch=main
|
|
391
|
+
[github-actions-href]: https://github.com/stacksjs/stacks/actions?query=workflow%3Aci
|
|
392
|
+
|
|
393
|
+
<!-- [codecov-src]: https://img.shields.io/codecov/c/gh/stacksjs/stacks/main?style=flat-square
|
|
394
|
+
[codecov-href]: https://codecov.io/gh/stacksjs/buddy -->
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load translation files from the project `locales/` directory once.
|
|
3
|
+
* Locale subdirectories (`locales/de.yml`, `locales/en.yml`, …) are detected
|
|
4
|
+
* automatically by `loadFromDirectory`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ensureLocalesLoaded(): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the active locale for an HTTP request.
|
|
9
|
+
* Order: STX locale prefix (`/en/...`), `?locale=`, `locale` cookie,
|
|
10
|
+
* Accept-Language, then `config.app.locale`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveRequestLocale(request?: Request): string;
|
|
13
|
+
/** Load `locales/` and activate the global translator for this request. */
|
|
14
|
+
export declare function applyRequestLocale(request?: Request): Promise<string>;
|
package/dist/index.d.ts
CHANGED
|
@@ -70,6 +70,17 @@ export {
|
|
|
70
70
|
createI18n,
|
|
71
71
|
useI18n,
|
|
72
72
|
} from './translator';
|
|
73
|
+
export {
|
|
74
|
+
ensureLocalesLoaded,
|
|
75
|
+
resolveRequestLocale,
|
|
76
|
+
applyRequestLocale,
|
|
77
|
+
} from './bootstrap';
|
|
78
|
+
export {
|
|
79
|
+
createLocaleSwitchResponse,
|
|
80
|
+
localizePath,
|
|
81
|
+
stripLocalePrefix,
|
|
82
|
+
type LocaleSwitchConfig,
|
|
83
|
+
} from './locale-switch';
|
|
73
84
|
export {
|
|
74
85
|
formatDate,
|
|
75
86
|
formatTime,
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var i=import.meta.require;function c(q,B){return r(B)(q)}function r(q){let B=q.split("-")[0].toLowerCase(),G=T[B];if(G)return G;return()=>"other"}function C(q,B="|"){let G=q.split(B).map((J)=>J.trim()),K=new Map;if(G.length===1)K.set("other",G[0]);else if(G.length===2)K.set("one",G[0]),K.set("other",G[1]);else if(G.length===3)K.set("zero",G[0]),K.set("one",G[1]),K.set("other",G[2]);else if(G.length>=4){let J=["zero","one","two","few","many","other"];for(let Q=0;Q<G.length&&Q<J.length;Q++)K.set(J[Q],G[Q])}return K}function O(q,B,G){let K=c(B,G);if(q.has(K))return q.get(K);if(q.has("other"))return q.get("other");let J=q.values().next();return J.done?"":J.value}var T={en:(q)=>q===1?"one":"other",de:(q)=>q===1?"one":"other",nl:(q)=>q===1?"one":"other",es:(q)=>q===1?"one":"other",it:(q)=>q===1?"one":"other",pt:(q)=>q===1?"one":"other",sv:(q)=>q===1?"one":"other",da:(q)=>q===1?"one":"other",no:(q)=>q===1?"one":"other",fi:(q)=>q===1?"one":"other",el:(q)=>q===1?"one":"other",he:(q)=>q===1?"one":"other",hu:(q)=>q===1?"one":"other",tr:(q)=>q===1?"one":"other",zh:()=>"other",ja:()=>"other",ko:()=>"other",vi:()=>"other",th:()=>"other",id:()=>"other",ms:()=>"other",fr:(q)=>q===0||q===1?"one":"other",ru:(q)=>{let B=q%10,G=q%100;if(B===1&&G!==11)return"one";if(B>=2&&B<=4&&(G<12||G>14))return"few";if(B===0||B>=5&&B<=9||G>=11&&G<=14)return"many";return"other"},uk:(q)=>{let B=q%10,G=q%100;if(B===1&&G!==11)return"one";if(B>=2&&B<=4&&(G<12||G>14))return"few";if(B===0||B>=5&&B<=9||G>=11&&G<=14)return"many";return"other"},pl:(q)=>{let B=q%10,G=q%100;if(q===1)return"one";if(B>=2&&B<=4&&(G<12||G>14))return"few";if(B===0||B===1||B>=5&&B<=9||G>=12&&G<=14)return"many";return"other"},cs:(q)=>{if(q===1)return"one";if(q>=2&&q<=4)return"few";return"other"},sk:(q)=>{if(q===1)return"one";if(q>=2&&q<=4)return"few";return"other"},ar:(q)=>{if(q===0)return"zero";if(q===1)return"one";if(q===2)return"two";let B=q%100;if(B>=3&&B<=10)return"few";if(B>=11&&B<=99)return"many";return"other"},cy:(q)=>{if(q===0)return"zero";if(q===1)return"one";if(q===2)return"two";if(q===3)return"few";if(q===6)return"many";return"other"},sl:(q)=>{let B=q%100;if(B===1)return"one";if(B===2)return"two";if(B===3||B===4)return"few";return"other"},ga:(q)=>{if(q===1)return"one";if(q===2)return"two";if(q>=3&&q<=6)return"few";if(q>=7&&q<=10)return"many";return"other"},lt:(q)=>{let B=q%10,G=q%100;if(B===1&&(G<11||G>19))return"one";if(B>=2&&B<=9&&(G<11||G>19))return"few";return"other"},lv:(q)=>{let B=q%10,G=q%100;if(q===0)return"zero";if(B===1&&G!==11)return"one";return"other"},ro:(q)=>{if(q===1)return"one";let B=q%100;if(q===0||B>=1&&B<=19)return"few";return"other"},hi:(q)=>q===0||q===1?"one":"other"};function Rq(){return Object.keys(T)}function Tq(q){return q.split("-")[0].toLowerCase()in T}function Fq(q,B){let G=q.split("-")[0].toLowerCase();T[G]=B}var v={locale:"en",fallbackLocale:"en",availableLocales:["en"],messages:{},warnMissing:!0,escapeValues:!1,keySeparator:".",pluralSeparator:"|"},Y="en",z="en",w={},H={...v};class j{_locale;_fallbackLocale;_messages;_config;constructor(q={}){this._config={...v,...q},this._locale=this._config.locale,this._fallbackLocale=this._config.fallbackLocale,this._messages=this._config.messages||{}}get locale(){return this._locale}set locale(q){this._locale=q}get fallbackLocale(){return this._fallbackLocale}get availableLocales(){return Object.keys(this._messages)}t=(q,B,G)=>{return this.translate(q,B,G)};tc=(q,B,G,K)=>{return this.translatePlural(q,B,G,K)};te=(q,B)=>{return this.hasTranslation(q,B)};tm=(q,B)=>{return this.getMessage(q,B)};setLocale=(q)=>{this._locale=q};addTranslations=(q,B)=>{if(!this._messages[q])this._messages[q]={};this._messages[q]=V(this._messages[q],B)};d=(q,B)=>{let G=typeof q==="number"?new Date(q):q,K=this._config.dateTimeFormats?.[this._locale]?.[B||"short"]||{};return new Intl.DateTimeFormat(this._locale,K).format(G)};n=(q,B)=>{let G=this._config.numberFormats?.[this._locale]?.[B||"decimal"]||{};return new Intl.NumberFormat(this._locale,G).format(q)};translate(q,B,G){let K=G||this._locale,J=this.getMessage(q,K);if(J===void 0){let Q=K.indexOf("-");if(Q>0){let X=K.slice(0,Q);if(X!==this._fallbackLocale)J=this.getMessage(q,X)}}if(J===void 0&&K!==this._fallbackLocale)J=this.getMessage(q,this._fallbackLocale);if(J===void 0){if(this._config.warnMissing)console.warn(`[i18n] Missing translation: "${q}" for locale "${K}"`);if(this._config.missingHandler){let Q=this._config.missingHandler(K,q);if(Q!==void 0)return Q}return q}if(typeof J!=="string")return q;return this.interpolate(J,B)}translatePlural(q,B,G,K){let J=K||this._locale,Q=this.getMessage(q,J);if(Q===void 0&&J!==this._fallbackLocale)Q=this.getMessage(q,this._fallbackLocale);if(Q===void 0||typeof Q!=="string"){if(this._config.warnMissing)console.warn(`[i18n] Missing plural translation: "${q}" for locale "${J}"`);return q}let X=C(Q,this._config.pluralSeparator),U=O(X,B,J),Z={count:B,n:B,...G};return this.interpolate(U,Z)}hasTranslation(q,B){let G=B||this._locale;return this.getMessage(q,G)!==void 0}getMessage(q,B){let G=this._messages[B];if(!G)return;let K=q.split(this._config.keySeparator||"."),J=G;for(let Q of K){if(J===void 0||typeof J==="string")return;J=J[Q]}return J}interpolate(q,B){if(!B)return q;return q.replace(/\{(\w+)\}/g,(G,K)=>{let J=B[K];if(J===void 0||J===null)return G;let Q=String(J);return this._config.escapeValues?qq(Q):Q})}}function n(q){Y=q}function _(){return Y}function Vq(q){z=q}function s(){return Object.keys(w)}function F(q,B){if(!w[q])w[q]={};w[q]=V(w[q],B)}function N(q){for(let[B,G]of Object.entries(q))F(B,G)}function L(q,B){return I(q,B||Y)!==void 0}function I(q,B){let G=w[B];if(!G)return;let K=q.split(H.keySeparator||"."),J=G;for(let Q of K){if(J===void 0||typeof J==="string")return;J=J[Q]}return J}function k(q,B){if(!B)return q;return q.replace(/\{(\w+)\}/g,(G,K)=>{let J=B[K];if(J===void 0||J===null)return G;return String(J)})}function f(q,B,G){let K=G||Y,J=I(q,K);if(J===void 0&&K!==z)J=I(q,z);if(J===void 0){if(H.warnMissing)console.warn(`[i18n] Missing translation: "${q}" for locale "${K}"`);return q}if(typeof J!=="string")return q;return k(J,B)}var t=f;function o(q,B,G,K){let J=K||Y,Q=I(q,J);if(Q===void 0&&J!==z)Q=I(q,z);if(Q===void 0||typeof Q!=="string")return q;let X=C(Q,H.pluralSeparator),U=O(X,B,J),Z={count:B,n:B,...G};return k(U,Z)}function l(q,B){return L(q,B)}function a(q,B){return I(q,B||Y)}function y(q={}){return new j(q)}function e(q={}){return y(q)}function Sq(q){if(H={...H,...q},q.locale)Y=q.locale;if(q.fallbackLocale)z=q.fallbackLocale;if(q.messages)N(q.messages)}function V(q,B){let G={...q};for(let K of Object.keys(B)){let J=B[K],Q=G[K];if(typeof J==="object"&&J!==null&&typeof Q==="object"&&Q!==null)G[K]=V(Q,J);else G[K]=J}return G}function qq(q){let B={"&":"&","<":"<",">":">",'"':""","'":"'"};return q.replace(/[&<>"']/g,(G)=>B[G]||G)}function Bq(q,B="medium",G){let K=G||_(),J=R(q),Q=typeof B==="string"?{dateStyle:B}:B;return new Intl.DateTimeFormat(K,Q).format(J)}function Gq(q,B="short",G){let K=G||_(),J=R(q),Q=typeof B==="string"?{timeStyle:B}:B;return new Intl.DateTimeFormat(K,Q).format(J)}function Jq(q,B="medium",G="short",K){let J=K||_(),Q=R(q);return new Intl.DateTimeFormat(J,{dateStyle:B,timeStyle:G}).format(Q)}function Kq(q,B=new Date,G="long",K){let J=K||_(),Q=R(q),X=R(B),U=Q.getTime()-X.getTime(),Z=Math.round(U/1000),$=Math.round(Z/60),A=Math.round($/60),E=Math.round(A/24),b=Math.round(E/7),h=Math.round(E/30),u=Math.round(E/365),W=new Intl.RelativeTimeFormat(J,{style:G});if(Math.abs(Z)<60)return W.format(Z,"second");if(Math.abs($)<60)return W.format($,"minute");if(Math.abs(A)<24)return W.format(A,"hour");if(Math.abs(E)<7)return W.format(E,"day");if(Math.abs(b)<4)return W.format(b,"week");if(Math.abs(h)<12)return W.format(h,"month");return W.format(u,"year")}function Qq(q,B={},G){let K=G||_(),J={...B};if(B.compact)J.notation="compact",J.compactDisplay="short";if(B.precision!==void 0)J.minimumFractionDigits=B.precision,J.maximumFractionDigits=B.precision;return new Intl.NumberFormat(K,J).format(q)}function Uq(q,B="USD",G={},K){let J=K||_();return new Intl.NumberFormat(J,{style:"currency",currency:B,...G}).format(q)}function Xq(q,B={},G){let K=G||_();return new Intl.NumberFormat(K,{style:"percent",...B}).format(q)}function Mq(q,B,G="short",K){let J=K||_();return new Intl.NumberFormat(J,{style:"unit",unit:B,unitDisplay:G}).format(q)}function Dq(q,B=2,G){let K=G||_();if(q===0)return"0 B";let J=1024,Q=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],X=Math.floor(Math.log(q)/Math.log(J)),U=q/Math.pow(J,X);return`${new Intl.NumberFormat(K,{minimumFractionDigits:0,maximumFractionDigits:B}).format(U)} ${Q[X]}`}function Zq(q,B="conjunction",G="long",K){let J=K||_();return new Intl.ListFormat(J,{type:B,style:G}).format(q)}function p(q,B){let G=B||_();return new Intl.PluralRules(G).select(q)}function bq(q,B,G){let K=p(q,G);return B[K]??B.other}function S(q,B="language",G="long",K){let J=K||_();try{return new Intl.DisplayNames(J,{type:B,style:G}).of(q)}catch{return}}function hq(q,B){return S(q,"language","long",B)}function vq(q,B){return S(q,"region","long",B)}function Lq(q,B){return S(q,"currency","long",B)}function kq(q,B,G={},K){let J=K||_();return new Intl.Collator(J,G).compare(q,B)}function fq(q,B={},G){let K=G||_(),J=new Intl.Collator(K,B);return[...q].sort((Q,X)=>J.compare(Q,X))}function R(q){if(q instanceof Date)return q;if(typeof q==="number")return new Date(q);return new Date(q)}function _q(q){let G=(q||_()).split("-")[0].toLowerCase();return["ar","he","fa","ur","yi","ps","sd","ug","ku","ckb"].includes(G)?"rtl":"ltr"}function yq(q){return _q(q)==="rtl"}import{existsSync as $q,readdirSync as g}from"fs";import{readFile as Aq}from"fs/promises";import{basename as d,extname as P,join as x}from"path";import{log as Eq}from"@stacksjs/logging";async function Wq(q){let{directory:B,extensions:G=[".json",".yaml",".yml",".ts",".js"],recursive:K=!1,namespaceSeparator:J="."}=q,Q={};if(!$q(B))return Eq.warn(`[i18n] Translation directory not found: ${B}`),Q;return await m(B,Q,G,K,J,""),N(Q),Q}async function m(q,B,G,K,J,Q){let X=g(q,{withFileTypes:!0});for(let U of X){let Z=x(q,U.name);if(U.isDirectory()){let $=U.name;if(Iq($))await Yq(Z,$,B,G);else if(K){let A=Q?`${Q}${J}${U.name}`:U.name;await m(Z,B,G,K,J,A)}}else if(U.isFile()){let $=P(U.name);if(G.includes($)){let A=d(U.name,$);if(!B[A])B[A]={};let E=await M(Z);if(Q)Hq(B[A],Q,E,J);else B[A]=D(B[A],E)}}}}async function Yq(q,B,G,K){if(!G[B])G[B]={};let J=g(q,{withFileTypes:!0});for(let Q of J){if(!Q.isFile())continue;let X=P(Q.name);if(!K.includes(X))continue;let U=x(q,Q.name),Z=d(Q.name,X),$=await M(U);if(Z===B)G[B]=D(G[B],$);else G[B][Z]=$}}async function M(q){let B=P(q).toLowerCase(),G=await Aq(q,"utf8");switch(B){case".json":try{return JSON.parse(G)}catch{throw Error(`Invalid JSON in translation file: ${q}`)}case".yaml":case".yml":return zq(G);case".ts":case".js":{let K=await import(q);return K.default||K}default:throw Error(`Unsupported file type: ${B}`)}}async function wq(q,B){let G=await M(B);return F(q,G),G}function zq(q){try{let B=Bun.YAML.parse(q);if(!B||typeof B!=="object"||Array.isArray(B))throw Error("YAML content must be an object at the root level");return B}catch(B){let G=B instanceof Error?B.message:"Unknown YAML parse error";throw Error(`Invalid YAML translation file: ${G}`)}}function Iq(q){return/^[a-z]{2}(?:-[A-Z]{2})?(?:-[A-Za-z]+)?$/i.test(q)}function Hq(q,B,G,K){let J=B.split(K),Q=q;for(let U=0;U<J.length-1;U++){let Z=J[U];if(!Q[Z]||typeof Q[Z]==="string")Q[Z]={};Q=Q[Z]}let X=J[J.length-1];Q[X]=G}function D(q,B){let G={...q};for(let K of Object.keys(B)){let J=B[K],Q=G[K];if(typeof J==="object"&&J!==null&&typeof Q==="object"&&Q!==null)G[K]=D(Q,J);else G[K]=J}return G}function cq(q,B={}){return{load:()=>Wq({directory:q,...B}),loadLocale:(G,K)=>wq(G,x(q,K))}}async function rq(q){let{loadTranslations:B}=await import("@stacksjs/ts-i18n"),G=await B(q);return N(G),G}var sq={en:{code:"en",name:"English",nativeName:"English",direction:"ltr"},"en-US":{code:"en-US",name:"English (US)",nativeName:"English (US)",direction:"ltr",region:"US"},"en-GB":{code:"en-GB",name:"English (UK)",nativeName:"English (UK)",direction:"ltr",region:"GB"},es:{code:"es",name:"Spanish",nativeName:"Espa\xF1ol",direction:"ltr"},fr:{code:"fr",name:"French",nativeName:"Fran\xE7ais",direction:"ltr"},de:{code:"de",name:"German",nativeName:"Deutsch",direction:"ltr"},it:{code:"it",name:"Italian",nativeName:"Italiano",direction:"ltr"},pt:{code:"pt",name:"Portuguese",nativeName:"Portugu\xEAs",direction:"ltr"},"pt-BR":{code:"pt-BR",name:"Portuguese (Brazil)",nativeName:"Portugu\xEAs (Brasil)",direction:"ltr",region:"BR"},zh:{code:"zh",name:"Chinese",nativeName:"\u4E2D\u6587",direction:"ltr"},"zh-CN":{code:"zh-CN",name:"Chinese (Simplified)",nativeName:"\u7B80\u4F53\u4E2D\u6587",direction:"ltr",region:"CN"},"zh-TW":{code:"zh-TW",name:"Chinese (Traditional)",nativeName:"\u7E41\u9AD4\u4E2D\u6587",direction:"ltr",region:"TW"},ja:{code:"ja",name:"Japanese",nativeName:"\u65E5\u672C\u8A9E",direction:"ltr"},ko:{code:"ko",name:"Korean",nativeName:"\uD55C\uAD6D\uC5B4",direction:"ltr"},ar:{code:"ar",name:"Arabic",nativeName:"\u0627\u0644\u0639\u0631\u0628\u064A\u0629",direction:"rtl"},he:{code:"he",name:"Hebrew",nativeName:"\u05E2\u05D1\u05E8\u05D9\u05EA",direction:"rtl"},ru:{code:"ru",name:"Russian",nativeName:"\u0420\u0443\u0441\u0441\u043A\u0438\u0439",direction:"ltr"},nl:{code:"nl",name:"Dutch",nativeName:"Nederlands",direction:"ltr"},pl:{code:"pl",name:"Polish",nativeName:"Polski",direction:"ltr"},tr:{code:"tr",name:"Turkish",nativeName:"T\xFCrk\xE7e",direction:"ltr"},vi:{code:"vi",name:"Vietnamese",nativeName:"Ti\u1EBFng Vi\u1EC7t",direction:"ltr"},th:{code:"th",name:"Thai",nativeName:"\u0E44\u0E17\u0E22",direction:"ltr"},hi:{code:"hi",name:"Hindi",nativeName:"\u0939\u093F\u0928\u094D\u0926\u0940",direction:"ltr"}};import{loadTranslations as JB,generateTypes as KB,generateTypesFromModule as QB,generateSampleConfig as UB,writeOutputs as XB,createTranslator as ZB}from"@stacksjs/ts-i18n";export{XB as writeI18nOutputs,e as useI18n,t as trans,a as tm,l as te,o as tc,f as t,fq as sortStrings,n as setLocale,Vq as setFallbackLocale,O as selectPluralForm,bq as selectPlural,C as parsePluralForms,JB as loadTranslationsFromDisk,N as loadTranslations,wq as loadLocale,rq as loadFromTsI18n,Wq as loadFromDirectory,M as loadFile,yq as isRTL,L as hasTranslation,Tq as hasPluralRule,_q as getTextDirection,Rq as getSupportedPluralLocales,vq as getRegionName,r as getPluralRule,c as getPluralCategory,_ as getLocale,hq as getLanguageName,S as getDisplayName,Lq as getCurrencyName,s as getAvailableLocales,QB as generateI18nTypesFromModule,KB as generateI18nTypes,UB as generateI18nSampleConfig,Mq as formatUnit,Gq as formatTime,Kq as formatRelativeTime,p as formatPlural,Xq as formatPercent,Qq as formatNumber,Zq as formatList,Jq as formatDateTime,Bq as formatDate,Uq as formatCurrency,Dq as formatBytes,ZB as createSimpleTranslator,cq as createLoader,y as createI18n,Sq as configure,kq as compareStrings,F as addTranslations,Fq as addPluralRule,sq as LOCALE_INFO,j as I18n};
|
|
2
|
+
var GG=import.meta.require;function JG(G,J){return KG(J)(G)}function KG(G){let J=G.split("-")[0].toLowerCase(),K=j[J];if(K)return K;return()=>"other"}function M(G,J="|"){let K=G.split(J).map((U)=>U.trim()),Z=new Map;if(K.length===1)Z.set("other",K[0]);else if(K.length===2)Z.set("one",K[0]),Z.set("other",K[1]);else if(K.length===3)Z.set("zero",K[0]),Z.set("one",K[1]),Z.set("other",K[2]);else if(K.length>=4){let U=["zero","one","two","few","many","other"];for(let _=0;_<K.length&&_<U.length;_++)Z.set(U[_],K[_])}return Z}function S(G,J,K){let Z=JG(J,K);if(G.has(Z))return G.get(Z);if(G.has("other"))return G.get("other");let U=G.values().next();return U.done?"":U.value}var j={en:(G)=>G===1?"one":"other",de:(G)=>G===1?"one":"other",nl:(G)=>G===1?"one":"other",es:(G)=>G===1?"one":"other",it:(G)=>G===1?"one":"other",pt:(G)=>G===1?"one":"other",sv:(G)=>G===1?"one":"other",da:(G)=>G===1?"one":"other",no:(G)=>G===1?"one":"other",fi:(G)=>G===1?"one":"other",el:(G)=>G===1?"one":"other",he:(G)=>G===1?"one":"other",hu:(G)=>G===1?"one":"other",tr:(G)=>G===1?"one":"other",zh:()=>"other",ja:()=>"other",ko:()=>"other",vi:()=>"other",th:()=>"other",id:()=>"other",ms:()=>"other",fr:(G)=>G===0||G===1?"one":"other",ru:(G)=>{let J=G%10,K=G%100;if(J===1&&K!==11)return"one";if(J>=2&&J<=4&&(K<12||K>14))return"few";if(J===0||J>=5&&J<=9||K>=11&&K<=14)return"many";return"other"},uk:(G)=>{let J=G%10,K=G%100;if(J===1&&K!==11)return"one";if(J>=2&&J<=4&&(K<12||K>14))return"few";if(J===0||J>=5&&J<=9||K>=11&&K<=14)return"many";return"other"},pl:(G)=>{let J=G%10,K=G%100;if(G===1)return"one";if(J>=2&&J<=4&&(K<12||K>14))return"few";if(J===0||J===1||J>=5&&J<=9||K>=12&&K<=14)return"many";return"other"},cs:(G)=>{if(G===1)return"one";if(G>=2&&G<=4)return"few";return"other"},sk:(G)=>{if(G===1)return"one";if(G>=2&&G<=4)return"few";return"other"},ar:(G)=>{if(G===0)return"zero";if(G===1)return"one";if(G===2)return"two";let J=G%100;if(J>=3&&J<=10)return"few";if(J>=11&&J<=99)return"many";return"other"},cy:(G)=>{if(G===0)return"zero";if(G===1)return"one";if(G===2)return"two";if(G===3)return"few";if(G===6)return"many";return"other"},sl:(G)=>{let J=G%100;if(J===1)return"one";if(J===2)return"two";if(J===3||J===4)return"few";return"other"},ga:(G)=>{if(G===1)return"one";if(G===2)return"two";if(G>=3&&G<=6)return"few";if(G>=7&&G<=10)return"many";return"other"},lt:(G)=>{let J=G%10,K=G%100;if(J===1&&(K<11||K>19))return"one";if(J>=2&&J<=9&&(K<11||K>19))return"few";return"other"},lv:(G)=>{let J=G%10,K=G%100;if(G===0)return"zero";if(J===1&&K!==11)return"one";return"other"},ro:(G)=>{if(G===1)return"one";let J=G%100;if(G===0||J>=1&&J<=19)return"few";return"other"},hi:(G)=>G===0||G===1?"one":"other"};function bG(){return Object.keys(j)}function vG(G){return G.split("-")[0].toLowerCase()in j}function hG(G,J){let K=G.split("-")[0].toLowerCase();j[K]=J}var g={locale:"en",fallbackLocale:"en",availableLocales:["en"],messages:{},warnMissing:!0,escapeValues:!1,keySeparator:".",pluralSeparator:"|"},H="en",R="en",N={},z={...g};class D{_locale;_fallbackLocale;_messages;_config;constructor(G={}){this._config={...g,...G},this._locale=this._config.locale,this._fallbackLocale=this._config.fallbackLocale,this._messages=this._config.messages||{}}get locale(){return this._locale}set locale(G){this._locale=G}get fallbackLocale(){return this._fallbackLocale}get availableLocales(){return Object.keys(this._messages)}t=(G,J,K)=>{return this.translate(G,J,K)};tc=(G,J,K,Z)=>{return this.translatePlural(G,J,K,Z)};te=(G,J)=>{return this.hasTranslation(G,J)};tm=(G,J)=>{return this.getMessage(G,J)};setLocale=(G)=>{this._locale=G};addTranslations=(G,J)=>{if(!this._messages[G])this._messages[G]={};this._messages[G]=P(this._messages[G],J)};d=(G,J)=>{let K=typeof G==="number"?new Date(G):G,Z=this._config.dateTimeFormats?.[this._locale]?.[J||"short"]||{};return new Intl.DateTimeFormat(this._locale,Z).format(K)};n=(G,J)=>{let K=this._config.numberFormats?.[this._locale]?.[J||"decimal"]||{};return new Intl.NumberFormat(this._locale,K).format(G)};translate(G,J,K){let Z=K||this._locale,U=this.getMessage(G,Z);if(U===void 0){let _=Z.indexOf("-");if(_>0){let B=Z.slice(0,_);if(B!==this._fallbackLocale)U=this.getMessage(G,B)}}if(U===void 0&&Z!==this._fallbackLocale)U=this.getMessage(G,this._fallbackLocale);if(U===void 0){if(this._config.warnMissing)console.warn(`[i18n] Missing translation: "${G}" for locale "${Z}"`);if(this._config.missingHandler){let _=this._config.missingHandler(Z,G);if(_!==void 0)return _}return G}if(typeof U!=="string")return G;return this.interpolate(U,J)}translatePlural(G,J,K,Z){let U=Z||this._locale,_=this.getMessage(G,U);if(_===void 0&&U!==this._fallbackLocale)_=this.getMessage(G,this._fallbackLocale);if(_===void 0||typeof _!=="string"){if(this._config.warnMissing)console.warn(`[i18n] Missing plural translation: "${G}" for locale "${U}"`);return G}let B=M(_,this._config.pluralSeparator),$=S(B,J,U),X={count:J,n:J,...K};return this.interpolate($,X)}hasTranslation(G,J){let K=J||this._locale;return this.getMessage(G,K)!==void 0}getMessage(G,J){let K=this._messages[J];if(!K)return;let Z=G.split(this._config.keySeparator||"."),U=K;for(let _ of Z){if(U===void 0||typeof U==="string")return;U=U[_]}return U}interpolate(G,J){if(!J)return G;return G.replace(/\{(\w+)\}/g,(K,Z)=>{let U=J[Z];if(U===void 0||U===null)return K;let _=String(U);return this._config.escapeValues?XG(_):_})}}function x(G){H=G}function E(){return H}function yG(G){R=G}function V(){return Object.keys(N)}function q(G,J){if(!N[G])N[G]={};N[G]=P(N[G],J)}function F(G){for(let[J,K]of Object.entries(G))q(J,K)}function p(G,J){return T(G,J||H)!==void 0}function T(G,J){let K=N[J];if(!K)return;let Z=G.split(z.keySeparator||"."),U=K;for(let _ of Z){if(U===void 0||typeof U==="string")return;U=U[_]}return U}function m(G,J){if(!J)return G;return G.replace(/\{(\w+)\}/g,(K,Z)=>{let U=J[Z];if(U===void 0||U===null)return K;return String(U)})}function d(G,J,K){let Z=K||H,U=T(G,Z);if(U===void 0&&Z!==R)U=T(G,R);if(U===void 0){if(z.warnMissing)console.warn(`[i18n] Missing translation: "${G}" for locale "${Z}"`);return G}if(typeof U!=="string")return G;return m(U,J)}var UG=d;function ZG(G,J,K,Z){let U=Z||H,_=T(G,U);if(_===void 0&&U!==R)_=T(G,R);if(_===void 0||typeof _!=="string")return G;let B=M(_,z.pluralSeparator),$=S(B,J,U),X={count:J,n:J,...K};return m($,X)}function _G(G,J){return p(G,J)}function $G(G,J){return T(G,J||H)}function u(G={}){return new D(G)}function BG(G={}){return u(G)}function i(G){if(z={...z,...G},G.locale)H=G.locale;if(G.fallbackLocale)R=G.fallbackLocale;if(G.messages)F(G.messages)}function P(G,J){let K={...G};for(let Z of Object.keys(J)){let U=J[Z],_=K[Z];if(typeof U==="object"&&U!==null&&typeof _==="object"&&_!==null)K[Z]=P(_,U);else K[Z]=U}return K}function XG(G){let J={"&":"&","<":"<",">":">",'"':""","'":"'"};return G.replace(/[&<>"']/g,(K)=>J[K]||K)}function EG(G,J="medium",K){let Z=K||E(),U=O(G),_=typeof J==="string"?{dateStyle:J}:J;return new Intl.DateTimeFormat(Z,_).format(U)}function QG(G,J="short",K){let Z=K||E(),U=O(G),_=typeof J==="string"?{timeStyle:J}:J;return new Intl.DateTimeFormat(Z,_).format(U)}function WG(G,J="medium",K="short",Z){let U=Z||E(),_=O(G);return new Intl.DateTimeFormat(U,{dateStyle:J,timeStyle:K}).format(_)}function YG(G,J=new Date,K="long",Z){let U=Z||E(),_=O(G),B=O(J),$=_.getTime()-B.getTime(),X=Math.round($/1000),Q=Math.round(X/60),W=Math.round(Q/60),A=Math.round(W/24),Y=Math.round(A/7),I=Math.round(A/30),C=Math.round(A/365),w=new Intl.RelativeTimeFormat(U,{style:K});if(Math.abs(X)<60)return w.format(X,"second");if(Math.abs(Q)<60)return w.format(Q,"minute");if(Math.abs(W)<24)return w.format(W,"hour");if(Math.abs(A)<7)return w.format(A,"day");if(Math.abs(Y)<4)return w.format(Y,"week");if(Math.abs(I)<12)return w.format(I,"month");return w.format(C,"year")}function AG(G,J={},K){let Z=K||E(),U={...J};if(J.compact)U.notation="compact",U.compactDisplay="short";if(J.precision!==void 0)U.minimumFractionDigits=J.precision,U.maximumFractionDigits=J.precision;return new Intl.NumberFormat(Z,U).format(G)}function IG(G,J="USD",K={},Z){let U=Z||E();return new Intl.NumberFormat(U,{style:"currency",currency:J,...K}).format(G)}function wG(G,J={},K){let Z=K||E();return new Intl.NumberFormat(Z,{style:"percent",...J}).format(G)}function mG(G,J,K="short",Z){let U=Z||E();return new Intl.NumberFormat(U,{style:"unit",unit:J,unitDisplay:K}).format(G)}function dG(G,J=2,K){let Z=K||E();if(G===0)return"0 B";let U=1024,_=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],B=Math.floor(Math.log(G)/Math.log(U)),$=G/Math.pow(U,B);return`${new Intl.NumberFormat(Z,{minimumFractionDigits:0,maximumFractionDigits:J}).format($)} ${_[B]}`}function HG(G,J="conjunction",K="long",Z){let U=Z||E();return new Intl.ListFormat(U,{type:J,style:K}).format(G)}function c(G,J){let K=J||E();return new Intl.PluralRules(K).select(G)}function uG(G,J,K){let Z=c(G,K);return J[Z]??J.other}function b(G,J="language",K="long",Z){let U=Z||E();try{return new Intl.DisplayNames(U,{type:J,style:K}).of(G)}catch{return}}function iG(G,J){return b(G,"language","long",J)}function cG(G,J){return b(G,"region","long",J)}function nG(G,J){return b(G,"currency","long",J)}function rG(G,J,K={},Z){let U=Z||E();return new Intl.Collator(U,K).compare(G,J)}function sG(G,J={},K){let Z=K||E(),U=new Intl.Collator(Z,J);return[...G].sort((_,B)=>U.compare(_,B))}function O(G){if(G instanceof Date)return G;if(typeof G==="number")return new Date(G);return new Date(G)}function NG(G){let K=(G||E()).split("-")[0].toLowerCase();return["ar","he","fa","ur","yi","ps","sd","ug","ku","ckb"].includes(K)?"rtl":"ltr"}function tG(G){return NG(G)==="rtl"}import{existsSync as RG,readdirSync as n}from"fs";import{readFile as TG}from"fs/promises";import{basename as r,extname as v,join as h}from"path";import{log as zG}from"@stacksjs/logging";async function L(G){let{directory:J,extensions:K=[".json",".yaml",".yml",".ts",".js"],recursive:Z=!1,namespaceSeparator:U="."}=G,_={};if(!RG(J))return zG.warn(`[i18n] Translation directory not found: ${J}`),_;return await s(J,_,K,Z,U,""),F(_),_}async function s(G,J,K,Z,U,_){let B=n(G,{withFileTypes:!0});for(let $ of B){let X=h(G,$.name);if($.isDirectory()){let Q=$.name;if(jG(Q))await FG(X,Q,J,K);else if(Z){let W=_?`${_}${U}${$.name}`:$.name;await s(X,J,K,Z,U,W)}}else if($.isFile()){let Q=v($.name);if(K.includes(Q)){let W=r($.name,Q);if(!J[W])J[W]={};let A=await k(X);if(_)VG(J[W],_,A,U);else J[W]=f(J[W],A)}}}}async function FG(G,J,K,Z){if(!K[J])K[J]={};let U=n(G,{withFileTypes:!0});for(let _ of U){if(!_.isFile())continue;let B=v(_.name);if(!Z.includes(B))continue;let $=h(G,_.name),X=r(_.name,B),Q=await k($);if(X===J)K[J]=f(K[J],Q);else K[J][X]=Q}}async function k(G){let J=v(G).toLowerCase(),K=await TG(G,"utf8");switch(J){case".json":try{return JSON.parse(K)}catch{throw Error(`Invalid JSON in translation file: ${G}`)}case".yaml":case".yml":return CG(K);case".ts":case".js":{let Z=await import(G);return Z.default||Z}default:throw Error(`Unsupported file type: ${J}`)}}async function OG(G,J){let K=await k(J);return q(G,K),K}function CG(G){try{let J=Bun.YAML.parse(G);if(!J||typeof J!=="object"||Array.isArray(J))throw Error("YAML content must be an object at the root level");return J}catch(J){let K=J instanceof Error?J.message:"Unknown YAML parse error";throw Error(`Invalid YAML translation file: ${K}`)}}function jG(G){return/^[a-z]{2}(?:-[A-Z]{2})?(?:-[A-Za-z]+)?$/i.test(G)}function VG(G,J,K,Z){let U=J.split(Z),_=G;for(let $=0;$<U.length-1;$++){let X=U[$];if(!_[X]||typeof _[X]==="string")_[X]={};_=_[X]}let B=U[U.length-1];_[B]=K}function f(G,J){let K={...G};for(let Z of Object.keys(J)){let U=J[Z],_=K[Z];if(typeof U==="object"&&U!==null&&typeof _==="object"&&_!==null)K[Z]=f(_,U);else K[Z]=U}return K}function KJ(G,J={}){return{load:()=>L({directory:G,...J}),loadLocale:(K,Z)=>OG(K,h(G,Z))}}async function UJ(G){let{loadTranslations:J}=await import("@stacksjs/ts-i18n"),K=await J(G);return F(K),K}var _J={en:{code:"en",name:"English",nativeName:"English",direction:"ltr"},"en-US":{code:"en-US",name:"English (US)",nativeName:"English (US)",direction:"ltr",region:"US"},"en-GB":{code:"en-GB",name:"English (UK)",nativeName:"English (UK)",direction:"ltr",region:"GB"},es:{code:"es",name:"Spanish",nativeName:"Espa\xF1ol",direction:"ltr"},fr:{code:"fr",name:"French",nativeName:"Fran\xE7ais",direction:"ltr"},de:{code:"de",name:"German",nativeName:"Deutsch",direction:"ltr"},it:{code:"it",name:"Italian",nativeName:"Italiano",direction:"ltr"},pt:{code:"pt",name:"Portuguese",nativeName:"Portugu\xEAs",direction:"ltr"},"pt-BR":{code:"pt-BR",name:"Portuguese (Brazil)",nativeName:"Portugu\xEAs (Brasil)",direction:"ltr",region:"BR"},zh:{code:"zh",name:"Chinese",nativeName:"\u4E2D\u6587",direction:"ltr"},"zh-CN":{code:"zh-CN",name:"Chinese (Simplified)",nativeName:"\u7B80\u4F53\u4E2D\u6587",direction:"ltr",region:"CN"},"zh-TW":{code:"zh-TW",name:"Chinese (Traditional)",nativeName:"\u7E41\u9AD4\u4E2D\u6587",direction:"ltr",region:"TW"},ja:{code:"ja",name:"Japanese",nativeName:"\u65E5\u672C\u8A9E",direction:"ltr"},ko:{code:"ko",name:"Korean",nativeName:"\uD55C\uAD6D\uC5B4",direction:"ltr"},ar:{code:"ar",name:"Arabic",nativeName:"\u0627\u0644\u0639\u0631\u0628\u064A\u0629",direction:"rtl"},he:{code:"he",name:"Hebrew",nativeName:"\u05E2\u05D1\u05E8\u05D9\u05EA",direction:"rtl"},ru:{code:"ru",name:"Russian",nativeName:"\u0420\u0443\u0441\u0441\u043A\u0438\u0439",direction:"ltr"},nl:{code:"nl",name:"Dutch",nativeName:"Nederlands",direction:"ltr"},pl:{code:"pl",name:"Polish",nativeName:"Polski",direction:"ltr"},tr:{code:"tr",name:"Turkish",nativeName:"T\xFCrk\xE7e",direction:"ltr"},vi:{code:"vi",name:"Vietnamese",nativeName:"Ti\u1EBFng Vi\u1EC7t",direction:"ltr"},th:{code:"th",name:"Thai",nativeName:"\u0E44\u0E17\u0E22",direction:"ltr"},hi:{code:"hi",name:"Hindi",nativeName:"\u0939\u093F\u0928\u094D\u0926\u0940",direction:"ltr"}};import{loadTranslations as FJ,generateTypes as OJ,generateTypesFromModule as CJ,generateSampleConfig as jJ,writeOutputs as VJ,createTranslator as qJ}from"@stacksjs/ts-i18n";import{existsSync as qG}from"fs";import{config as y}from"@stacksjs/config";import{projectPath as MG}from"@stacksjs/path";var t=!1;async function o(){if(t)return;let G=MG("locales");if(qG(G))await L({directory:G,extensions:[".yaml",".yml",".json"]});let J=y.app,K=J?.locale??"en",Z=J?.fallbackLocale??K,U=V();i({locale:K,fallbackLocale:Z,availableLocales:U.length>0?U:[K],warnMissing:(J?.env??process.env.APP_ENV)!=="production"}),t=!0}function l(G){let K=y.app?.locale??"en",Z=new Set(V());if(!Z.size)Z.add(K);let U=(Y)=>{let I=Y.trim().toLowerCase();if(Z.has(I))return I;let C=I.split("-")[0];if(Z.has(C))return C;return K};if(!G)return K;let _=new URL(G.url),B=_.pathname,$=G.headers.get("x-stx-locale");if($)return U($);for(let Y of Z){if(Y===K)continue;if(B===`/${Y}`||B===`/${Y}/`)return U(Y);if(B.startsWith(`/${Y}/`))return U(Y)}let X=_.searchParams.get("locale");if(X)return U(X);let W=(G.headers.get("cookie")??"").match(/(?:^|;\s*)locale=([a-z]{2}(?:-[a-z]{2})?)(?:;|$)/i);if(W?.[1])return U(W[1]);let A=G.headers.get("accept-language")??"";for(let Y of A.split(",")){let I=Y.split(";")[0]?.trim();if(I)return U(I)}return K}async function SG(G){await o();let J=l(G);return x(J),J}function DG(G,J){let K=G.trim().toLowerCase();if(J.locales.includes(K))return K;let Z=K.split("-")[0];if(J.locales.includes(Z))return Z;return null}function a(G,J){for(let K of J){if(G===`/${K}`||G===`/${K}/`)return{locale:K,path:"/"};if(G.startsWith(`/${K}/`))return{locale:K,path:G.slice(K.length+1)}}return{locale:null,path:G}}function e(G,J,K){if(J===K)return G;if(G==="/")return`/${J}/`;if(G==="/404.html")return`/${J}/404.html`;return`/${J}${G}`}function xG(G,J,K){let Z=DG(J,K);if(!Z)return Response.redirect(new URL("/",G.url).href,302);let U=G.headers.get("referer"),_="/";if(U)try{let Q=new URL(U);if(!Q.pathname.startsWith("/locale/"))_=a(Q.pathname,K.locales).path||"/"}catch{_="/"}let B=e(_,Z,K.defaultLocale),$=new URL(B,G.url),X=Response.redirect($.href,302);return X.headers.append("Set-Cookie",`locale=${Z}; Path=/; Max-Age=31536000; SameSite=Lax`),X}export{VJ as writeI18nOutputs,BG as useI18n,UG as trans,$G as tm,_G as te,ZG as tc,d as t,a as stripLocalePrefix,sG as sortStrings,x as setLocale,yG as setFallbackLocale,S as selectPluralForm,uG as selectPlural,l as resolveRequestLocale,M as parsePluralForms,e as localizePath,FJ as loadTranslationsFromDisk,F as loadTranslations,OG as loadLocale,UJ as loadFromTsI18n,L as loadFromDirectory,k as loadFile,tG as isRTL,p as hasTranslation,vG as hasPluralRule,NG as getTextDirection,bG as getSupportedPluralLocales,cG as getRegionName,KG as getPluralRule,JG as getPluralCategory,E as getLocale,iG as getLanguageName,b as getDisplayName,nG as getCurrencyName,V as getAvailableLocales,CJ as generateI18nTypesFromModule,OJ as generateI18nTypes,jJ as generateI18nSampleConfig,mG as formatUnit,QG as formatTime,YG as formatRelativeTime,c as formatPlural,wG as formatPercent,AG as formatNumber,HG as formatList,WG as formatDateTime,EG as formatDate,IG as formatCurrency,dG as formatBytes,o as ensureLocalesLoaded,qJ as createSimpleTranslator,xG as createLocaleSwitchResponse,KJ as createLoader,u as createI18n,i as configure,rG as compareStrings,SG as applyRequestLocale,q as addTranslations,hG as addPluralRule,_J as LOCALE_INFO,D as I18n};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function stripLocalePrefix(path: string, locales: string[]): { locale: string | null, path: string };
|
|
2
|
+
export declare function localizePath(path: string, locale: string, defaultLocale: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Build a redirect Response that sets the `locale` cookie and sends the
|
|
5
|
+
* visitor to the equivalent path in the requested locale (STX-style).
|
|
6
|
+
*/
|
|
7
|
+
export declare function createLocaleSwitchResponse(request: Request, localeCode: string, config: LocaleSwitchConfig): Response;
|
|
8
|
+
/**
|
|
9
|
+
* Locale switch redirect — mirrors STX `buildLangPickerScript` path logic
|
|
10
|
+
* (`/<code>/…` prefixes) so cookie-based fallbacks and `/locale/{code}`
|
|
11
|
+
* redirects stay consistent with stx-serve i18n routing.
|
|
12
|
+
*/
|
|
13
|
+
export declare interface LocaleSwitchConfig {
|
|
14
|
+
locales: string[]
|
|
15
|
+
defaultLocale: string
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/i18n",
|
|
3
|
-
"version": "0.70.
|
|
3
|
+
"version": "0.70.54",
|
|
4
4
|
"description": "Internationalization system for Stacks.js applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
"@stacksjs/ts-i18n": "^0.1.9"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@stacksjs/development": "
|
|
41
|
+
"@stacksjs/development": "0.70.54"
|
|
42
42
|
}
|
|
43
43
|
}
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { config } from '@stacksjs/config'
|
|
3
|
+
import { projectPath } from '@stacksjs/path'
|
|
4
|
+
import { loadFromDirectory } from './loader'
|
|
5
|
+
import { configure, getAvailableLocales, setLocale } from './translator'
|
|
6
|
+
|
|
7
|
+
let localesLoaded = false
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load translation files from the project `locales/` directory once.
|
|
11
|
+
* Locale subdirectories (`locales/de.yml`, `locales/en.yml`, …) are detected
|
|
12
|
+
* automatically by `loadFromDirectory`.
|
|
13
|
+
*/
|
|
14
|
+
export async function ensureLocalesLoaded(): Promise<void> {
|
|
15
|
+
if (localesLoaded)
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
const directory = projectPath('locales')
|
|
19
|
+
if (existsSync(directory)) {
|
|
20
|
+
await loadFromDirectory({
|
|
21
|
+
directory,
|
|
22
|
+
extensions: ['.yaml', '.yml', '.json'],
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const app = (config as { app?: { locale?: string, fallbackLocale?: string, env?: string } }).app
|
|
27
|
+
const defaultLocale = app?.locale ?? 'en'
|
|
28
|
+
const fallback = app?.fallbackLocale ?? defaultLocale
|
|
29
|
+
const available = getAvailableLocales()
|
|
30
|
+
configure({
|
|
31
|
+
locale: defaultLocale,
|
|
32
|
+
fallbackLocale: fallback,
|
|
33
|
+
availableLocales: available.length > 0 ? available : [defaultLocale],
|
|
34
|
+
warnMissing: (app?.env ?? process.env.APP_ENV) !== 'production',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
localesLoaded = true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the active locale for an HTTP request.
|
|
42
|
+
* Order: STX locale prefix (`/en/...`), `?locale=`, `locale` cookie,
|
|
43
|
+
* Accept-Language, then `config.app.locale`.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveRequestLocale(request?: Request): string {
|
|
46
|
+
const app = (config as { app?: { locale?: string, fallbackLocale?: string } }).app
|
|
47
|
+
const defaultLocale = app?.locale ?? 'en'
|
|
48
|
+
const available = new Set(getAvailableLocales())
|
|
49
|
+
if (!available.size)
|
|
50
|
+
available.add(defaultLocale)
|
|
51
|
+
|
|
52
|
+
const pick = (code: string): string => {
|
|
53
|
+
const normalized = code.trim().toLowerCase()
|
|
54
|
+
if (available.has(normalized))
|
|
55
|
+
return normalized
|
|
56
|
+
const short = normalized.split('-')[0]
|
|
57
|
+
if (available.has(short))
|
|
58
|
+
return short
|
|
59
|
+
return defaultLocale
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!request)
|
|
63
|
+
return defaultLocale
|
|
64
|
+
|
|
65
|
+
const url = new URL(request.url)
|
|
66
|
+
const pathname = url.pathname
|
|
67
|
+
|
|
68
|
+
const fromStx = request.headers.get('x-stx-locale')
|
|
69
|
+
if (fromStx)
|
|
70
|
+
return pick(fromStx)
|
|
71
|
+
|
|
72
|
+
// Non-default locales are served under `/<code>/…` by stx-serve i18n routing.
|
|
73
|
+
for (const code of available) {
|
|
74
|
+
if (code === defaultLocale)
|
|
75
|
+
continue
|
|
76
|
+
if (pathname === `/${code}` || pathname === `/${code}/`)
|
|
77
|
+
return pick(code)
|
|
78
|
+
if (pathname.startsWith(`/${code}/`))
|
|
79
|
+
return pick(code)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fromQuery = url.searchParams.get('locale')
|
|
83
|
+
if (fromQuery)
|
|
84
|
+
return pick(fromQuery)
|
|
85
|
+
|
|
86
|
+
const cookie = request.headers.get('cookie') ?? ''
|
|
87
|
+
const match = cookie.match(/(?:^|;\s*)locale=([a-z]{2}(?:-[a-z]{2})?)(?:;|$)/i)
|
|
88
|
+
if (match?.[1])
|
|
89
|
+
return pick(match[1])
|
|
90
|
+
|
|
91
|
+
const accept = request.headers.get('accept-language') ?? ''
|
|
92
|
+
for (const part of accept.split(',')) {
|
|
93
|
+
const tag = part.split(';')[0]?.trim()
|
|
94
|
+
if (tag)
|
|
95
|
+
return pick(tag)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return defaultLocale
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Load `locales/` and activate the global translator for this request. */
|
|
102
|
+
export async function applyRequestLocale(request?: Request): Promise<string> {
|
|
103
|
+
await ensureLocalesLoaded()
|
|
104
|
+
const locale = resolveRequestLocale(request)
|
|
105
|
+
setLocale(locale)
|
|
106
|
+
return locale
|
|
107
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -74,6 +74,19 @@ export {
|
|
|
74
74
|
useI18n,
|
|
75
75
|
} from './translator'
|
|
76
76
|
|
|
77
|
+
export {
|
|
78
|
+
ensureLocalesLoaded,
|
|
79
|
+
resolveRequestLocale,
|
|
80
|
+
applyRequestLocale,
|
|
81
|
+
} from './bootstrap'
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
createLocaleSwitchResponse,
|
|
85
|
+
localizePath,
|
|
86
|
+
stripLocalePrefix,
|
|
87
|
+
type LocaleSwitchConfig,
|
|
88
|
+
} from './locale-switch'
|
|
89
|
+
|
|
77
90
|
export {
|
|
78
91
|
formatDate,
|
|
79
92
|
formatTime,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale switch redirect — mirrors STX `buildLangPickerScript` path logic
|
|
3
|
+
* (`/<code>/…` prefixes) so cookie-based fallbacks and `/locale/{code}`
|
|
4
|
+
* redirects stay consistent with stx-serve i18n routing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface LocaleSwitchConfig {
|
|
8
|
+
locales: string[]
|
|
9
|
+
defaultLocale: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeLocale(code: string, config: LocaleSwitchConfig): string | null {
|
|
13
|
+
const normalized = code.trim().toLowerCase()
|
|
14
|
+
if (config.locales.includes(normalized))
|
|
15
|
+
return normalized
|
|
16
|
+
const short = normalized.split('-')[0]
|
|
17
|
+
if (config.locales.includes(short))
|
|
18
|
+
return short
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function stripLocalePrefix(path: string, locales: string[]): { locale: string | null, path: string } {
|
|
23
|
+
for (const loc of locales) {
|
|
24
|
+
if (path === `/${loc}` || path === `/${loc}/`)
|
|
25
|
+
return { locale: loc, path: '/' }
|
|
26
|
+
if (path.startsWith(`/${loc}/`))
|
|
27
|
+
return { locale: loc, path: path.slice(loc.length + 1) }
|
|
28
|
+
}
|
|
29
|
+
return { locale: null, path }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function localizePath(path: string, locale: string, defaultLocale: string): string {
|
|
33
|
+
if (locale === defaultLocale)
|
|
34
|
+
return path
|
|
35
|
+
if (path === '/')
|
|
36
|
+
return `/${locale}/`
|
|
37
|
+
if (path === '/404.html')
|
|
38
|
+
return `/${locale}/404.html`
|
|
39
|
+
return `/${locale}${path}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a redirect Response that sets the `locale` cookie and sends the
|
|
44
|
+
* visitor to the equivalent path in the requested locale (STX-style).
|
|
45
|
+
*/
|
|
46
|
+
export function createLocaleSwitchResponse(
|
|
47
|
+
request: Request,
|
|
48
|
+
localeCode: string,
|
|
49
|
+
config: LocaleSwitchConfig,
|
|
50
|
+
): Response {
|
|
51
|
+
const locale = normalizeLocale(localeCode, config)
|
|
52
|
+
if (!locale) {
|
|
53
|
+
return Response.redirect(new URL('/', request.url).href, 302)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const referer = request.headers.get('referer')
|
|
57
|
+
let basePath = '/'
|
|
58
|
+
|
|
59
|
+
if (referer) {
|
|
60
|
+
try {
|
|
61
|
+
const ref = new URL(referer)
|
|
62
|
+
if (!ref.pathname.startsWith('/locale/')) {
|
|
63
|
+
const stripped = stripLocalePrefix(ref.pathname, config.locales)
|
|
64
|
+
basePath = stripped.path || '/'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
basePath = '/'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const targetPath = localizePath(basePath, locale, config.defaultLocale)
|
|
73
|
+
const target = new URL(targetPath, request.url)
|
|
74
|
+
|
|
75
|
+
const res = Response.redirect(target.href, 302)
|
|
76
|
+
res.headers.append(
|
|
77
|
+
'Set-Cookie',
|
|
78
|
+
`locale=${locale}; Path=/; Max-Age=31536000; SameSite=Lax`,
|
|
79
|
+
)
|
|
80
|
+
return res
|
|
81
|
+
}
|