@buenojs/bueno 0.8.3 → 0.8.5

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.
Files changed (218) hide show
  1. package/README.md +136 -16
  2. package/dist/cli/{index.js → bin.js} +3036 -1421
  3. package/dist/container/index.js +250 -0
  4. package/dist/context/index.js +219 -0
  5. package/dist/database/index.js +493 -0
  6. package/dist/frontend/index.js +7697 -0
  7. package/dist/health/index.js +364 -0
  8. package/dist/i18n/index.js +345 -0
  9. package/dist/index.js +11043 -6482
  10. package/dist/jobs/index.js +819 -0
  11. package/dist/lock/index.js +367 -0
  12. package/dist/logger/index.js +281 -0
  13. package/dist/metrics/index.js +289 -0
  14. package/dist/middleware/index.js +77 -0
  15. package/dist/migrations/index.js +571 -0
  16. package/dist/modules/index.js +3346 -0
  17. package/dist/notification/index.js +484 -0
  18. package/dist/observability/index.js +331 -0
  19. package/dist/openapi/index.js +776 -0
  20. package/dist/orm/index.js +1356 -0
  21. package/dist/router/index.js +886 -0
  22. package/dist/rpc/index.js +691 -0
  23. package/dist/schema/index.js +400 -0
  24. package/dist/telemetry/index.js +595 -0
  25. package/dist/template/index.js +640 -0
  26. package/dist/templates/index.js +640 -0
  27. package/dist/testing/index.js +1111 -0
  28. package/dist/types/index.js +60 -0
  29. package/package.json +121 -27
  30. package/src/cache/index.ts +2 -1
  31. package/src/cli/bin.ts +2 -2
  32. package/src/cli/commands/build.ts +183 -165
  33. package/src/cli/commands/dev.ts +96 -89
  34. package/src/cli/commands/generate.ts +142 -111
  35. package/src/cli/commands/help.ts +20 -16
  36. package/src/cli/commands/index.ts +3 -6
  37. package/src/cli/commands/migration.ts +124 -105
  38. package/src/cli/commands/new.ts +392 -438
  39. package/src/cli/commands/start.ts +81 -79
  40. package/src/cli/core/args.ts +68 -50
  41. package/src/cli/core/console.ts +89 -95
  42. package/src/cli/core/index.ts +4 -4
  43. package/src/cli/core/prompt.ts +65 -62
  44. package/src/cli/core/spinner.ts +23 -20
  45. package/src/cli/index.ts +46 -38
  46. package/src/cli/templates/database/index.ts +61 -0
  47. package/src/cli/templates/database/mysql.ts +14 -0
  48. package/src/cli/templates/database/none.ts +16 -0
  49. package/src/cli/templates/database/postgresql.ts +14 -0
  50. package/src/cli/templates/database/sqlite.ts +14 -0
  51. package/src/cli/templates/deploy.ts +29 -26
  52. package/src/cli/templates/docker.ts +41 -30
  53. package/src/cli/templates/frontend/index.ts +63 -0
  54. package/src/cli/templates/frontend/none.ts +17 -0
  55. package/src/cli/templates/frontend/react.ts +140 -0
  56. package/src/cli/templates/frontend/solid.ts +134 -0
  57. package/src/cli/templates/frontend/svelte.ts +131 -0
  58. package/src/cli/templates/frontend/vue.ts +130 -0
  59. package/src/cli/templates/generators/index.ts +339 -0
  60. package/src/cli/templates/generators/types.ts +56 -0
  61. package/src/cli/templates/index.ts +35 -2
  62. package/src/cli/templates/project/api.ts +81 -0
  63. package/src/cli/templates/project/default.ts +140 -0
  64. package/src/cli/templates/project/fullstack.ts +111 -0
  65. package/src/cli/templates/project/index.ts +95 -0
  66. package/src/cli/templates/project/minimal.ts +45 -0
  67. package/src/cli/templates/project/types.ts +94 -0
  68. package/src/cli/templates/project/website.ts +263 -0
  69. package/src/cli/utils/fs.ts +55 -41
  70. package/src/cli/utils/index.ts +3 -2
  71. package/src/cli/utils/strings.ts +47 -33
  72. package/src/cli/utils/version.ts +47 -0
  73. package/src/config/env-validation.ts +100 -0
  74. package/src/config/env.ts +169 -41
  75. package/src/config/index.ts +28 -20
  76. package/src/config/loader.ts +25 -16
  77. package/src/config/merge.ts +21 -10
  78. package/src/config/types.ts +545 -25
  79. package/src/config/validation.ts +215 -7
  80. package/src/container/forward-ref.ts +22 -22
  81. package/src/container/index.ts +34 -12
  82. package/src/context/index.ts +11 -1
  83. package/src/database/index.ts +7 -190
  84. package/src/database/orm/builder.ts +457 -0
  85. package/src/database/orm/casts/index.ts +130 -0
  86. package/src/database/orm/casts/types.ts +25 -0
  87. package/src/database/orm/compiler.ts +304 -0
  88. package/src/database/orm/hooks/index.ts +114 -0
  89. package/src/database/orm/index.ts +61 -0
  90. package/src/database/orm/model-registry.ts +59 -0
  91. package/src/database/orm/model.ts +821 -0
  92. package/src/database/orm/relationships/base.ts +146 -0
  93. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  94. package/src/database/orm/relationships/belongs-to.ts +56 -0
  95. package/src/database/orm/relationships/has-many.ts +45 -0
  96. package/src/database/orm/relationships/has-one.ts +41 -0
  97. package/src/database/orm/relationships/index.ts +11 -0
  98. package/src/database/orm/scopes/index.ts +55 -0
  99. package/src/events/__tests__/event-system.test.ts +235 -0
  100. package/src/events/config.ts +238 -0
  101. package/src/events/example-usage.ts +185 -0
  102. package/src/events/index.ts +278 -0
  103. package/src/events/manager.ts +385 -0
  104. package/src/events/registry.ts +182 -0
  105. package/src/events/types.ts +124 -0
  106. package/src/frontend/api-routes.ts +65 -23
  107. package/src/frontend/bundler.ts +76 -34
  108. package/src/frontend/console-client.ts +2 -2
  109. package/src/frontend/console-stream.ts +94 -38
  110. package/src/frontend/dev-server.ts +94 -46
  111. package/src/frontend/file-router.ts +61 -19
  112. package/src/frontend/frameworks/index.ts +37 -10
  113. package/src/frontend/frameworks/react.ts +10 -8
  114. package/src/frontend/frameworks/solid.ts +11 -9
  115. package/src/frontend/frameworks/svelte.ts +15 -9
  116. package/src/frontend/frameworks/vue.ts +13 -11
  117. package/src/frontend/hmr-client.ts +12 -10
  118. package/src/frontend/hmr.ts +146 -103
  119. package/src/frontend/index.ts +14 -5
  120. package/src/frontend/islands.ts +41 -22
  121. package/src/frontend/isr.ts +59 -37
  122. package/src/frontend/layout.ts +36 -21
  123. package/src/frontend/ssr/react.ts +74 -27
  124. package/src/frontend/ssr/solid.ts +54 -20
  125. package/src/frontend/ssr/svelte.ts +48 -14
  126. package/src/frontend/ssr/vue.ts +50 -18
  127. package/src/frontend/ssr.ts +83 -39
  128. package/src/frontend/types.ts +91 -56
  129. package/src/health/index.ts +21 -9
  130. package/src/i18n/engine.ts +305 -0
  131. package/src/i18n/index.ts +38 -0
  132. package/src/i18n/loader.ts +218 -0
  133. package/src/i18n/middleware.ts +164 -0
  134. package/src/i18n/negotiator.ts +162 -0
  135. package/src/i18n/types.ts +158 -0
  136. package/src/index.ts +179 -27
  137. package/src/jobs/drivers/memory.ts +315 -0
  138. package/src/jobs/drivers/redis.ts +459 -0
  139. package/src/jobs/index.ts +30 -0
  140. package/src/jobs/queue.ts +281 -0
  141. package/src/jobs/types.ts +295 -0
  142. package/src/jobs/worker.ts +380 -0
  143. package/src/logger/index.ts +1 -3
  144. package/src/logger/transports/index.ts +62 -22
  145. package/src/metrics/index.ts +25 -16
  146. package/src/migrations/index.ts +9 -0
  147. package/src/modules/filters.ts +13 -17
  148. package/src/modules/guards.ts +49 -26
  149. package/src/modules/index.ts +409 -298
  150. package/src/modules/interceptors.ts +58 -20
  151. package/src/modules/lazy.ts +11 -19
  152. package/src/modules/lifecycle.ts +15 -7
  153. package/src/modules/metadata.ts +15 -5
  154. package/src/modules/pipes.ts +94 -72
  155. package/src/notification/channels/base.ts +68 -0
  156. package/src/notification/channels/email.ts +105 -0
  157. package/src/notification/channels/push.ts +104 -0
  158. package/src/notification/channels/sms.ts +105 -0
  159. package/src/notification/channels/whatsapp.ts +104 -0
  160. package/src/notification/index.ts +48 -0
  161. package/src/notification/service.ts +354 -0
  162. package/src/notification/types.ts +344 -0
  163. package/src/observability/__tests__/observability.test.ts +483 -0
  164. package/src/observability/breadcrumbs.ts +114 -0
  165. package/src/observability/index.ts +136 -0
  166. package/src/observability/interceptor.ts +85 -0
  167. package/src/observability/service.ts +303 -0
  168. package/src/observability/trace.ts +37 -0
  169. package/src/observability/types.ts +196 -0
  170. package/src/openapi/__tests__/decorators.test.ts +335 -0
  171. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  172. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  173. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  174. package/src/openapi/decorators.ts +328 -0
  175. package/src/openapi/document-builder.ts +274 -0
  176. package/src/openapi/index.ts +112 -0
  177. package/src/openapi/metadata.ts +112 -0
  178. package/src/openapi/route-scanner.ts +289 -0
  179. package/src/openapi/schema-generator.ts +256 -0
  180. package/src/openapi/swagger-module.ts +166 -0
  181. package/src/openapi/types.ts +398 -0
  182. package/src/orm/index.ts +10 -0
  183. package/src/rpc/index.ts +3 -1
  184. package/src/schema/index.ts +9 -0
  185. package/src/security/index.ts +15 -6
  186. package/src/ssg/index.ts +9 -8
  187. package/src/telemetry/index.ts +76 -22
  188. package/src/template/index.ts +7 -0
  189. package/src/templates/engine.ts +224 -0
  190. package/src/templates/index.ts +9 -0
  191. package/src/templates/loader.ts +331 -0
  192. package/src/templates/renderers/markdown.ts +212 -0
  193. package/src/templates/renderers/simple.ts +269 -0
  194. package/src/templates/types.ts +154 -0
  195. package/src/testing/index.ts +100 -27
  196. package/src/types/optional-deps.d.ts +347 -187
  197. package/src/validation/index.ts +92 -2
  198. package/src/validation/schemas.ts +536 -0
  199. package/tests/integration/fullstack.test.ts +4 -4
  200. package/tests/unit/database.test.ts +2 -72
  201. package/tests/unit/env-validation.test.ts +166 -0
  202. package/tests/unit/events.test.ts +910 -0
  203. package/tests/unit/i18n.test.ts +455 -0
  204. package/tests/unit/jobs.test.ts +493 -0
  205. package/tests/unit/notification.test.ts +988 -0
  206. package/tests/unit/observability.test.ts +453 -0
  207. package/tests/unit/orm/builder.test.ts +323 -0
  208. package/tests/unit/orm/casts.test.ts +179 -0
  209. package/tests/unit/orm/compiler.test.ts +220 -0
  210. package/tests/unit/orm/eager-loading.test.ts +285 -0
  211. package/tests/unit/orm/hooks.test.ts +191 -0
  212. package/tests/unit/orm/model.test.ts +373 -0
  213. package/tests/unit/orm/relationships.test.ts +303 -0
  214. package/tests/unit/orm/scopes.test.ts +74 -0
  215. package/tests/unit/templates-simple.test.ts +53 -0
  216. package/tests/unit/templates.test.ts +454 -0
  217. package/tests/unit/validation.test.ts +18 -24
  218. package/tsconfig.json +11 -3
@@ -0,0 +1,164 @@
1
+ /**
2
+ * i18n Middleware — Locale Detection and Translation Binding
3
+ *
4
+ * Detects locale from cookie (priority 1) then Accept-Language header (priority 2),
5
+ * binds t() and locale to context, persists locale choice in a cookie.
6
+ */
7
+
8
+ import type { Context } from "../context";
9
+ import type { Middleware } from "../middleware";
10
+ import { type I18n, createI18n } from "./engine";
11
+ import type { I18nConfig, TranslationFunction } from "./types";
12
+
13
+ // ============= Typed Context Helpers =============
14
+
15
+ /**
16
+ * Get the current locale from context.
17
+ * Typed helper for accessing the locale set by i18nMiddleware.
18
+ *
19
+ * Returns the default locale ("en") if middleware has not run.
20
+ * Useful in route handlers:
21
+ *
22
+ * @example
23
+ * router.get('/locale', (ctx) => {
24
+ * const locale = getLocale(ctx); // 'fr', 'en', etc.
25
+ * return ctx.json({ locale });
26
+ * });
27
+ *
28
+ * @param ctx Bueno Context instance
29
+ * @returns Current locale, or "en" if middleware hasn't run
30
+ */
31
+ export function getLocale(ctx: Context): string {
32
+ return (ctx.get("locale") as string | undefined) ?? "en";
33
+ }
34
+
35
+ /**
36
+ * Get the bound translation function from context.
37
+ * Typed helper for accessing the t() set by i18nMiddleware.
38
+ *
39
+ * Returns an identity function (returns key as-is) if middleware has not run.
40
+ * Useful in route handlers:
41
+ *
42
+ * @example
43
+ * router.get('/greeting', (ctx) => {
44
+ * const t = getT(ctx);
45
+ * const message = t('greeting', { name: 'Alice' });
46
+ * return ctx.text(message);
47
+ * });
48
+ *
49
+ * @param ctx Bueno Context instance
50
+ * @returns Translation function, or identity function if middleware hasn't run
51
+ */
52
+ export function getT(ctx: Context): TranslationFunction {
53
+ return (ctx.get("t") as TranslationFunction | undefined) ?? ((key) => key);
54
+ }
55
+
56
+ // ============= Middleware Options =============
57
+
58
+ /**
59
+ * Options for i18nMiddleware.
60
+ * Extends I18nConfig and adds an optional pre-constructed I18n instance.
61
+ */
62
+ export interface I18nMiddlewareOptions extends I18nConfig {
63
+ /**
64
+ * Pre-constructed I18n instance.
65
+ * If not provided, one is created from the other options.
66
+ *
67
+ * Pass this when you want a single shared instance across multiple
68
+ * middleware chains or route groups (prevents double-loading locale files).
69
+ */
70
+ i18n?: I18n;
71
+ }
72
+
73
+ // ============= Middleware Factory =============
74
+
75
+ /**
76
+ * Create i18n middleware.
77
+ *
78
+ * Locale detection priority:
79
+ * 1. Cookie (bueno_locale or custom cookieName)
80
+ * 2. Accept-Language header
81
+ * 3. Default locale (config.defaultLocale)
82
+ *
83
+ * Sets on context (available via getLocale/getT):
84
+ * - ctx.set('locale', detectedLocale)
85
+ * - ctx.set('t', translationFunction)
86
+ *
87
+ * Sets on response:
88
+ * - Set-Cookie header to persist locale choice
89
+ * - Vary: Accept-Language header (for correct CDN caching)
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const router = new Router();
94
+ * router.use(i18nMiddleware({
95
+ * defaultLocale: 'en',
96
+ * supportedLocales: ['en', 'fr', 'de'],
97
+ * basePath: 'resources/i18n'
98
+ * }));
99
+ *
100
+ * router.get('/hello', (ctx) => {
101
+ * const locale = getLocale(ctx);
102
+ * const t = getT(ctx);
103
+ * return ctx.json({ message: t('greeting') });
104
+ * });
105
+ * ```
106
+ *
107
+ * @param options Configuration options
108
+ * @returns Koa-style middleware function
109
+ */
110
+ export function i18nMiddleware(
111
+ options: I18nMiddlewareOptions = {},
112
+ ): Middleware {
113
+ const engine = options.i18n ?? createI18n(options);
114
+ const negotiator = engine.getNegotiator();
115
+ const cookieName = engine.config.cookieName;
116
+ const cookieMaxAge = engine.config.cookieMaxAge;
117
+
118
+ return async (
119
+ ctx: Context,
120
+ next: () => Promise<Response>,
121
+ ): Promise<Response> => {
122
+ // --- Step 1: Detect locale ---
123
+ let locale: string;
124
+
125
+ // Priority 1: Cookie
126
+ const cookieLocale = ctx.getCookie(cookieName);
127
+ if (cookieLocale && negotiator.isSupported(cookieLocale)) {
128
+ locale = cookieLocale;
129
+ } else {
130
+ // Priority 2: Accept-Language header
131
+ const acceptLanguage = ctx.getHeader("accept-language") ?? "";
132
+ if (acceptLanguage) {
133
+ const match = negotiator.negotiate(acceptLanguage);
134
+ locale = match.locale;
135
+ } else {
136
+ locale = engine.config.defaultLocale;
137
+ }
138
+ }
139
+
140
+ // --- Step 2: Store on context ---
141
+ // Use 'as never' cast for now (same pattern as requestId middleware)
142
+ // Typed access via getLocale(ctx) and getT(ctx) helpers
143
+ ctx.set("locale" as never, locale);
144
+ ctx.set("t" as never, engine.createTranslator(locale));
145
+
146
+ // --- Step 3: Call next ---
147
+ const response = await next();
148
+
149
+ // --- Step 4: Set cookie on response ---
150
+ // Always refresh the cookie so it stays alive during user session
151
+ const cookieValue = `${cookieName}=${locale}; Max-Age=${cookieMaxAge}; Path=/; SameSite=Lax`;
152
+ response.headers.append("Set-Cookie", cookieValue);
153
+
154
+ // --- Step 5: Set Vary header ---
155
+ // Instruct caches to vary on Accept-Language so different locales are cached separately
156
+ const existing = response.headers.get("Vary");
157
+ response.headers.set(
158
+ "Vary",
159
+ existing ? `${existing}, Accept-Language` : "Accept-Language",
160
+ );
161
+
162
+ return response;
163
+ };
164
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Locale Negotiation — Accept-Language Header Parsing
3
+ *
4
+ * Parses RFC 7231 Accept-Language headers and matches them against
5
+ * supported locales using exact and language-prefix matching.
6
+ *
7
+ * @see https://tools.ietf.org/html/rfc7231#section-5.3.5
8
+ */
9
+
10
+ import type { LocaleMatch } from "./types";
11
+
12
+ // ============= Types =============
13
+
14
+ /**
15
+ * Parsed entry from an Accept-Language header.
16
+ */
17
+ interface AcceptEntry {
18
+ locale: string; // e.g. "en-US"
19
+ quality: number; // 0.0–1.0, default 1.0
20
+ }
21
+
22
+ // ============= Parsing =============
23
+
24
+ /**
25
+ * Parse RFC 7231 Accept-Language header value into quality-sorted entries.
26
+ *
27
+ * @example
28
+ * Input: "en-US,en;q=0.9,fr;q=0.8"
29
+ * Output: [
30
+ * { locale: "en-US", quality: 1.0 },
31
+ * { locale: "en", quality: 0.9 },
32
+ * { locale: "fr", quality: 0.8 }
33
+ * ]
34
+ *
35
+ * @param header Raw Accept-Language header value
36
+ * @returns Array of entries sorted by quality (highest first)
37
+ */
38
+ export function parseAcceptLanguage(header: string): AcceptEntry[] {
39
+ if (!header.trim()) return [];
40
+
41
+ return header
42
+ .split(",")
43
+ .map((part) => {
44
+ const [localeRaw, qRaw] = part.trim().split(";");
45
+ const locale = localeRaw?.trim() ?? "";
46
+ const quality = qRaw
47
+ ? Number.parseFloat(qRaw.trim().replace("q=", ""))
48
+ : 1.0;
49
+ return { locale, quality: isNaN(quality) ? 1.0 : quality };
50
+ })
51
+ .filter((e) => e.locale.length > 0)
52
+ .sort((a, b) => b.quality - a.quality);
53
+ }
54
+
55
+ /**
56
+ * Normalise a locale tag:
57
+ * - Converts underscore to hyphen ("en_US" → "en-US")
58
+ * - Lowercases language subtag ("EN" → "en")
59
+ * - Uppercases region subtag ("us" → "US")
60
+ *
61
+ * @example
62
+ * normaliseLocale("en_US") → "en-US"
63
+ * normaliseLocale("EN-us") → "en-US"
64
+ * normaliseLocale("fr") → "fr"
65
+ *
66
+ * @param locale Raw locale string
67
+ * @returns Normalised locale string
68
+ */
69
+ export function normaliseLocale(locale: string): string {
70
+ const parts = locale.replace("_", "-").split("-");
71
+ if (parts.length === 0) return locale;
72
+ const lang = parts[0]!.toLowerCase();
73
+ if (parts.length === 1) return lang;
74
+ const region = parts[1]!.toUpperCase();
75
+ return `${lang}-${region}`;
76
+ }
77
+
78
+ /**
79
+ * Extract the language subtag from a locale.
80
+ * @example languageSubtag("en-US") → "en"
81
+ * @param locale Locale identifier
82
+ * @returns Language subtag (first part before hyphen)
83
+ */
84
+ function languageSubtag(locale: string): string {
85
+ return locale.split("-")[0]!.toLowerCase();
86
+ }
87
+
88
+ // ============= Negotiator =============
89
+
90
+ /**
91
+ * Negotiates the best locale match given an Accept-Language header
92
+ * and a list of supported locales.
93
+ *
94
+ * Matching strategy (two-pass):
95
+ * 1. Exact match: find a supported locale that exactly matches (case-normalised) any entry
96
+ * 2. Language prefix match: find a supported locale whose language subtag matches any entry
97
+ * 3. Default: return the configured default locale
98
+ *
99
+ * This two-pass approach ensures correct behavior even when entries
100
+ * are out of quality order, e.g. "fr-CA,de;q=0.9" against ["de","fr"]
101
+ * should return "fr" (via prefix match), not "de".
102
+ */
103
+ export class LocaleNegotiator {
104
+ private supported: string[];
105
+ private defaultLocale: string;
106
+
107
+ constructor(supportedLocales: string[], defaultLocale: string) {
108
+ this.supported = supportedLocales;
109
+ this.defaultLocale = defaultLocale;
110
+ }
111
+
112
+ /**
113
+ * Find the best matching locale given a raw Accept-Language header string.
114
+ *
115
+ * @param acceptLanguageHeader Raw Accept-Language header value
116
+ * @returns LocaleMatch with matched locale and strategy used
117
+ */
118
+ negotiate(acceptLanguageHeader: string): LocaleMatch {
119
+ const entries = parseAcceptLanguage(acceptLanguageHeader);
120
+
121
+ // Pass 1: exact match (case-normalised)
122
+ for (const entry of entries) {
123
+ const norm = normaliseLocale(entry.locale);
124
+
125
+ const exact = this.supported.find((s) => normaliseLocale(s) === norm);
126
+ if (exact) {
127
+ return { locale: exact, strategy: "exact", source: entry.locale };
128
+ }
129
+ }
130
+
131
+ // Pass 2: language prefix match ("en-US" matches supported "en")
132
+ for (const entry of entries) {
133
+ const lang = languageSubtag(entry.locale);
134
+
135
+ const prefix = this.supported.find((s) => languageSubtag(s) === lang);
136
+ if (prefix) {
137
+ return { locale: prefix, strategy: "prefix", source: entry.locale };
138
+ }
139
+ }
140
+
141
+ // Default fallback
142
+ return {
143
+ locale: this.defaultLocale,
144
+ strategy: "default",
145
+ source: "",
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Validate whether a given locale string is in the supported list.
151
+ * Used to validate cookie values before trusting them.
152
+ *
153
+ * Performs strict membership check — does not normalise or approximate.
154
+ * If a cookie contains "fr-CA" but only "fr" is supported, this returns false.
155
+ *
156
+ * @param locale Locale to validate
157
+ * @returns true if locale is in supportedLocales
158
+ */
159
+ isSupported(locale: string): boolean {
160
+ return this.supported.includes(locale);
161
+ }
162
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * i18n Types and Interfaces
3
+ *
4
+ * Core types for the internationalisation system:
5
+ * locale negotiation, translation lookup, plural forms, and configuration.
6
+ */
7
+
8
+ // ============= CLDR Plural Keys =============
9
+
10
+ /**
11
+ * CLDR-standard plural form categories.
12
+ * Used to select the appropriate plural variant of a translation.
13
+ *
14
+ * @see https://cldr.unicode.org/index/cldr-spec/plural-rules
15
+ */
16
+ export type PluralKey = "zero" | "one" | "two" | "few" | "many" | "other";
17
+
18
+ // ============= Translation Types =============
19
+
20
+ /**
21
+ * Raw translation file shape — can be flat or nested JSON.
22
+ * @example
23
+ * { "nav.home": "Home" } or { "nav": { "home": "Home" } }
24
+ */
25
+ export type TranslationMap = Record<string, unknown>;
26
+
27
+ /**
28
+ * Flat map of dot-notation keys → string values.
29
+ * The loader always normalises nested keys to this format before caching.
30
+ * @example
31
+ * Map { "nav.home" → "Home", "nav.about" → "About" }
32
+ */
33
+ export type FlatTranslations = Map<string, string>;
34
+
35
+ /**
36
+ * Parameters passed to the translation function.
37
+ * `count` is special — it drives plural form selection.
38
+ * Other keys are used for variable interpolation ({{ key }}).
39
+ */
40
+ export interface TranslationParams {
41
+ count?: number;
42
+ [key: string]: unknown;
43
+ }
44
+
45
+ /**
46
+ * The t() function signature stored on context and returned by the engine.
47
+ * @example
48
+ * t('greeting', { name: 'Alice' }) → "Hello, Alice!"
49
+ */
50
+ export type TranslationFunction = (
51
+ key: string,
52
+ params?: TranslationParams,
53
+ ) => string;
54
+
55
+ // ============= Locale Negotiation =============
56
+
57
+ /**
58
+ * Result of a locale negotiation attempt against supported locales.
59
+ * Used to determine which locale to load based on client preferences.
60
+ */
61
+ export interface LocaleMatch {
62
+ /** The matched locale from supportedLocales (e.g. "fr") */
63
+ locale: string;
64
+ /** How the match was found: exact string match, language prefix match, or default */
65
+ strategy: "exact" | "prefix" | "default";
66
+ /** The original value that was negotiated (e.g. "fr-CA" from Accept-Language) */
67
+ source: string;
68
+ }
69
+
70
+ // ============= I18n Context (what is stored on ctx) =============
71
+
72
+ /**
73
+ * What the middleware stores on the Bueno Context instance.
74
+ * Accessed via ctx.get('locale') and ctx.get('t').
75
+ * Typed helpers getLocale(ctx) and getT(ctx) are recommended.
76
+ */
77
+ export interface I18nContext {
78
+ locale: string;
79
+ t: TranslationFunction;
80
+ }
81
+
82
+ // ============= I18n Configuration =============
83
+
84
+ /**
85
+ * User-facing i18n configuration.
86
+ * All fields are optional; defaults are applied by the engine.
87
+ * Environment variable pattern: BUENO_I18N_*
88
+ */
89
+ export interface I18nConfig {
90
+ /** Enable/disable the i18n system (default: false) */
91
+ enabled?: boolean;
92
+ /** Default locale — used as fallback when requested locale has missing keys (default: "en") */
93
+ defaultLocale?: string;
94
+ /** List of all supported locale identifiers (default: ["en"]) */
95
+ supportedLocales?: string[];
96
+ /** Base directory for locale JSON files (default: "resources/i18n") */
97
+ basePath?: string;
98
+ /**
99
+ * When a key is missing in the requested locale,
100
+ * fall back to the defaultLocale before returning the key string (default: true)
101
+ */
102
+ fallbackToDefault?: boolean;
103
+ /** Cookie name used to persist locale choice (default: "bueno_locale") */
104
+ cookieName?: string;
105
+ /** Cookie max-age in seconds (default: 31536000 = 1 year) */
106
+ cookieMaxAge?: number;
107
+ /** Enable file watching for hot reload in development (default: false) */
108
+ watch?: boolean;
109
+ }
110
+
111
+ // ============= Resolved I18n Config (internal) =============
112
+
113
+ /**
114
+ * Fully-resolved i18n configuration with all defaults applied.
115
+ * All fields are required (non-optional) — used internally by the engine.
116
+ */
117
+ export interface ResolvedI18nConfig {
118
+ defaultLocale: string;
119
+ supportedLocales: string[];
120
+ basePath: string;
121
+ fallbackToDefault: boolean;
122
+ cookieName: string;
123
+ cookieMaxAge: number;
124
+ }
125
+
126
+ // ============= Loader Types =============
127
+
128
+ /**
129
+ * A loaded and cached locale bundle.
130
+ * Contains flattened translations and metadata.
131
+ */
132
+ export interface LocaleBundle {
133
+ /** Locale identifier e.g. "en", "fr-CA" */
134
+ locale: string;
135
+ /** Flat dot-notation key → string value map */
136
+ translations: FlatTranslations;
137
+ /** When this bundle was loaded (ms epoch) */
138
+ loadedAt: number;
139
+ }
140
+
141
+ // ============= Metrics =============
142
+
143
+ /**
144
+ * Metrics collected by the translation engine.
145
+ * Useful for debugging and performance monitoring.
146
+ */
147
+ export interface I18nMetrics {
148
+ /** Total number of t() calls */
149
+ totalLookups: number;
150
+ /** Lookups that found a key in the requested locale */
151
+ hits: number;
152
+ /** Lookups that fell back to the default locale */
153
+ fallbacks: number;
154
+ /** Lookups that returned the key string (complete miss) */
155
+ misses: number;
156
+ /** Locales currently loaded in memory */
157
+ loadedLocales: string[];
158
+ }