bro-framework 2.2.2 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  ---
22
22
 
23
- > "NestJS wants four decorators, three modules, and an existential crisis just to handle a GET request. Express makes you write the same 40 lines of CORS, JSON parsing, and auth middleware for every project. bro.js gives you file routing, auto-validation, JWT auth, WebSockets, and live docs out of the box. Be honest: you just want to return an object."
23
+ > Skip the boilerplate. bro.js gives you file-based routing, automatic Zod validation, JWT auth, and WebSockets right out of the box. Just write your business logic, return an object, and let the framework handle the rest.
24
24
 
25
25
  ---
26
26
 
@@ -97,7 +97,7 @@ export default defineRoute({
97
97
  Create a `.js` file in the `routes/` directory, and it automatically becomes an endpoint. We use Next.js-style bracket syntax for dynamic parameters. A file named `routes/users/[id].get.js` translates natively to a `GET /users/:id` Express route under the hood.
98
98
 
99
99
  ### Bouncer-Grade Validation
100
- Powered by Zod. Attach a schema to `body`, `query`, or `params` in your route definition. If the client sends malformed data, `bro.js` automatically rejects the request with a structured `400 Bad Request` JSON payload *before* your handler ever executes. You never have to manually validate inputs again.
100
+ Powered by Zod. Attach a schema to `body`, `query`, or `params` directly in your route definition. If the client sends malformed data, `bro.js` automatically rejects the request with a structured `400 Bad Request` JSON payload *before* your handler ever executes. You never have to manually validate inputs again. You can also define a `response` schema to strongly type your OpenAPI documentation (strictly opt-in; arbitrary 200s work out of the box).
101
101
 
102
102
  ### Zero-Config JWTs
103
103
  Add `auth: true` to your route config. `bro.js` will intercept the request, extract the `Authorization: Bearer <token>` header, verify the signature using your `jwtSecret`, and inject the decoded payload directly into `ctx.user`.
@@ -115,7 +115,22 @@ Tired of writing frontend `fetch` wrappers? Run `bro sdk`. The CLI will parse yo
115
115
  Don't spin up a separate worker server. Drop a JavaScript file anywhere in the `tasks/` folder, export a cron string (e.g., `"0 0 * * *"`), and an async handler. `bro.js` natively schedules it as a background worker with full access to your injected database and WebSocket contexts.
116
116
 
117
117
  ### Zero-Boilerplate File Uploads
118
- Add `upload: true` to a route. `bro.js` automatically hooks into `multer`, parses the `multipart/form-data` payload in memory, and injects the files directly into `ctx.files`.
118
+ Add `upload: true` to a route. `bro.js` automatically hooks into `multer`, parses the `multipart/form-data` payload in memory, and injects the files directly into `ctx.files`. It also natively supports granular file limits, restricting max sizes, parts, and fielding counts instantly to protect your RAM. (Note: Use the `storage` configuration for heavy production disk writing to prevent memory exhaustion).
119
+
120
+ ### File-Based Locale
121
+ Create a `locale/` folder with one translation file per locale, such as `locale/en.js` and `locale/fr.js`. Export a plain object from each file, then use `t()` in any route:
122
+
123
+ ```javascript
124
+ // locale/fr.js
125
+ export default { welcome: 'Bienvenue, {name} !' };
126
+
127
+ // routes/welcome.get.js
128
+ export default defineRoute({
129
+ handler: async ({ t }) => ({ message: t('welcome', { name: 'Sam' }) })
130
+ });
131
+ ```
132
+
133
+ The locale is negotiated dynamically using RFC 9110 `Accept-Language` headers, supporting full region fallback and custom defaults, and the generated SDK can securely set it via `setLocale('fr')`.
119
134
 
120
135
  ---
121
136
 
@@ -178,9 +193,9 @@ Add `upload: true` to a route. `bro.js` automatically hooks into `multer`, parse
178
193
  | Command | Description |
179
194
  | :--- | :--- |
180
195
  | `bro dev` | Development server featuring instant boot, visual CLI banner, and `chokidar`-powered hot module remapping. |
181
- | `bro start` &nbsp; | Production runner locked down for security. Zero watcher overhead, suppressed internal logs, and isolated API docs. |
196
+ | `bro start` &nbsp; | Production runner locked down for security. Features Graceful Shutdown APIs (with `onShutdown` DB teardown), suppressed internal logs, and isolated API docs. |
182
197
  | `bro init` | Automated workspace scaffolder. Generates configuration files and forcefully ensures your `package.json` respects `"type": "module"`. |
183
- | `bro sdk` | Route parser and browser client compiler. Generates your frontend SDK in one hit. |
198
+ | `bro sdk` | Route parser and browser client compiler. Generates your typed `bro-sdk.js` frontend SDK in one hit. |
184
199
 
185
200
  ---
186
201
 
package/bin/bro.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import path from 'path';
4
4
  import fs from 'fs';
@@ -37,6 +37,12 @@ export default defineConfig({
37
37
  jwtSecret: 'dev_secret_please_change',
38
38
  expiresIn: '7d'
39
39
  },
40
+
41
+ // Optional file-based API translations
42
+ // Add locale/en.js, locale/fr.js, etc.
43
+ locale: {
44
+ defaultLocale: 'en'
45
+ },
40
46
 
41
47
  // API Documentation (Scalar UI)
42
48
  docs: process.env.NODE_ENV !== 'production', // Set to false to disable completely, or true to force in prod
@@ -190,7 +196,8 @@ async function bootstrap() {
190
196
  process.exit(1);
191
197
  }
192
198
 
193
- const { app, server, routes: initialRoutes, reload, io, shutdown } = await createServer(globalConfig, routesDir, db);
199
+ const localeDir = globalConfig.locale?.directory || path.join(cwd, 'locale');
200
+ const { app, server, routes: initialRoutes, reload, reloadLocale, io, shutdown } = await createServer(globalConfig, routesDir, db);
194
201
  const port = globalConfig.port;
195
202
 
196
203
  let currentRoutes = initialRoutes;
@@ -217,20 +224,28 @@ async function bootstrap() {
217
224
 
218
225
  printCurrentRoutes(currentRoutes);
219
226
 
220
- const watcher = chokidar.watch(routesDir, { ignoreInitial: true });
227
+ const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts}';
228
+ const watcher = chokidar.watch([routesDir, localeGlob], { ignoreInitial: true });
221
229
 
222
230
  watcher.on('all', async (event, filepath) => {
223
- if (!filepath.endsWith('.js') && !filepath.endsWith('.ts')) return;
224
-
231
+ const isJavaScriptFile = filepath.endsWith('.js') || filepath.endsWith('.ts') || filepath.endsWith('.mjs');
232
+ if (!isJavaScriptFile) return;
233
+ const relLocale = path.relative(path.resolve(localeDir), filepath);
234
+ const isLocaleFile = !relLocale.startsWith('..') && !path.isAbsolute(relLocale);
235
+
225
236
  try {
226
237
  const reloadStartTime = performance.now();
227
- currentRoutes = await reload();
238
+ if (isLocaleFile) {
239
+ await reloadLocale();
240
+ } else {
241
+ currentRoutes = await reload();
242
+ }
228
243
  const reloadTimeMs = performance.now() - reloadStartTime;
229
244
 
230
- printHotReload(path.basename(filepath), event, reloadTimeMs);
245
+ printHotReload(path.basename(filepath), event, reloadTimeMs, isLocaleFile ? 'Locale' : 'Route');
231
246
  printCurrentRoutes(currentRoutes);
232
247
  } catch (err) {
233
- console.error(`\n ✗ Error hot-reloading routes:`, err);
248
+ console.error(`\n ✗ Error hot-reloading ${isLocaleFile ? 'locale' : 'routes'}:`, err);
234
249
  }
235
250
  });
236
251
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bro-framework",
3
- "version": "2.2.2",
3
+ "version": "2.3.1",
4
4
  "description": "The No-BS Backend Framework for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.d.ts CHANGED
@@ -12,16 +12,18 @@ export interface BroContext<Body = any, Params = any, Query = any> {
12
12
  db?: any;
13
13
  io?: any;
14
14
  files?: any[];
15
+ locale: string;
16
+ t: (key: string, values?: Record<string, unknown>) => string;
15
17
  error?: any;
16
18
  }
17
19
 
18
20
  export interface RouteConfig<Body = any, Params = any, Query = any> {
19
21
  auth?: boolean;
20
22
  upload?: boolean | { limits?: any, fields?: { name: string, maxCount?: number }[], single?: string, array?: string, fileFilter?: any, storage?: any };
21
- schema?: { body?: Body; params?: Params; query?: Query; };
22
23
  body?: Body;
23
24
  params?: Params;
24
25
  query?: Query;
26
+ response?: ZodTypeAny;
25
27
  rateLimit?: {
26
28
  windowMs: number;
27
29
  max: number;
@@ -34,12 +36,23 @@ export function defineRoute<Body = any, Params = any, Query = any>(
34
36
  config: RouteConfig<Body, Params, Query>
35
37
  ): RouteConfig<Body, Params, Query>;
36
38
 
39
+ export function loadLocale(directory: string, options?: { defaultLocale?: string }): Promise<{
40
+ locales: string[];
41
+ defaultLocale: string;
42
+ resolveLocale: (request: any) => string;
43
+ translate: (locale: string, key: string, values?: Record<string, unknown>) => string;
44
+ }>;
45
+
37
46
  export interface BroConfig {
38
47
  env?: ZodTypeAny;
39
48
  server?: {
40
49
  port?: number;
41
50
  cors?: boolean | object;
42
51
  };
52
+ locale?: {
53
+ directory?: string;
54
+ defaultLocale?: string;
55
+ };
43
56
  auth?: {
44
57
  jwtSecret?: string;
45
58
  expiresIn?: string | number;
@@ -59,6 +72,7 @@ export interface BroConfig {
59
72
  };
60
73
  db?: () => Promise<any> | any;
61
74
  sockets?: (io: any, db: any) => Promise<void> | void;
75
+ onShutdown?: (db: any) => Promise<void> | void;
62
76
  }
63
77
 
64
78
  export function defineConfig(config: BroConfig): BroConfig;
package/src/index.js CHANGED
@@ -26,3 +26,4 @@ export function defineConfig(config) {
26
26
  }
27
27
 
28
28
  export { z };
29
+ export { loadLocale } from './locale.js';
package/src/locale.js ADDED
@@ -0,0 +1,118 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ import crypto from 'node:crypto';
5
+
6
+ const LOCALE_EXTENSIONS = new Set(['.js', '.mjs', '.ts']);
7
+
8
+ function localeFromFilename(fileName) {
9
+ return path.basename(fileName, path.extname(fileName));
10
+ }
11
+
12
+ function normalizeLocale(locale) {
13
+ const normalized = String(locale || '').trim().replaceAll('_', '-').toLowerCase();
14
+ const aliases = {
15
+ english: 'en',
16
+ arabic: 'ar',
17
+ french: 'fr',
18
+ spanish: 'es',
19
+ german: 'de',
20
+ portuguese: 'pt',
21
+ italian: 'it',
22
+ japanese: 'ja',
23
+ korean: 'ko',
24
+ chinese: 'zh'
25
+ };
26
+ return aliases[normalized] || normalized;
27
+ }
28
+
29
+ function findLocale(locales, requestedLocale, fallbackLocale) {
30
+ const requested = normalizeLocale(requestedLocale);
31
+ const exact = locales.find(locale => normalizeLocale(locale) === requested);
32
+ if (exact) return exact;
33
+
34
+ const language = requested.split('-')[0];
35
+ const languageMatch = locales.find(locale => normalizeLocale(locale).split('-')[0] === language);
36
+ if (languageMatch) return languageMatch;
37
+
38
+ return fallbackLocale;
39
+ }
40
+
41
+ function readMessage(messages, key) {
42
+ return key.split('.').reduce((value, part) => value?.[part], messages);
43
+ }
44
+
45
+ function interpolate(message, values) {
46
+ return message.replace(/\{(\w+)\}/g, (_, name) => {
47
+ return values[name] === undefined ? `{${name}}` : String(values[name]);
48
+ });
49
+ }
50
+
51
+ function parseAcceptLanguage(header) {
52
+ return String(header || '')
53
+ .split(',')
54
+ .map(value => {
55
+ const [tag, qVal] = value.split(';').map(s => s.trim());
56
+ let q = 1;
57
+ if (qVal && qVal.startsWith('q=')) {
58
+ const parsedQ = parseFloat(qVal.slice(2));
59
+ if (!isNaN(parsedQ)) q = parsedQ;
60
+ }
61
+ return { tag, q };
62
+ })
63
+ .filter(item => item.tag && item.q > 0)
64
+ .sort((a, b) => b.q - a.q)
65
+ .map(item => item.tag);
66
+ }
67
+
68
+ /**
69
+ * Loads translation files from the application's locale directory.
70
+ * Each file name becomes a locale, for example locale/en-US.js.
71
+ * @param {string} directory
72
+ * @param {{ defaultLocale?: string }} [options]
73
+ */
74
+ export async function loadLocale(directory, options = {}) {
75
+ const files = fs.existsSync(directory)
76
+ ? fs.readdirSync(directory).filter(file => {
77
+ const fullPath = path.join(directory, file);
78
+ return fs.statSync(fullPath).isFile() && LOCALE_EXTENSIONS.has(path.extname(file));
79
+ })
80
+ : [];
81
+
82
+ const messages = {};
83
+ for (const file of files) {
84
+ const module = await import(`${pathToFileURL(path.join(directory, file)).href}?update=${crypto.randomUUID()}`);
85
+ const catalog = module.default || module.messages || module;
86
+ if (catalog && typeof catalog === 'object') {
87
+ messages[localeFromFilename(file)] = catalog;
88
+ }
89
+ }
90
+
91
+ const locales = Object.keys(messages);
92
+ const configuredDefault = options.defaultLocale;
93
+ const defaultLocale = findLocale(locales, configuredDefault, null)
94
+ || locales.find(locale => ['en', 'en-us'].includes(normalizeLocale(locale)))
95
+ || locales[0]
96
+ || 'en';
97
+
98
+ function resolveLocale(request) {
99
+ const requestedLocales = parseAcceptLanguage(request?.headers?.['accept-language']);
100
+ for (const requested of requestedLocales) {
101
+ const match = findLocale(locales, requested, null);
102
+ if (match) return match;
103
+ }
104
+ return defaultLocale;
105
+ }
106
+
107
+ function translate(locale, key, values = {}) {
108
+ const selectedLocale = findLocale(locales, locale, defaultLocale);
109
+ const message = readMessage(messages[selectedLocale], key)
110
+ ?? readMessage(messages[defaultLocale], key);
111
+
112
+ if (message === undefined) return key;
113
+ if (typeof message !== 'string') return message;
114
+ return interpolate(message, values);
115
+ }
116
+
117
+ return { locales, defaultLocale, resolveLocale, translate };
118
+ }
package/src/logger.js CHANGED
@@ -72,8 +72,8 @@ export function printRoute(method, routePath, hasAuth, isLast = false) {
72
72
  console.log(` ${colors.dim}${branch}${colors.reset} ${coloredMethod} ${routePath}${authIcon}`);
73
73
  }
74
74
 
75
- export function printHotReload(fileName, event, reloadTimeMs) {
75
+ export function printHotReload(fileName, event, reloadTimeMs, resourceType = 'Route') {
76
76
  const time = typeof reloadTimeMs === 'number' ? reloadTimeMs.toFixed(0) : reloadTimeMs;
77
- console.log(`\n ${colors.cyan}Route updated:${colors.reset} ${colors.bold}${fileName}${colors.reset} ${colors.dim}(${event})${colors.reset}`);
77
+ console.log(`\n ${colors.cyan}${resourceType} updated:${colors.reset} ${colors.bold}${fileName}${colors.reset} ${colors.dim}(${event})${colors.reset}`);
78
78
  console.log(` ${colors.dim}Remapped in ${time}ms${colors.reset}\n`);
79
79
  }
package/src/router.js CHANGED
@@ -116,10 +116,15 @@ export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
116
116
  summary: config.summary || `${method.toUpperCase()} ${routePath}`,
117
117
  responses: { '200': { description: 'Successful response' } }
118
118
  };
119
-
120
- const bodySchema = config.schema?.body || config.body;
121
- const paramsSchema = config.schema?.params || config.params;
122
- const querySchema = config.schema?.query || config.query;
119
+
120
+ if (config.response) {
121
+ operation.responses['200'].content = {
122
+ 'application/json': { schema: zodToJsonSchema(config.response) }
123
+ };
124
+ }
125
+ const bodySchema = config.body;
126
+ const paramsSchema = config.params;
127
+ const querySchema = config.query;
123
128
 
124
129
  if (bodySchema) {
125
130
  operation.requestBody = {
package/src/sdk.js CHANGED
@@ -21,11 +21,16 @@ export async function generateSDK() {
21
21
  const code = `// Auto-generated by bro.js
22
22
  const CONFIG = {
23
23
  baseURL: 'http://localhost:5000',
24
- tokenKey: 'bro_token'
24
+ tokenKey: 'bro_token',
25
+ locale: undefined
25
26
  };
26
27
 
27
28
  async function request(method, path, data) {
28
29
  const headers = {};
30
+
31
+ if (CONFIG.locale) {
32
+ headers['Accept-Language'] = CONFIG.locale;
33
+ }
29
34
 
30
35
  if (typeof localStorage !== 'undefined') {
31
36
  const token = localStorage.getItem(CONFIG.tokenKey);
@@ -76,6 +81,10 @@ export function setBaseURL(url) {
76
81
  export function setTokenKey(key) {
77
82
  CONFIG.tokenKey = key;
78
83
  }
84
+
85
+ export function setLocale(locale) {
86
+ CONFIG.locale = locale;
87
+ }
79
88
  `;
80
89
 
81
90
  fs.writeFileSync(path.join(process.cwd(), 'bro-sdk.js'), code, 'utf-8');
package/src/server.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import express from 'express';
2
2
  import cors from 'cors';
3
3
  import http from 'node:http';
4
+ import path from 'node:path';
4
5
  import { Server } from 'socket.io';
5
6
  import rateLimit from 'express-rate-limit';
6
7
  import multer from 'multer';
7
8
  import { apiReference } from '@scalar/express-api-reference';
8
9
  import { verifyJwt, signJwt } from './auth.js';
10
+ import { loadLocale } from './locale.js';
9
11
  import { loadRoutes } from './router.js';
10
12
  import { scanTasks } from './tasks.js';
11
13
 
@@ -14,7 +16,7 @@ import { scanTasks } from './tasks.js';
14
16
  * @param {Object} globalConfig - User's bro.config.js configurations.
15
17
  * @param {string} routesDir - Path to the target routes directory.
16
18
  * @param {any} db - Initialized database instance.
17
- * @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, io: import('socket.io').Server }>}
19
+ * @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, reloadLocale: Function, io: import('socket.io').Server, shutdown: Function }>}
18
20
  */
19
21
  export async function createServer(globalConfig, routesDir, db) {
20
22
  if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
@@ -23,6 +25,8 @@ export async function createServer(globalConfig, routesDir, db) {
23
25
 
24
26
  const app = express();
25
27
  const server = http.createServer(app);
28
+ const localeDirectory = globalConfig.locale?.directory || path.join(process.cwd(), 'locale');
29
+ let locale = await loadLocale(localeDirectory, globalConfig.locale);
26
30
 
27
31
  const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : true;
28
32
 
@@ -41,12 +45,14 @@ export async function createServer(globalConfig, routesDir, db) {
41
45
  if (globalConfig.sockets) {
42
46
  await globalConfig.sockets(io, db);
43
47
  }
44
-
45
48
  const createHandler = (routeConfig) => {
46
49
  const middlewares = [];
47
- const bodySchema = routeConfig.schema?.body || routeConfig.body;
48
- const paramsSchema = routeConfig.schema?.params || routeConfig.params;
49
- const querySchema = routeConfig.schema?.query || routeConfig.query;
50
+ if (routeConfig.schema) {
51
+ throw new Error("Nested 'schema' object is no longer supported in bro.js v2.3.0+. Please use flat, top-level properties (body, query, params) instead.");
52
+ }
53
+ const bodySchema = routeConfig.body;
54
+ const paramsSchema = routeConfig.params;
55
+ const querySchema = routeConfig.query;
50
56
 
51
57
  if (routeConfig.rateLimit) {
52
58
  middlewares.push(rateLimit(routeConfig.rateLimit));
@@ -80,6 +86,7 @@ export async function createServer(globalConfig, routesDir, db) {
80
86
 
81
87
  middlewares.push(async (req, res) => {
82
88
  try {
89
+ const requestLocale = locale.resolveLocale(req);
83
90
  const ctx = {
84
91
  env: globalConfig.envData || process.env,
85
92
  db,
@@ -88,6 +95,8 @@ export async function createServer(globalConfig, routesDir, db) {
88
95
  params: req.params,
89
96
  query: req.query,
90
97
  files: req.files || req.file,
98
+ locale: requestLocale,
99
+ t: (key, values) => locale.translate(requestLocale, key, values),
91
100
  user: null,
92
101
  jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
93
102
  error: (status, message) => {
@@ -97,21 +106,18 @@ export async function createServer(globalConfig, routesDir, db) {
97
106
  }
98
107
  };
99
108
 
100
- if (routeConfig.auth) {
101
- const authHeader = req.headers.authorization;
102
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
103
- return res.status(401).json({ error: 'Unauthorized', details: 'Missing or invalid Bearer token' });
104
- }
105
-
106
- const token = authHeader.split(' ')[1];
107
- const authResult = verifyJwt(token, globalConfig.jwtSecret);
108
-
109
- if (!authResult.valid) {
110
- return res.status(401).json({ error: 'Unauthorized', details: authResult.error });
111
- }
112
-
113
- ctx.user = authResult.payload;
109
+ const authHeader = req.headers.authorization;
110
+ if ((!authHeader || !authHeader.startsWith('Bearer ')) && routeConfig.auth) {
111
+ return res.status(401).json({ error: 'Unauthorized', details: 'Missing or invalid Bearer token' });
114
112
  }
113
+
114
+ const token = authHeader?.split(' ')[1] ?? '';
115
+ const authResult = verifyJwt(token, globalConfig.jwtSecret);
116
+
117
+ if (!authResult.valid && routeConfig.auth) {
118
+ return res.status(401).json({ error: 'Unauthorized', details: authResult.error });
119
+ }
120
+ ctx.user = authResult?.payload ?? null;
115
121
 
116
122
  if (paramsSchema) {
117
123
  const result = paramsSchema.safeParse(req.params);
@@ -235,17 +241,34 @@ export async function createServer(globalConfig, routesDir, db) {
235
241
  return routes;
236
242
  };
237
243
 
244
+ const reloadLocale = async () => {
245
+ locale = await loadLocale(localeDirectory, globalConfig.locale);
246
+ return locale;
247
+ };
248
+
238
249
  const initialRoutes = await reload();
239
250
 
240
251
  const taskManager = await scanTasks({ db, io });
241
252
 
253
+ let isShuttingDown = false;
242
254
  const shutdown = async () => {
255
+ if (isShuttingDown) return;
256
+ isShuttingDown = true;
243
257
  if (taskManager) taskManager.stopAll();
244
258
  if (io) io.close();
259
+
260
+ if (typeof globalConfig.onShutdown === 'function') {
261
+ try {
262
+ await globalConfig.onShutdown(db);
263
+ } catch (err) {
264
+ console.error('[bro.js] Error during database teardown hook:', err);
265
+ }
266
+ }
267
+
245
268
  return new Promise((resolve) => {
246
269
  server.close(() => resolve());
247
270
  });
248
271
  };
249
272
 
250
- return { app, server, routes: initialRoutes, reload, io, shutdown };
273
+ return { app, server, routes: initialRoutes, reload, reloadLocale, io, shutdown };
251
274
  }