@laot/nuix 1.0.0 → 1.0.2

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
@@ -1,11 +1,18 @@
1
1
  # NUIX
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@laot/nuix.svg)](https://www.npmjs.com/package/@laot/nuix)
4
+ [![license](https://img.shields.io/npm/l/@laot/nuix.svg)](https://github.com/laot7490/nuix/blob/main/LICENSE)
5
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@laot/nuix)](https://bundlephobia.com/package/@laot/nuix)
6
+
3
7
  > Modular, type-safe TypeScript library for FiveM NUI projects. Zero runtime dependencies.
4
8
 
5
9
  ## Install
6
10
 
7
11
  ```bash
8
12
  npm install @laot/nuix
13
+ pnpm add @laot/nuix
14
+ yarn add @laot/nuix
15
+ bun add @laot/nuix
9
16
  ```
10
17
 
11
18
  ## Quick Start
@@ -16,8 +23,9 @@ npm install @laot/nuix
16
23
  import type { NuiEventMap } from "@laot/nuix";
17
24
 
18
25
  interface MyEvents extends NuiEventMap {
19
- getPlayer: { request: { id: number }; response: { name: string; level: number } };
26
+ getPlayer: { request: { id: number }; response: { name: string; level: number } };
20
27
  sendNotify: { request: { message: string }; response: void };
28
+ showMenu: { request: { items: string[] }; response: void };
21
29
  }
22
30
  ```
23
31
 
@@ -28,9 +36,8 @@ import { createFetchNui } from "@laot/nuix";
28
36
 
29
37
  const fetchNui = createFetchNui<MyEvents>();
30
38
 
31
- // Fully typed — request and response inferred from event map
32
39
  const player = await fetchNui("getPlayer", { id: 1 });
33
- console.log(player.name); // ✅ typed as string
40
+ console.log(player.name); // string
34
41
 
35
42
  await fetchNui("sendNotify", { message: "Hello!" });
36
43
 
@@ -40,20 +47,31 @@ const data = await fetchNui("getPlayer", { id: 2 }, { timeout: 5000 });
40
47
 
41
48
  ### 3. NUI Message Listener
42
49
 
50
+ **Switch-case** — single listener for all actions:
51
+
43
52
  ```ts
44
53
  import { onNuiMessage } from "@laot/nuix";
45
54
 
46
- interface MyMessages extends NuiEventMap {
47
- showMenu: { request: { items: string[] }; response: void };
48
- hideMenu: { request: void; response: void };
49
- }
55
+ const unsub = onNuiMessage<MyEvents>((action, data) => {
56
+ switch (action) {
57
+ case "getPlayer":
58
+ console.log(data.name);
59
+ break;
60
+ case "sendNotify":
61
+ console.log(data.message);
62
+ break;
63
+ }
64
+ });
50
65
 
51
- const unsub = onNuiMessage<MyMessages, "showMenu">("showMenu", (data) => {
66
+ unsub(); // stop listening
67
+ ```
68
+
69
+ **Per-action** — filtered by action, `data` is fully typed:
70
+
71
+ ```ts
72
+ const unsub = onNuiMessage<MyEvents, "showMenu">("showMenu", (data) => {
52
73
  console.log(data.items); // ✅ typed as string[]
53
74
  });
54
-
55
- // Clean up
56
- unsub();
57
75
  ```
58
76
 
59
77
  ### 4. Lua-Style Formatter
@@ -68,63 +86,89 @@ luaFormat("Accuracy: %f%%", 99.5);
68
86
  // → "Accuracy: 99.5%"
69
87
  ```
70
88
 
71
- ### 5. Translator
89
+ ### 5. Translator (Global)
90
+
91
+ Register locales once at runtime (e.g. when Lua sends them), then use `_U` anywhere:
92
+
93
+ ```ts
94
+ import { registerLocales, _U, onNuiMessage } from "@laot/nuix";
95
+ import type { NuiEventMap, LocaleRecord } from "@laot/nuix";
96
+
97
+ interface Events extends NuiEventMap {
98
+ setLocales: { request: LocaleRecord; response: void };
99
+ showMenu: { request: { items: string[] }; response: void };
100
+ }
101
+
102
+ onNuiMessage<Events>((action, data) => {
103
+ switch (action) {
104
+ case "setLocales":
105
+ registerLocales(data);
106
+ break;
107
+ case "showMenu":
108
+ openMenu(data.items);
109
+ break;
110
+ }
111
+ });
112
+
113
+ // Use _U anywhere
114
+ _U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
115
+ _U("ui.level", "Lv.", 42); // → "Level 42"
116
+ _U("missing.key", "Fallback"); // → "Fallback"
117
+ ```
118
+
119
+ You can also extend locales incrementally with `extendLocales`:
120
+
121
+ ```ts
122
+ import { extendLocales } from "@laot/nuix";
123
+
124
+ extendLocales({ ui: { subtitle: "Overview" } });
125
+ // Merges into existing locales without replacing them
126
+ ```
127
+
128
+ ### 6. Translator (Isolated)
129
+
130
+ If you need a separate translator instance with its own locale scope:
72
131
 
73
132
  ```ts
74
133
  import { createTranslator, mergeLocales } from "@laot/nuix";
75
134
 
76
- const _U = createTranslator({
135
+ const _T = createTranslator({
77
136
  locales: {
78
- client: {
79
- greeting: "Hello %s!",
80
- level: "Level %d",
81
- },
82
- server: {
83
- error: "Error: %s",
84
- },
85
- flat_key: "Plain message: %s",
137
+ greeting: "Hello %s!",
138
+ level: "Level %d",
86
139
  },
87
140
  });
88
141
 
89
- _U("client.greeting", "MISSING", "Laot"); // → "Hello Laot!"
90
- _U("client.level", "MISSING", 42); // → "Level 42"
91
- _U("flat_key", "MISSING", "test"); // → "Plain message: test"
92
- _U("no.key", "Not found"); // → "Not found"
93
-
94
- // Deep-merge multiple locale files
95
- const base = { client: { greeting: "Hello %s!" } };
96
- const overrides = { client: { greeting: "Hey %s, welcome back!" } };
97
- const merged = mergeLocales(base, overrides);
98
- const _T = createTranslator({ locales: merged });
142
+ _T("greeting", "MISSING", "Laot"); // → "Hello Laot!"
143
+ _T("level", "MISSING", 42); // → "Level 42"
99
144
 
100
- _T("client.greeting", "MISSING", "Laot"); // "Hey Laot, welcome back!"
145
+ // Deep-merge multiple locale records
146
+ const base = { ui: { greeting: "Hello %s!" } };
147
+ const patch = { ui: { greeting: "Hey %s, welcome back!" } };
148
+ const merged = mergeLocales(base, patch);
101
149
  ```
102
150
 
103
- ### 6. Debug Mode
151
+ ### 7. Debug Mode
104
152
 
105
- Enable console logging for every `fetchNui` call — useful during local development:
153
+ Enable console logging for every `fetchNui` call:
106
154
 
107
155
  ```ts
108
156
  const fetchNui = createFetchNui<MyEvents>({ debug: true });
109
157
 
110
158
  await fetchNui("getPlayer", { id: 1 });
111
- // Console:
112
159
  // [NUIX] → getPlayer { id: 1 }
113
160
  // [NUIX] ← getPlayer { name: "Laot", level: 42 }
114
161
  ```
115
162
 
116
- ### 7. Mock Data (Local Development)
163
+ ### 8. Mock Data (Local Development)
117
164
 
118
- When developing outside FiveM, `fetchNui` can return pre-defined mock responses instead of making real HTTP calls:
165
+ Return pre-defined responses without real HTTP calls — useful when developing outside FiveM:
119
166
 
120
167
  ```ts
121
168
  const fetchNui = createFetchNui<MyEvents>({
122
169
  debug: true,
123
170
  mockData: {
124
- // Static response
125
171
  getPlayer: { name: "DevPlayer", level: 99 },
126
-
127
- // Dynamic response based on request
128
172
  sendNotify: (req) => {
129
173
  console.log("Mock notification:", req.message);
130
174
  },
@@ -132,22 +176,24 @@ const fetchNui = createFetchNui<MyEvents>({
132
176
  });
133
177
 
134
178
  const player = await fetchNui("getPlayer", { id: 1 });
135
- // Console: [NUIX] → getPlayer { id: 1 }
136
- // Console: [NUIX] ← getPlayer (mock) { name: "DevPlayer", level: 99 }
137
- // player = { name: "DevPlayer", level: 99 }
179
+ // [NUIX] → getPlayer { id: 1 }
180
+ // [NUIX] ← getPlayer (mock) { name: "DevPlayer", level: 99 }
138
181
  ```
139
182
 
140
- ## Lua Callback Example
183
+ ## Lua Examples
141
184
 
142
185
  ```lua
143
- -- client side
186
+ -- NUI callback (responds to fetchNui calls)
144
187
  RegisterNUICallback("getPlayer", function(data, cb)
145
188
  local player = GetPlayerData(data.id)
146
189
  cb({ name = player.name, level = player.level })
147
190
  end)
148
191
 
149
- -- sending messages to NUI
192
+ -- Send messages to NUI
150
193
  SendNUIMessage({ action = "showMenu", data = { items = {"Pistol", "Rifle"} } })
194
+
195
+ -- Send locales to NUI (for registerLocales)
196
+ SendNUIMessage({ action = "setLocales", data = Locales })
151
197
  ```
152
198
 
153
199
  ## API Reference
@@ -155,9 +201,13 @@ SendNUIMessage({ action = "showMenu", data = { items = {"Pistol", "Rifle"} } })
155
201
  | Export | Type | Description |
156
202
  |---|---|---|
157
203
  | `createFetchNui<TMap>(options?)` | Factory | Returns a typed `fetchNui` function (supports debug & mock) |
158
- | `onNuiMessage<TMap, K>(action, handler)` | Function | Listens for NUI messages by action |
204
+ | `onNuiMessage<TMap>(handler)` | Function | Single listener for all actions (switch-case) |
205
+ | `onNuiMessage<TMap, K>(action, handler)` | Function | Per-action listener with typed data |
159
206
  | `luaFormat(template, ...args)` | Function | Lua-style `%s`/`%d`/`%f` formatter |
160
- | `createTranslator(options)` | Factory | Returns a `_U` translator function |
207
+ | `registerLocales(locales)` | Function | Sets the global locale map at runtime |
208
+ | `extendLocales(...records)` | Function | Merges new entries into the global locale map |
209
+ | `_U(key, fallback, ...args)` | Function | Global translator — reads from registered locales |
210
+ | `createTranslator(options)` | Factory | Returns an isolated translator function |
161
211
  | `mergeLocales(...records)` | Function | Deep-merges locale records |
162
212
 
163
213
  ## Build
package/dist/index.cjs CHANGED
@@ -20,11 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ _U: () => _U,
23
24
  createFetchNui: () => createFetchNui,
24
25
  createTranslator: () => createTranslator,
26
+ extendLocales: () => extendLocales,
25
27
  luaFormat: () => luaFormat,
26
28
  mergeLocales: () => mergeLocales,
27
- onNuiMessage: () => onNuiMessage
29
+ onNuiMessage: () => onNuiMessage,
30
+ registerLocales: () => registerLocales
28
31
  });
29
32
  module.exports = __toCommonJS(index_exports);
30
33
 
@@ -38,7 +41,8 @@ function getResourceName() {
38
41
  function createFetchNui(factoryOptions) {
39
42
  const debug = factoryOptions?.debug ?? false;
40
43
  const mockData = factoryOptions?.mockData;
41
- return async function fetchNui(event, data, options) {
44
+ return async function fetchNui(event, ...args) {
45
+ const [data, options] = args;
42
46
  if (debug) {
43
47
  console.log(`[NUIX] \u2192 ${event}`, data ?? {});
44
48
  }
@@ -86,12 +90,17 @@ function createFetchNui(factoryOptions) {
86
90
  }
87
91
 
88
92
  // src/listener.ts
89
- function onNuiMessage(action, handler) {
93
+ function onNuiMessage(actionOrHandler, handler) {
90
94
  const listener = (event) => {
91
95
  const payload = event.data;
92
96
  if (!payload || typeof payload !== "object") return;
93
- if (payload.action !== action) return;
94
- handler(payload.data);
97
+ if (!payload.action) return;
98
+ if (typeof actionOrHandler === "string") {
99
+ if (payload.action !== actionOrHandler) return;
100
+ handler(payload.data);
101
+ } else {
102
+ actionOrHandler(payload.action, payload.data);
103
+ }
95
104
  };
96
105
  window.addEventListener("message", listener);
97
106
  return () => {
@@ -143,12 +152,24 @@ function createTranslator(options) {
143
152
  return args.length > 0 ? luaFormat(template, ...args) : template;
144
153
  };
145
154
  }
155
+ var _locales = {};
156
+ function registerLocales(locales) {
157
+ _locales = locales;
158
+ }
159
+ function extendLocales(...records) {
160
+ _locales = mergeLocales(_locales, ...records);
161
+ }
162
+ function _U(key, fallback, ...args) {
163
+ const template = resolveKey(_locales, key);
164
+ if (template === void 0) return fallback;
165
+ return args.length > 0 ? luaFormat(template, ...args) : template;
166
+ }
146
167
  function mergeLocales(...records) {
147
168
  const result = {};
148
169
  for (const record of records) {
149
170
  for (const [key, value] of Object.entries(record)) {
150
171
  const existing = result[key];
151
- if (typeof value === "object" && typeof existing === "object" && existing !== void 0) {
172
+ if (value !== null && existing !== null && typeof value === "object" && typeof existing === "object" && existing !== void 0) {
152
173
  result[key] = mergeLocales(existing, value);
153
174
  } else {
154
175
  result[key] = value;
@@ -159,10 +180,13 @@ function mergeLocales(...records) {
159
180
  }
160
181
  // Annotate the CommonJS export names for ESM import in node:
161
182
  0 && (module.exports = {
183
+ _U,
162
184
  createFetchNui,
163
185
  createTranslator,
186
+ extendLocales,
164
187
  luaFormat,
165
188
  mergeLocales,
166
- onNuiMessage
189
+ onNuiMessage,
190
+ registerLocales
167
191
  });
168
192
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/listener.ts","../src/utils.ts"],"sourcesContent":["// ─── Types ───\r\nexport type {\r\n\tNuiEventMap,\r\n\tNuiMessagePayload,\r\n\tFetchNuiOptions,\r\n\tFetchNuiFactoryOptions,\r\n\tLocaleRecord,\r\n\tTranslatorOptions,\r\n\tTranslatorFn,\r\n\tFormatArg,\r\n\tUnsubscribeFn,\r\n\tNuiMessageHandler,\r\n} from \"./types\";\r\n\r\n// ─── Client ───\r\nexport { createFetchNui } from \"./client\";\r\n\r\n// ─── Listener ───\r\nexport { onNuiMessage } from \"./listener\";\r\n\r\n// ─── Utils ───\r\nexport { luaFormat, createTranslator, mergeLocales } from \"./utils\";\r\n","import type { NuiEventMap, FetchNuiOptions, FetchNuiFactoryOptions } from \"./types\";\r\n\r\n// ─── Resource Name ───\r\n\r\n/**\r\n * Grabs the resource name from FiveM's injected global.\r\n * Falls back to `\"nui-frame-app\"` when running outside the game (local dev, tests).\r\n */\r\nfunction getResourceName(): string {\r\n\tif (typeof window !== \"undefined\" && window.GetParentResourceName) {\r\n\t\treturn window.GetParentResourceName();\r\n\t}\r\n\treturn \"nui-frame-app\";\r\n}\r\n\r\n// ─── FetchNui Factory ───\r\n\r\n/**\r\n * Creates a fully typed `fetchNui` function tied to your event map.\r\n *\r\n * The returned function POSTs JSON to `https://<resourceName>/<event>`,\r\n * which maps 1:1 to `RegisterNUICallback` on the Lua side.\r\n *\r\n * Supports optional debug logging, mock data for local dev, and per-call timeout.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { request: { id: number }; response: { name: string } };\r\n * notify: { request: { msg: string }; response: void };\r\n * }\r\n *\r\n * // Production usage\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n * // player.name is typed as string\r\n *\r\n * // Local dev with mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (req) => { console.log(\"Mock:\", req.msg); },\r\n * },\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * RegisterNUICallback(\"getPlayer\", function(data, cb)\r\n * local player = GetPlayerData(data.id)\r\n * cb({ name = player.name })\r\n * end)\r\n * ```\r\n */\r\nexport function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>) {\r\n\tconst debug = factoryOptions?.debug ?? false;\r\n\tconst mockData = factoryOptions?.mockData;\r\n\r\n\treturn async function fetchNui<K extends keyof TMap & string>(\r\n\t\tevent: K,\r\n\t\tdata?: TMap[K][\"request\"],\r\n\t\toptions?: FetchNuiOptions,\r\n\t): Promise<TMap[K][\"response\"]> {\r\n\t\tif (debug) {\r\n\t\t\tconsole.log(`[NUIX] → ${event}`, data ?? {});\r\n\t\t}\r\n\r\n\t\t// ─── Mock Mode ───\r\n\r\n\t\tif (mockData && event in mockData) {\r\n\t\t\tconst mock = mockData[event];\r\n\r\n\t\t\tif (mock === undefined) {\r\n\t\t\t\tthrow new Error(`[NUIX] Mock data for \"${event}\" is undefined`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result =\r\n\t\t\t\ttypeof mock === \"function\"\r\n\t\t\t\t\t? (mock as (req: TMap[K][\"request\"]) => TMap[K][\"response\"])(data as TMap[K][\"request\"])\r\n\t\t\t\t\t: mock;\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event} (mock)`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result as TMap[K][\"response\"];\r\n\t\t}\r\n\r\n\t\t// ─── Real Fetch ───\r\n\r\n\t\tconst url = `https://${getResourceName()}/${event}`;\r\n\r\n\t\tconst controller = options?.timeout ? new AbortController() : undefined;\r\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n\t\tif (controller && options?.timeout) {\r\n\t\t\ttimeoutId = setTimeout(() => controller.abort(), options.timeout);\r\n\t\t}\r\n\r\n\t\ttry {\r\n\t\t\tconst response = await fetch(url, {\r\n\t\t\t\tmethod: \"POST\",\r\n\t\t\t\theaders: { \"Content-Type\": \"application/json; charset=UTF-8\" },\r\n\t\t\t\tbody: JSON.stringify(data ?? {}),\r\n\t\t\t\tsignal: controller?.signal,\r\n\t\t\t});\r\n\r\n\t\t\tif (!response.ok) {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") failed with HTTP ${response.status}`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result = (await response.json()) as TMap[K][\"response\"];\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event}`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result;\r\n\t\t} catch (error) {\r\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") timed out after ${options?.timeout}ms`);\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t} finally {\r\n\t\t\tif (timeoutId !== undefined) clearTimeout(timeoutId);\r\n\t\t}\r\n\t};\r\n}\r\n","import type { NuiEventMap, NuiMessagePayload, NuiMessageHandler, UnsubscribeFn } from \"./types\";\r\n\r\n// ─── NUI Message Listener ───\r\n\r\n/**\r\n * Listens for NUI messages from Lua (`SendNUIMessage`), filtered by action name.\r\n * Only messages matching the given `action` trigger the handler.\r\n * Returns a cleanup function to stop listening.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyMessages extends NuiEventMap {\r\n * showMenu: { request: { items: string[] }; response: void };\r\n * hideMenu: { request: void; response: void };\r\n * }\r\n *\r\n * const unsub = onNuiMessage<MyMessages, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // typed as string[]\r\n * });\r\n *\r\n * // Stop listening when done\r\n * unsub();\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * SendNUIMessage({ action = \"showMenu\", data = { items = {\"Pistol\", \"Rifle\"} } })\r\n * ```\r\n */\r\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"request\"]>,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload<TMap[K][\"request\"]>>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (payload.action !== action) return;\r\n\r\n\t\thandler(payload.data);\r\n\t};\r\n\r\n\twindow.addEventListener(\"message\", listener);\r\n\r\n\treturn () => {\r\n\t\twindow.removeEventListener(\"message\", listener);\r\n\t};\r\n}\r\n","import type { FormatArg, LocaleRecord, TranslatorFn, TranslatorOptions } from \"./types\";\r\n\r\n// ─── Lua-Style Formatter ───\r\n\r\n/**\r\n * Formats a string using Lua-style placeholders.\r\n *\r\n * Specifiers:\r\n * - `%s` — string (null/undefined becomes empty string)\r\n * - `%d` — integer (floors the value, NaN becomes 0)\r\n * - `%f` — float (NaN becomes 0)\r\n * - `%%` — literal percent sign\r\n *\r\n * @example\r\n * ```ts\r\n * luaFormat(\"Hello %s, you are level %d\", \"Laot\", 42);\r\n * // → \"Hello Laot, you are level 42\"\r\n *\r\n * luaFormat(\"Accuracy: %f%%\", 99.5);\r\n * // → \"Accuracy: 99.5%\"\r\n *\r\n * luaFormat(\"Safe: %s %d\", undefined, NaN);\r\n * // → \"Safe: 0\"\r\n * ```\r\n */\r\nexport function luaFormat(template: string, ...args: FormatArg[]): string {\r\n\tlet argIndex = 0;\r\n\r\n\treturn template.replace(/%([sdfi%])/g, (match, specifier: string) => {\r\n\t\tif (specifier === \"%\") return \"%\";\r\n\r\n\t\tconst raw = args[argIndex++];\r\n\r\n\t\tswitch (specifier) {\r\n\t\t\tcase \"s\":\r\n\t\t\t\treturn String(raw ?? \"\");\r\n\r\n\t\t\tcase \"d\":\r\n\t\t\tcase \"i\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : Math.floor(num));\r\n\t\t\t}\r\n\r\n\t\t\tcase \"f\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : num);\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn match;\r\n\t\t}\r\n\t});\r\n}\r\n\r\n// ─── Dot-Notation Resolver ───\r\n\r\n/**\r\n * Walks a nested locale object by splitting the key on `.`\r\n * Returns the string value at the end of the path, or `undefined` if any segment is missing.\r\n *\r\n * `\"client.greeting\"` → looks up `locales.client.greeting`\r\n * `\"flat_key\"` → looks up `locales.flat_key` directly\r\n */\r\nfunction resolveKey(locales: LocaleRecord, key: string): string | undefined {\r\n\tconst segments = key.split(\".\");\r\n\tlet current: LocaleRecord | string = locales;\r\n\r\n\tfor (const segment of segments) {\r\n\t\tif (typeof current !== \"object\" || current === null) return undefined;\r\n\t\tconst next: string | LocaleRecord | undefined = current[segment];\r\n\t\tif (next === undefined) return undefined;\r\n\t\tcurrent = next;\r\n\t}\r\n\r\n\treturn typeof current === \"string\" ? current : undefined;\r\n}\r\n\r\n// ─── Translator Factory ───\r\n\r\n/**\r\n * Creates a `_U(key, fallback, ...args)` translator bound to a locale record.\r\n *\r\n * The key supports dot-notation to traverse nested locale objects.\r\n * If the key isn't found, the fallback string is returned as-is.\r\n * Any extra args are passed through `luaFormat` for placeholder substitution.\r\n *\r\n * @example\r\n * ```ts\r\n * const _U = createTranslator({\r\n * locales: {\r\n * client: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * server: {\r\n * error: \"Error: %s\",\r\n * },\r\n * flat_key: \"Plain message\",\r\n * },\r\n * });\r\n *\r\n * _U(\"client.greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"client.level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _U(\"flat_key\", \"MISSING\"); // → \"Plain message\"\r\n * _U(\"no.key\", \"Not found\"); // → \"Not found\"\r\n * ```\r\n */\r\nexport function createTranslator(options: TranslatorOptions): TranslatorFn {\r\n\tconst { locales } = options;\r\n\r\n\treturn (key: string, fallback: string, ...args: FormatArg[]): string => {\r\n\t\tconst template = resolveKey(locales, key);\r\n\r\n\t\tif (template === undefined) {\r\n\t\t\treturn fallback;\r\n\t\t}\r\n\r\n\t\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n\t};\r\n}\r\n\r\n// ─── Deep Merge ───\r\n\r\n/**\r\n * Deep-merges multiple locale records into one.\r\n * Later records override earlier ones on key conflicts.\r\n * Nested objects are merged recursively, not replaced entirely.\r\n *\r\n * @example\r\n * ```ts\r\n * const base = { client: { greeting: \"Hello %s!\", level: \"Level %d\" } };\r\n * const overrides = { client: { greeting: \"Hey %s!\" } };\r\n *\r\n * const merged = mergeLocales(base, overrides);\r\n * // merged.client.greeting → \"Hey %s!\"\r\n * // merged.client.level → \"Level %d\" (preserved from base)\r\n * ```\r\n */\r\nexport function mergeLocales(...records: LocaleRecord[]): LocaleRecord {\r\n\tconst result: LocaleRecord = {};\r\n\r\n\tfor (const record of records) {\r\n\t\tfor (const [key, value] of Object.entries(record)) {\r\n\t\t\tconst existing = result[key];\r\n\r\n\t\t\tif (typeof value === \"object\" && typeof existing === \"object\" && existing !== undefined) {\r\n\t\t\t\tresult[key] = mergeLocales(existing, value);\r\n\t\t\t} else {\r\n\t\t\t\tresult[key] = value;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/listener.ts","../src/utils.ts"],"sourcesContent":["// ─── Types ───\r\nexport type {\r\n\tNuiEventMap,\r\n\tNuiMessagePayload,\r\n\tFetchNuiOptions,\r\n\tFetchNuiFactoryOptions,\r\n\tLocaleRecord,\r\n\tTranslatorOptions,\r\n\tTranslatorFn,\r\n\tFormatArg,\r\n\tUnsubscribeFn,\r\n\tNuiMessageHandler,\r\n} from \"./types\";\r\n\r\n// ─── Client ───\r\nexport { createFetchNui } from \"./client\";\r\n\r\n// ─── Listener ───\r\nexport { onNuiMessage } from \"./listener\";\r\n\r\n// ─── Utils ───\r\nexport { luaFormat, createTranslator, mergeLocales, registerLocales, extendLocales, _U } from \"./utils\";\r\n","import type { NuiEventMap, FetchNuiOptions, FetchNuiFactoryOptions } from \"./types\";\r\n\r\n// ─── Resource Name ───\r\n\r\n/**\r\n * Grabs the resource name from FiveM's injected global.\r\n * Falls back to `\"nui-frame-app\"` when running outside the game (local dev, tests).\r\n */\r\nfunction getResourceName(): string {\r\n\tif (typeof window !== \"undefined\" && window.GetParentResourceName) {\r\n\t\treturn window.GetParentResourceName();\r\n\t}\r\n\treturn \"nui-frame-app\";\r\n}\r\n\r\n// ─── FetchNui Factory ───\r\n\r\n/**\r\n * Creates a typed `fetchNui` function for your event map.\r\n * POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { request: { id: number }; response: { name: string } };\r\n * notify: { request: { msg: string }; response: void };\r\n * }\r\n *\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n *\r\n * // With mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (req) => { console.log(\"Mock:\", req.msg); },\r\n * },\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * RegisterNUICallback(\"getPlayer\", function(data, cb)\r\n * local player = GetPlayerData(data.id)\r\n * cb({ name = player.name })\r\n * end)\r\n * ```\r\n */\r\nexport function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>) {\r\n\tconst debug = factoryOptions?.debug ?? false;\r\n\tconst mockData = factoryOptions?.mockData;\r\n\r\n\treturn async function fetchNui<K extends keyof TMap & string>(\r\n\t\tevent: K,\r\n\t\t...args: TMap[K][\"request\"] extends void\r\n\t\t\t? [data?: TMap[K][\"request\"], options?: FetchNuiOptions]\r\n\t\t\t: [data: TMap[K][\"request\"], options?: FetchNuiOptions]\r\n\t): Promise<TMap[K][\"response\"]> {\r\n\t\tconst [data, options] = args;\r\n\t\tif (debug) {\r\n\t\t\tconsole.log(`[NUIX] → ${event}`, data ?? {});\r\n\t\t}\r\n\r\n\t\t// ─── Mock Mode ───\r\n\r\n\t\tif (mockData && event in mockData) {\r\n\t\t\tconst mock = mockData[event];\r\n\r\n\t\t\tif (mock === undefined) {\r\n\t\t\t\tthrow new Error(`[NUIX] Mock data for \"${event}\" is undefined`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result =\r\n\t\t\t\ttypeof mock === \"function\"\r\n\t\t\t\t\t? (mock as (req: TMap[K][\"request\"]) => TMap[K][\"response\"])(data as TMap[K][\"request\"])\r\n\t\t\t\t\t: mock;\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event} (mock)`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result as TMap[K][\"response\"];\r\n\t\t}\r\n\r\n\t\t// ─── Real Fetch ───\r\n\r\n\t\tconst url = `https://${getResourceName()}/${event}`;\r\n\r\n\t\tconst controller = options?.timeout ? new AbortController() : undefined;\r\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n\t\tif (controller && options?.timeout) {\r\n\t\t\ttimeoutId = setTimeout(() => controller.abort(), options.timeout);\r\n\t\t}\r\n\r\n\t\ttry {\r\n\t\t\tconst response = await fetch(url, {\r\n\t\t\t\tmethod: \"POST\",\r\n\t\t\t\theaders: { \"Content-Type\": \"application/json; charset=UTF-8\" },\r\n\t\t\t\tbody: JSON.stringify(data ?? {}),\r\n\t\t\t\tsignal: controller?.signal,\r\n\t\t\t});\r\n\r\n\t\t\tif (!response.ok) {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") failed with HTTP ${response.status}`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result = (await response.json()) as TMap[K][\"response\"];\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event}`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result;\r\n\t\t} catch (error) {\r\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") timed out after ${options?.timeout}ms`);\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t} finally {\r\n\t\t\tif (timeoutId !== undefined) clearTimeout(timeoutId);\r\n\t\t}\r\n\t};\r\n}\r\n","import type { NuiEventMap, NuiMessagePayload, NuiMessageHandler, UnsubscribeFn } from \"./types\";\r\n\r\n// ─── NUI Message Listener ───\r\n\r\n/**\r\n * Listens for NUI messages from Lua (`SendNUIMessage`).\r\n *\r\n * **Per-action** — filters by action name, `data` is fully typed:\r\n * ```ts\r\n * onNuiMessage<Events, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // ✅ typed as string[]\r\n * });\r\n * ```\r\n *\r\n * **Switch-case** — single listener for all actions:\r\n * ```ts\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * case \"showMenu\":\r\n * openMenu(data.items);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * SendNUIMessage({ action = \"showMenu\", data = { items = {\"Pistol\", \"Rifle\"} } })\r\n * ```\r\n */\r\n\r\n// Per-action overload\r\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"request\"]>,\r\n): UnsubscribeFn;\r\n\r\n// Switch-case overload\r\nexport function onNuiMessage<TMap extends NuiEventMap>(\r\n\thandler: (action: keyof TMap & string, data: any) => void,\r\n): UnsubscribeFn;\r\n\r\n// Implementation\r\nexport function onNuiMessage(\r\n\tactionOrHandler: string | ((action: string, data: unknown) => void),\r\n\thandler?: (data: unknown) => void,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (!payload.action) return;\r\n\r\n\t\tif (typeof actionOrHandler === \"string\") {\r\n\t\t\tif (payload.action !== actionOrHandler) return;\r\n\t\t\thandler!(payload.data);\r\n\t\t} else {\r\n\t\t\tactionOrHandler(payload.action, payload.data);\r\n\t\t}\r\n\t};\r\n\r\n\twindow.addEventListener(\"message\", listener);\r\n\r\n\treturn () => {\r\n\t\twindow.removeEventListener(\"message\", listener);\r\n\t};\r\n}\r\n","import type { FormatArg, LocaleRecord, TranslatorFn, TranslatorOptions } from \"./types\";\r\n\r\n// ─── Lua-Style Formatter ───\r\n\r\n/**\r\n * Formats a string using Lua-style placeholders.\r\n *\r\n * Specifiers:\r\n * - `%s` — string (null/undefined becomes empty string)\r\n * - `%d` / `%i` — integer (floors the value, NaN becomes 0)\r\n * - `%f` — float (NaN becomes 0)\r\n * - `%%` — literal percent sign\r\n *\r\n * @example\r\n * ```ts\r\n * luaFormat(\"Hello %s, you are level %d\", \"Laot\", 42);\r\n * // → \"Hello Laot, you are level 42\"\r\n *\r\n * luaFormat(\"Accuracy: %f%%\", 99.5);\r\n * // → \"Accuracy: 99.5%\"\r\n *\r\n * luaFormat(\"Safe: %s %d\", undefined, NaN);\r\n * // → \"Safe: 0\"\r\n * ```\r\n */\r\nexport function luaFormat(template: string, ...args: FormatArg[]): string {\r\n\tlet argIndex = 0;\r\n\r\n\treturn template.replace(/%([sdfi%])/g, (match, specifier: string) => {\r\n\t\tif (specifier === \"%\") return \"%\";\r\n\r\n\t\tconst raw = args[argIndex++];\r\n\r\n\t\tswitch (specifier) {\r\n\t\t\tcase \"s\":\r\n\t\t\t\treturn String(raw ?? \"\");\r\n\r\n\t\t\tcase \"d\":\r\n\t\t\tcase \"i\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : Math.floor(num));\r\n\t\t\t}\r\n\r\n\t\t\tcase \"f\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : num);\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn match;\r\n\t\t}\r\n\t});\r\n}\r\n\r\n// ─── Dot-Notation Resolver ───\r\n\r\n/**\r\n * Walks a nested locale object by splitting the key on `.`\r\n * Returns the string value at the end of the path, or `undefined` if any segment is missing.\r\n *\r\n * `\"client.greeting\"` → looks up `locales.client.greeting`\r\n * `\"flat_key\"` → looks up `locales.flat_key` directly\r\n */\r\nfunction resolveKey(locales: LocaleRecord, key: string): string | undefined {\r\n\tconst segments = key.split(\".\");\r\n\tlet current: LocaleRecord | string = locales;\r\n\r\n\tfor (const segment of segments) {\r\n\t\tif (typeof current !== \"object\" || current === null) return undefined;\r\n\t\tconst next: string | LocaleRecord | undefined = current[segment];\r\n\t\tif (next === undefined) return undefined;\r\n\t\tcurrent = next;\r\n\t}\r\n\r\n\treturn typeof current === \"string\" ? current : undefined;\r\n}\r\n\r\n// ─── Translator Factory ───\r\n\r\n/**\r\n * Creates an isolated translator bound to a specific locale record.\r\n * Use this when you need a separate translator instance, independent of the global `_U`.\r\n *\r\n * @example\r\n * ```ts\r\n * const _T = createTranslator({\r\n * locales: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * });\r\n *\r\n * _T(\"greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _T(\"level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _T(\"no.key\", \"Not found\"); // → \"Not found\"\r\n * ```\r\n */\r\nexport function createTranslator(options: TranslatorOptions): TranslatorFn {\r\n\tconst { locales } = options;\r\n\r\n\treturn (key: string, fallback: string, ...args: FormatArg[]): string => {\r\n\t\tconst template = resolveKey(locales, key);\r\n\r\n\t\tif (template === undefined) {\r\n\t\t\treturn fallback;\r\n\t\t}\r\n\r\n\t\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n\t};\r\n}\r\n\r\n// ─── Global Locale Registry ───\r\n\r\nlet _locales: LocaleRecord = {};\r\n\r\n/**\r\n * Sets the global locale map. Call this when Lua sends locale data to the NUI.\r\n *\r\n * @example\r\n * ```ts\r\n * // Lua side:\r\n * // SendNUIMessage({ action = \"setLocales\", data = locales })\r\n *\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport function registerLocales(locales: LocaleRecord): void {\r\n\t_locales = locales;\r\n}\r\n\r\n/**\r\n * Merges new entries into the current global locale map without replacing it.\r\n *\r\n * @example\r\n * ```ts\r\n * registerLocales({ ui: { title: \"Dashboard\" } });\r\n * extendLocales({ ui: { subtitle: \"Overview\" } });\r\n *\r\n * _U(\"ui.title\", \"\"); // → \"Dashboard\"\r\n * _U(\"ui.subtitle\", \"\"); // → \"Overview\"\r\n * ```\r\n */\r\nexport function extendLocales(...records: LocaleRecord[]): void {\r\n\t_locales = mergeLocales(_locales, ...records);\r\n}\r\n\r\n/**\r\n * Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.\r\n *\r\n * @param key Dot-notated key, e.g. `\"ui.greeting\"` or flat `\"title\"`\r\n * @param fallback Returned as-is when the key doesn't exist\r\n * @param args Values for `%s`, `%d`, `%f` placeholders\r\n *\r\n * @example\r\n * ```ts\r\n * import { registerLocales, _U } from \"@laot/nuix\";\r\n *\r\n * // After Lua sends locales:\r\n * // { ui: { greeting: \"Hello %s!\", level: \"Level %d\" } }\r\n *\r\n * _U(\"ui.greeting\", \"Hi\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"ui.level\", \"Lv.\", 42); // → \"Level 42\"\r\n * _U(\"missing.key\", \"Fallback\"); // → \"Fallback\"\r\n * ```\r\n */\r\nexport function _U(key: string, fallback: string, ...args: FormatArg[]): string {\r\n\tconst template = resolveKey(_locales, key);\r\n\tif (template === undefined) return fallback;\r\n\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n}\r\n\r\n// ─── Deep Merge ───\r\n\r\n/**\r\n * Deep-merges multiple locale records into one.\r\n * Later records override earlier ones on key conflicts.\r\n * Nested objects are merged recursively, not replaced entirely.\r\n *\r\n * @example\r\n * ```ts\r\n * const base = { client: { greeting: \"Hello %s!\", level: \"Level %d\" } };\r\n * const overrides = { client: { greeting: \"Hey %s!\" } };\r\n *\r\n * const merged = mergeLocales(base, overrides);\r\n * // merged.client.greeting → \"Hey %s!\"\r\n * // merged.client.level → \"Level %d\" (preserved from base)\r\n * ```\r\n */\r\nexport function mergeLocales(...records: LocaleRecord[]): LocaleRecord {\r\n\tconst result: LocaleRecord = {};\r\n\r\n\tfor (const record of records) {\r\n\t\tfor (const [key, value] of Object.entries(record)) {\r\n\t\t\tconst existing = result[key];\r\n\r\n\t\t\tif (\r\n\t\t\t\tvalue !== null &&\r\n\t\t\t\texisting !== null &&\r\n\t\t\t\ttypeof value === \"object\" &&\r\n\t\t\t\ttypeof existing === \"object\" &&\r\n\t\t\t\texisting !== undefined\r\n\t\t\t) {\r\n\t\t\t\tresult[key] = mergeLocales(existing, value);\r\n\t\t\t} else {\r\n\t\t\t\tresult[key] = value;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -39,7 +39,7 @@ interface FetchNuiOptions {
39
39
  timeout?: number;
40
40
  }
41
41
  /**
42
- * Options passed to `createFetchNui()` at factory level.
42
+ * Config for `createFetchNui()`.
43
43
  *
44
44
  * @example
45
45
  * ```ts
@@ -53,25 +53,20 @@ interface FetchNuiOptions {
53
53
  * ```
54
54
  */
55
55
  interface FetchNuiFactoryOptions<TMap extends NuiEventMap> {
56
- /** Logs every `fetchNui` call and response to the console with `[NUIX]` prefix. */
56
+ /** Logs every call and response to the console with `[NUIX]` prefix. */
57
57
  debug?: boolean;
58
- /**
59
- * Mock responses for local development outside FiveM.
60
- * Can be a static value or a function that receives the request and returns the response.
61
- * When a mock exists for an event, no HTTP call is made.
62
- */
58
+ /** Static or dynamic mock responses — when set, no real HTTP call is made. */
63
59
  mockData?: {
64
60
  [K in keyof TMap]?: TMap[K]["response"] | ((request: TMap[K]["request"]) => TMap[K]["response"]);
65
61
  };
66
62
  }
67
63
  /**
68
- * Locale record can be flat strings or nested objects.
69
- * Nested keys are accessed via dot-notation in the translator.
64
+ * Flat strings or nested objects. Nested keys use dot-notation (`"ui.greeting"`).
70
65
  *
71
66
  * @example
72
67
  * ```ts
73
68
  * const locales: LocaleRecord = {
74
- * client: {
69
+ * ui: {
75
70
  * greeting: "Hello %s!",
76
71
  * level: "Level %d",
77
72
  * },
@@ -83,20 +78,20 @@ type LocaleRecord = {
83
78
  [key: string]: string | LocaleRecord;
84
79
  };
85
80
  interface TranslatorOptions {
86
- /** The locale map. Same structure as `LocaleRecord` — flat or nested. */
81
+ /** The locale map — flat or nested. */
87
82
  locales: LocaleRecord;
88
83
  }
89
84
  /**
90
- * Translator function returned by `createTranslator`.
85
+ * Translator function signature used by both `createTranslator` and the global `_U`.
91
86
  *
92
- * @param key - Dot-notated key like `'client.greeting'` or flat like `'title'`
93
- * @param fallback - Returned when the key doesn't exist in the locale map
94
- * @param args - Format arguments for `%s`, `%d`, `%f` placeholders
87
+ * @param key Dot-notated key like `"ui.greeting"` or flat like `"title"`
88
+ * @param fallback Returned when the key doesn't exist in the locale map
89
+ * @param args Format arguments for `%s`, `%d`, `%f` placeholders
95
90
  *
96
91
  * @example
97
92
  * ```ts
98
- * _U("client.greeting", "MISSING", "Laot"); // → "Hello Laot!"
99
- * _U("no.key", "Not found"); // → "Not found"
93
+ * _U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
94
+ * _U("missing.key", "Fallback"); // → "Fallback"
100
95
  * ```
101
96
  */
102
97
  type TranslatorFn = (key: string, fallback: string, ...args: FormatArg[]) => string;
@@ -111,12 +106,8 @@ type UnsubscribeFn = () => void;
111
106
  type NuiMessageHandler<TData = unknown> = (data: TData) => void;
112
107
 
113
108
  /**
114
- * Creates a fully typed `fetchNui` function tied to your event map.
115
- *
116
- * The returned function POSTs JSON to `https://<resourceName>/<event>`,
117
- * which maps 1:1 to `RegisterNUICallback` on the Lua side.
118
- *
119
- * Supports optional debug logging, mock data for local dev, and per-call timeout.
109
+ * Creates a typed `fetchNui` function for your event map.
110
+ * POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.
120
111
  *
121
112
  * @example
122
113
  * ```ts
@@ -125,12 +116,10 @@ type NuiMessageHandler<TData = unknown> = (data: TData) => void;
125
116
  * notify: { request: { msg: string }; response: void };
126
117
  * }
127
118
  *
128
- * // Production usage
129
119
  * const fetchNui = createFetchNui<MyEvents>();
130
120
  * const player = await fetchNui("getPlayer", { id: 1 });
131
- * // player.name is typed as string
132
121
  *
133
- * // Local dev with mocks + debug
122
+ * // With mocks + debug
134
123
  * const fetchNui = createFetchNui<MyEvents>({
135
124
  * debug: true,
136
125
  * mockData: {
@@ -148,26 +137,30 @@ type NuiMessageHandler<TData = unknown> = (data: TData) => void;
148
137
  * end)
149
138
  * ```
150
139
  */
151
- declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>): <K extends keyof TMap & string>(event: K, data?: TMap[K]["request"], options?: FetchNuiOptions) => Promise<TMap[K]["response"]>;
140
+ declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>): <K extends keyof TMap & string>(event: K, ...args: TMap[K]["request"] extends void ? [data?: TMap[K]["request"], options?: FetchNuiOptions] : [data: TMap[K]["request"], options?: FetchNuiOptions]) => Promise<TMap[K]["response"]>;
152
141
 
153
142
  /**
154
- * Listens for NUI messages from Lua (`SendNUIMessage`), filtered by action name.
155
- * Only messages matching the given `action` trigger the handler.
156
- * Returns a cleanup function to stop listening.
143
+ * Listens for NUI messages from Lua (`SendNUIMessage`).
157
144
  *
158
- * @example
145
+ * **Per-action** — filters by action name, `data` is fully typed:
159
146
  * ```ts
160
- * interface MyMessages extends NuiEventMap {
161
- * showMenu: { request: { items: string[] }; response: void };
162
- * hideMenu: { request: void; response: void };
163
- * }
164
- *
165
- * const unsub = onNuiMessage<MyMessages, "showMenu">("showMenu", (data) => {
166
- * console.log(data.items); // typed as string[]
147
+ * onNuiMessage<Events, "showMenu">("showMenu", (data) => {
148
+ * console.log(data.items); // typed as string[]
167
149
  * });
150
+ * ```
168
151
  *
169
- * // Stop listening when done
170
- * unsub();
152
+ * **Switch-case** single listener for all actions:
153
+ * ```ts
154
+ * onNuiMessage<Events>((action, data) => {
155
+ * switch (action) {
156
+ * case "setLocales":
157
+ * registerLocales(data);
158
+ * break;
159
+ * case "showMenu":
160
+ * openMenu(data.items);
161
+ * break;
162
+ * }
163
+ * });
171
164
  * ```
172
165
  *
173
166
  * Lua side:
@@ -176,13 +169,14 @@ declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: Fetch
176
169
  * ```
177
170
  */
178
171
  declare function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(action: K, handler: NuiMessageHandler<TMap[K]["request"]>): UnsubscribeFn;
172
+ declare function onNuiMessage<TMap extends NuiEventMap>(handler: (action: keyof TMap & string, data: any) => void): UnsubscribeFn;
179
173
 
180
174
  /**
181
175
  * Formats a string using Lua-style placeholders.
182
176
  *
183
177
  * Specifiers:
184
178
  * - `%s` — string (null/undefined becomes empty string)
185
- * - `%d` — integer (floors the value, NaN becomes 0)
179
+ * - `%d` / `%i` — integer (floors the value, NaN becomes 0)
186
180
  * - `%f` — float (NaN becomes 0)
187
181
  * - `%%` — literal percent sign
188
182
  *
@@ -200,34 +194,75 @@ declare function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & s
200
194
  */
201
195
  declare function luaFormat(template: string, ...args: FormatArg[]): string;
202
196
  /**
203
- * Creates a `_U(key, fallback, ...args)` translator bound to a locale record.
204
- *
205
- * The key supports dot-notation to traverse nested locale objects.
206
- * If the key isn't found, the fallback string is returned as-is.
207
- * Any extra args are passed through `luaFormat` for placeholder substitution.
197
+ * Creates an isolated translator bound to a specific locale record.
198
+ * Use this when you need a separate translator instance, independent of the global `_U`.
208
199
  *
209
200
  * @example
210
201
  * ```ts
211
- * const _U = createTranslator({
202
+ * const _T = createTranslator({
212
203
  * locales: {
213
- * client: {
214
- * greeting: "Hello %s!",
215
- * level: "Level %d",
216
- * },
217
- * server: {
218
- * error: "Error: %s",
219
- * },
220
- * flat_key: "Plain message",
204
+ * greeting: "Hello %s!",
205
+ * level: "Level %d",
221
206
  * },
222
207
  * });
223
208
  *
224
- * _U("client.greeting", "MISSING", "Laot"); // → "Hello Laot!"
225
- * _U("client.level", "MISSING", 42); // → "Level 42"
226
- * _U("flat_key", "MISSING"); // → "Plain message"
227
- * _U("no.key", "Not found"); // → "Not found"
209
+ * _T("greeting", "MISSING", "Laot"); // → "Hello Laot!"
210
+ * _T("level", "MISSING", 42); // → "Level 42"
211
+ * _T("no.key", "Not found"); // → "Not found"
228
212
  * ```
229
213
  */
230
214
  declare function createTranslator(options: TranslatorOptions): TranslatorFn;
215
+ /**
216
+ * Sets the global locale map. Call this when Lua sends locale data to the NUI.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * // Lua side:
221
+ * // SendNUIMessage({ action = "setLocales", data = locales })
222
+ *
223
+ * onNuiMessage<Events>((action, data) => {
224
+ * switch (action) {
225
+ * case "setLocales":
226
+ * registerLocales(data);
227
+ * break;
228
+ * }
229
+ * });
230
+ * ```
231
+ */
232
+ declare function registerLocales(locales: LocaleRecord): void;
233
+ /**
234
+ * Merges new entries into the current global locale map without replacing it.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * registerLocales({ ui: { title: "Dashboard" } });
239
+ * extendLocales({ ui: { subtitle: "Overview" } });
240
+ *
241
+ * _U("ui.title", ""); // → "Dashboard"
242
+ * _U("ui.subtitle", ""); // → "Overview"
243
+ * ```
244
+ */
245
+ declare function extendLocales(...records: LocaleRecord[]): void;
246
+ /**
247
+ * Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.
248
+ *
249
+ * @param key Dot-notated key, e.g. `"ui.greeting"` or flat `"title"`
250
+ * @param fallback Returned as-is when the key doesn't exist
251
+ * @param args Values for `%s`, `%d`, `%f` placeholders
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * import { registerLocales, _U } from "@laot/nuix";
256
+ *
257
+ * // After Lua sends locales:
258
+ * // { ui: { greeting: "Hello %s!", level: "Level %d" } }
259
+ *
260
+ * _U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
261
+ * _U("ui.level", "Lv.", 42); // → "Level 42"
262
+ * _U("missing.key", "Fallback"); // → "Fallback"
263
+ * ```
264
+ */
265
+ declare function _U(key: string, fallback: string, ...args: FormatArg[]): string;
231
266
  /**
232
267
  * Deep-merges multiple locale records into one.
233
268
  * Later records override earlier ones on key conflicts.
@@ -245,4 +280,4 @@ declare function createTranslator(options: TranslatorOptions): TranslatorFn;
245
280
  */
246
281
  declare function mergeLocales(...records: LocaleRecord[]): LocaleRecord;
247
282
 
248
- export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, createFetchNui, createTranslator, luaFormat, mergeLocales, onNuiMessage };
283
+ export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, _U, createFetchNui, createTranslator, extendLocales, luaFormat, mergeLocales, onNuiMessage, registerLocales };
package/dist/index.d.ts CHANGED
@@ -39,7 +39,7 @@ interface FetchNuiOptions {
39
39
  timeout?: number;
40
40
  }
41
41
  /**
42
- * Options passed to `createFetchNui()` at factory level.
42
+ * Config for `createFetchNui()`.
43
43
  *
44
44
  * @example
45
45
  * ```ts
@@ -53,25 +53,20 @@ interface FetchNuiOptions {
53
53
  * ```
54
54
  */
55
55
  interface FetchNuiFactoryOptions<TMap extends NuiEventMap> {
56
- /** Logs every `fetchNui` call and response to the console with `[NUIX]` prefix. */
56
+ /** Logs every call and response to the console with `[NUIX]` prefix. */
57
57
  debug?: boolean;
58
- /**
59
- * Mock responses for local development outside FiveM.
60
- * Can be a static value or a function that receives the request and returns the response.
61
- * When a mock exists for an event, no HTTP call is made.
62
- */
58
+ /** Static or dynamic mock responses — when set, no real HTTP call is made. */
63
59
  mockData?: {
64
60
  [K in keyof TMap]?: TMap[K]["response"] | ((request: TMap[K]["request"]) => TMap[K]["response"]);
65
61
  };
66
62
  }
67
63
  /**
68
- * Locale record can be flat strings or nested objects.
69
- * Nested keys are accessed via dot-notation in the translator.
64
+ * Flat strings or nested objects. Nested keys use dot-notation (`"ui.greeting"`).
70
65
  *
71
66
  * @example
72
67
  * ```ts
73
68
  * const locales: LocaleRecord = {
74
- * client: {
69
+ * ui: {
75
70
  * greeting: "Hello %s!",
76
71
  * level: "Level %d",
77
72
  * },
@@ -83,20 +78,20 @@ type LocaleRecord = {
83
78
  [key: string]: string | LocaleRecord;
84
79
  };
85
80
  interface TranslatorOptions {
86
- /** The locale map. Same structure as `LocaleRecord` — flat or nested. */
81
+ /** The locale map — flat or nested. */
87
82
  locales: LocaleRecord;
88
83
  }
89
84
  /**
90
- * Translator function returned by `createTranslator`.
85
+ * Translator function signature used by both `createTranslator` and the global `_U`.
91
86
  *
92
- * @param key - Dot-notated key like `'client.greeting'` or flat like `'title'`
93
- * @param fallback - Returned when the key doesn't exist in the locale map
94
- * @param args - Format arguments for `%s`, `%d`, `%f` placeholders
87
+ * @param key Dot-notated key like `"ui.greeting"` or flat like `"title"`
88
+ * @param fallback Returned when the key doesn't exist in the locale map
89
+ * @param args Format arguments for `%s`, `%d`, `%f` placeholders
95
90
  *
96
91
  * @example
97
92
  * ```ts
98
- * _U("client.greeting", "MISSING", "Laot"); // → "Hello Laot!"
99
- * _U("no.key", "Not found"); // → "Not found"
93
+ * _U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
94
+ * _U("missing.key", "Fallback"); // → "Fallback"
100
95
  * ```
101
96
  */
102
97
  type TranslatorFn = (key: string, fallback: string, ...args: FormatArg[]) => string;
@@ -111,12 +106,8 @@ type UnsubscribeFn = () => void;
111
106
  type NuiMessageHandler<TData = unknown> = (data: TData) => void;
112
107
 
113
108
  /**
114
- * Creates a fully typed `fetchNui` function tied to your event map.
115
- *
116
- * The returned function POSTs JSON to `https://<resourceName>/<event>`,
117
- * which maps 1:1 to `RegisterNUICallback` on the Lua side.
118
- *
119
- * Supports optional debug logging, mock data for local dev, and per-call timeout.
109
+ * Creates a typed `fetchNui` function for your event map.
110
+ * POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.
120
111
  *
121
112
  * @example
122
113
  * ```ts
@@ -125,12 +116,10 @@ type NuiMessageHandler<TData = unknown> = (data: TData) => void;
125
116
  * notify: { request: { msg: string }; response: void };
126
117
  * }
127
118
  *
128
- * // Production usage
129
119
  * const fetchNui = createFetchNui<MyEvents>();
130
120
  * const player = await fetchNui("getPlayer", { id: 1 });
131
- * // player.name is typed as string
132
121
  *
133
- * // Local dev with mocks + debug
122
+ * // With mocks + debug
134
123
  * const fetchNui = createFetchNui<MyEvents>({
135
124
  * debug: true,
136
125
  * mockData: {
@@ -148,26 +137,30 @@ type NuiMessageHandler<TData = unknown> = (data: TData) => void;
148
137
  * end)
149
138
  * ```
150
139
  */
151
- declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>): <K extends keyof TMap & string>(event: K, data?: TMap[K]["request"], options?: FetchNuiOptions) => Promise<TMap[K]["response"]>;
140
+ declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>): <K extends keyof TMap & string>(event: K, ...args: TMap[K]["request"] extends void ? [data?: TMap[K]["request"], options?: FetchNuiOptions] : [data: TMap[K]["request"], options?: FetchNuiOptions]) => Promise<TMap[K]["response"]>;
152
141
 
153
142
  /**
154
- * Listens for NUI messages from Lua (`SendNUIMessage`), filtered by action name.
155
- * Only messages matching the given `action` trigger the handler.
156
- * Returns a cleanup function to stop listening.
143
+ * Listens for NUI messages from Lua (`SendNUIMessage`).
157
144
  *
158
- * @example
145
+ * **Per-action** — filters by action name, `data` is fully typed:
159
146
  * ```ts
160
- * interface MyMessages extends NuiEventMap {
161
- * showMenu: { request: { items: string[] }; response: void };
162
- * hideMenu: { request: void; response: void };
163
- * }
164
- *
165
- * const unsub = onNuiMessage<MyMessages, "showMenu">("showMenu", (data) => {
166
- * console.log(data.items); // typed as string[]
147
+ * onNuiMessage<Events, "showMenu">("showMenu", (data) => {
148
+ * console.log(data.items); // typed as string[]
167
149
  * });
150
+ * ```
168
151
  *
169
- * // Stop listening when done
170
- * unsub();
152
+ * **Switch-case** single listener for all actions:
153
+ * ```ts
154
+ * onNuiMessage<Events>((action, data) => {
155
+ * switch (action) {
156
+ * case "setLocales":
157
+ * registerLocales(data);
158
+ * break;
159
+ * case "showMenu":
160
+ * openMenu(data.items);
161
+ * break;
162
+ * }
163
+ * });
171
164
  * ```
172
165
  *
173
166
  * Lua side:
@@ -176,13 +169,14 @@ declare function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: Fetch
176
169
  * ```
177
170
  */
178
171
  declare function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(action: K, handler: NuiMessageHandler<TMap[K]["request"]>): UnsubscribeFn;
172
+ declare function onNuiMessage<TMap extends NuiEventMap>(handler: (action: keyof TMap & string, data: any) => void): UnsubscribeFn;
179
173
 
180
174
  /**
181
175
  * Formats a string using Lua-style placeholders.
182
176
  *
183
177
  * Specifiers:
184
178
  * - `%s` — string (null/undefined becomes empty string)
185
- * - `%d` — integer (floors the value, NaN becomes 0)
179
+ * - `%d` / `%i` — integer (floors the value, NaN becomes 0)
186
180
  * - `%f` — float (NaN becomes 0)
187
181
  * - `%%` — literal percent sign
188
182
  *
@@ -200,34 +194,75 @@ declare function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & s
200
194
  */
201
195
  declare function luaFormat(template: string, ...args: FormatArg[]): string;
202
196
  /**
203
- * Creates a `_U(key, fallback, ...args)` translator bound to a locale record.
204
- *
205
- * The key supports dot-notation to traverse nested locale objects.
206
- * If the key isn't found, the fallback string is returned as-is.
207
- * Any extra args are passed through `luaFormat` for placeholder substitution.
197
+ * Creates an isolated translator bound to a specific locale record.
198
+ * Use this when you need a separate translator instance, independent of the global `_U`.
208
199
  *
209
200
  * @example
210
201
  * ```ts
211
- * const _U = createTranslator({
202
+ * const _T = createTranslator({
212
203
  * locales: {
213
- * client: {
214
- * greeting: "Hello %s!",
215
- * level: "Level %d",
216
- * },
217
- * server: {
218
- * error: "Error: %s",
219
- * },
220
- * flat_key: "Plain message",
204
+ * greeting: "Hello %s!",
205
+ * level: "Level %d",
221
206
  * },
222
207
  * });
223
208
  *
224
- * _U("client.greeting", "MISSING", "Laot"); // → "Hello Laot!"
225
- * _U("client.level", "MISSING", 42); // → "Level 42"
226
- * _U("flat_key", "MISSING"); // → "Plain message"
227
- * _U("no.key", "Not found"); // → "Not found"
209
+ * _T("greeting", "MISSING", "Laot"); // → "Hello Laot!"
210
+ * _T("level", "MISSING", 42); // → "Level 42"
211
+ * _T("no.key", "Not found"); // → "Not found"
228
212
  * ```
229
213
  */
230
214
  declare function createTranslator(options: TranslatorOptions): TranslatorFn;
215
+ /**
216
+ * Sets the global locale map. Call this when Lua sends locale data to the NUI.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * // Lua side:
221
+ * // SendNUIMessage({ action = "setLocales", data = locales })
222
+ *
223
+ * onNuiMessage<Events>((action, data) => {
224
+ * switch (action) {
225
+ * case "setLocales":
226
+ * registerLocales(data);
227
+ * break;
228
+ * }
229
+ * });
230
+ * ```
231
+ */
232
+ declare function registerLocales(locales: LocaleRecord): void;
233
+ /**
234
+ * Merges new entries into the current global locale map without replacing it.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * registerLocales({ ui: { title: "Dashboard" } });
239
+ * extendLocales({ ui: { subtitle: "Overview" } });
240
+ *
241
+ * _U("ui.title", ""); // → "Dashboard"
242
+ * _U("ui.subtitle", ""); // → "Overview"
243
+ * ```
244
+ */
245
+ declare function extendLocales(...records: LocaleRecord[]): void;
246
+ /**
247
+ * Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.
248
+ *
249
+ * @param key Dot-notated key, e.g. `"ui.greeting"` or flat `"title"`
250
+ * @param fallback Returned as-is when the key doesn't exist
251
+ * @param args Values for `%s`, `%d`, `%f` placeholders
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * import { registerLocales, _U } from "@laot/nuix";
256
+ *
257
+ * // After Lua sends locales:
258
+ * // { ui: { greeting: "Hello %s!", level: "Level %d" } }
259
+ *
260
+ * _U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
261
+ * _U("ui.level", "Lv.", 42); // → "Level 42"
262
+ * _U("missing.key", "Fallback"); // → "Fallback"
263
+ * ```
264
+ */
265
+ declare function _U(key: string, fallback: string, ...args: FormatArg[]): string;
231
266
  /**
232
267
  * Deep-merges multiple locale records into one.
233
268
  * Later records override earlier ones on key conflicts.
@@ -245,4 +280,4 @@ declare function createTranslator(options: TranslatorOptions): TranslatorFn;
245
280
  */
246
281
  declare function mergeLocales(...records: LocaleRecord[]): LocaleRecord;
247
282
 
248
- export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, createFetchNui, createTranslator, luaFormat, mergeLocales, onNuiMessage };
283
+ export { type FetchNuiFactoryOptions, type FetchNuiOptions, type FormatArg, type LocaleRecord, type NuiEventMap, type NuiMessageHandler, type NuiMessagePayload, type TranslatorFn, type TranslatorOptions, type UnsubscribeFn, _U, createFetchNui, createTranslator, extendLocales, luaFormat, mergeLocales, onNuiMessage, registerLocales };
package/dist/index.js CHANGED
@@ -8,7 +8,8 @@ function getResourceName() {
8
8
  function createFetchNui(factoryOptions) {
9
9
  const debug = factoryOptions?.debug ?? false;
10
10
  const mockData = factoryOptions?.mockData;
11
- return async function fetchNui(event, data, options) {
11
+ return async function fetchNui(event, ...args) {
12
+ const [data, options] = args;
12
13
  if (debug) {
13
14
  console.log(`[NUIX] \u2192 ${event}`, data ?? {});
14
15
  }
@@ -56,12 +57,17 @@ function createFetchNui(factoryOptions) {
56
57
  }
57
58
 
58
59
  // src/listener.ts
59
- function onNuiMessage(action, handler) {
60
+ function onNuiMessage(actionOrHandler, handler) {
60
61
  const listener = (event) => {
61
62
  const payload = event.data;
62
63
  if (!payload || typeof payload !== "object") return;
63
- if (payload.action !== action) return;
64
- handler(payload.data);
64
+ if (!payload.action) return;
65
+ if (typeof actionOrHandler === "string") {
66
+ if (payload.action !== actionOrHandler) return;
67
+ handler(payload.data);
68
+ } else {
69
+ actionOrHandler(payload.action, payload.data);
70
+ }
65
71
  };
66
72
  window.addEventListener("message", listener);
67
73
  return () => {
@@ -113,12 +119,24 @@ function createTranslator(options) {
113
119
  return args.length > 0 ? luaFormat(template, ...args) : template;
114
120
  };
115
121
  }
122
+ var _locales = {};
123
+ function registerLocales(locales) {
124
+ _locales = locales;
125
+ }
126
+ function extendLocales(...records) {
127
+ _locales = mergeLocales(_locales, ...records);
128
+ }
129
+ function _U(key, fallback, ...args) {
130
+ const template = resolveKey(_locales, key);
131
+ if (template === void 0) return fallback;
132
+ return args.length > 0 ? luaFormat(template, ...args) : template;
133
+ }
116
134
  function mergeLocales(...records) {
117
135
  const result = {};
118
136
  for (const record of records) {
119
137
  for (const [key, value] of Object.entries(record)) {
120
138
  const existing = result[key];
121
- if (typeof value === "object" && typeof existing === "object" && existing !== void 0) {
139
+ if (value !== null && existing !== null && typeof value === "object" && typeof existing === "object" && existing !== void 0) {
122
140
  result[key] = mergeLocales(existing, value);
123
141
  } else {
124
142
  result[key] = value;
@@ -128,10 +146,13 @@ function mergeLocales(...records) {
128
146
  return result;
129
147
  }
130
148
  export {
149
+ _U,
131
150
  createFetchNui,
132
151
  createTranslator,
152
+ extendLocales,
133
153
  luaFormat,
134
154
  mergeLocales,
135
- onNuiMessage
155
+ onNuiMessage,
156
+ registerLocales
136
157
  };
137
158
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/listener.ts","../src/utils.ts"],"sourcesContent":["import type { NuiEventMap, FetchNuiOptions, FetchNuiFactoryOptions } from \"./types\";\r\n\r\n// ─── Resource Name ───\r\n\r\n/**\r\n * Grabs the resource name from FiveM's injected global.\r\n * Falls back to `\"nui-frame-app\"` when running outside the game (local dev, tests).\r\n */\r\nfunction getResourceName(): string {\r\n\tif (typeof window !== \"undefined\" && window.GetParentResourceName) {\r\n\t\treturn window.GetParentResourceName();\r\n\t}\r\n\treturn \"nui-frame-app\";\r\n}\r\n\r\n// ─── FetchNui Factory ───\r\n\r\n/**\r\n * Creates a fully typed `fetchNui` function tied to your event map.\r\n *\r\n * The returned function POSTs JSON to `https://<resourceName>/<event>`,\r\n * which maps 1:1 to `RegisterNUICallback` on the Lua side.\r\n *\r\n * Supports optional debug logging, mock data for local dev, and per-call timeout.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { request: { id: number }; response: { name: string } };\r\n * notify: { request: { msg: string }; response: void };\r\n * }\r\n *\r\n * // Production usage\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n * // player.name is typed as string\r\n *\r\n * // Local dev with mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (req) => { console.log(\"Mock:\", req.msg); },\r\n * },\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * RegisterNUICallback(\"getPlayer\", function(data, cb)\r\n * local player = GetPlayerData(data.id)\r\n * cb({ name = player.name })\r\n * end)\r\n * ```\r\n */\r\nexport function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>) {\r\n\tconst debug = factoryOptions?.debug ?? false;\r\n\tconst mockData = factoryOptions?.mockData;\r\n\r\n\treturn async function fetchNui<K extends keyof TMap & string>(\r\n\t\tevent: K,\r\n\t\tdata?: TMap[K][\"request\"],\r\n\t\toptions?: FetchNuiOptions,\r\n\t): Promise<TMap[K][\"response\"]> {\r\n\t\tif (debug) {\r\n\t\t\tconsole.log(`[NUIX] → ${event}`, data ?? {});\r\n\t\t}\r\n\r\n\t\t// ─── Mock Mode ───\r\n\r\n\t\tif (mockData && event in mockData) {\r\n\t\t\tconst mock = mockData[event];\r\n\r\n\t\t\tif (mock === undefined) {\r\n\t\t\t\tthrow new Error(`[NUIX] Mock data for \"${event}\" is undefined`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result =\r\n\t\t\t\ttypeof mock === \"function\"\r\n\t\t\t\t\t? (mock as (req: TMap[K][\"request\"]) => TMap[K][\"response\"])(data as TMap[K][\"request\"])\r\n\t\t\t\t\t: mock;\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event} (mock)`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result as TMap[K][\"response\"];\r\n\t\t}\r\n\r\n\t\t// ─── Real Fetch ───\r\n\r\n\t\tconst url = `https://${getResourceName()}/${event}`;\r\n\r\n\t\tconst controller = options?.timeout ? new AbortController() : undefined;\r\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n\t\tif (controller && options?.timeout) {\r\n\t\t\ttimeoutId = setTimeout(() => controller.abort(), options.timeout);\r\n\t\t}\r\n\r\n\t\ttry {\r\n\t\t\tconst response = await fetch(url, {\r\n\t\t\t\tmethod: \"POST\",\r\n\t\t\t\theaders: { \"Content-Type\": \"application/json; charset=UTF-8\" },\r\n\t\t\t\tbody: JSON.stringify(data ?? {}),\r\n\t\t\t\tsignal: controller?.signal,\r\n\t\t\t});\r\n\r\n\t\t\tif (!response.ok) {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") failed with HTTP ${response.status}`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result = (await response.json()) as TMap[K][\"response\"];\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event}`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result;\r\n\t\t} catch (error) {\r\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") timed out after ${options?.timeout}ms`);\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t} finally {\r\n\t\t\tif (timeoutId !== undefined) clearTimeout(timeoutId);\r\n\t\t}\r\n\t};\r\n}\r\n","import type { NuiEventMap, NuiMessagePayload, NuiMessageHandler, UnsubscribeFn } from \"./types\";\r\n\r\n// ─── NUI Message Listener ───\r\n\r\n/**\r\n * Listens for NUI messages from Lua (`SendNUIMessage`), filtered by action name.\r\n * Only messages matching the given `action` trigger the handler.\r\n * Returns a cleanup function to stop listening.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyMessages extends NuiEventMap {\r\n * showMenu: { request: { items: string[] }; response: void };\r\n * hideMenu: { request: void; response: void };\r\n * }\r\n *\r\n * const unsub = onNuiMessage<MyMessages, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // typed as string[]\r\n * });\r\n *\r\n * // Stop listening when done\r\n * unsub();\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * SendNUIMessage({ action = \"showMenu\", data = { items = {\"Pistol\", \"Rifle\"} } })\r\n * ```\r\n */\r\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"request\"]>,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload<TMap[K][\"request\"]>>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (payload.action !== action) return;\r\n\r\n\t\thandler(payload.data);\r\n\t};\r\n\r\n\twindow.addEventListener(\"message\", listener);\r\n\r\n\treturn () => {\r\n\t\twindow.removeEventListener(\"message\", listener);\r\n\t};\r\n}\r\n","import type { FormatArg, LocaleRecord, TranslatorFn, TranslatorOptions } from \"./types\";\r\n\r\n// ─── Lua-Style Formatter ───\r\n\r\n/**\r\n * Formats a string using Lua-style placeholders.\r\n *\r\n * Specifiers:\r\n * - `%s` — string (null/undefined becomes empty string)\r\n * - `%d` — integer (floors the value, NaN becomes 0)\r\n * - `%f` — float (NaN becomes 0)\r\n * - `%%` — literal percent sign\r\n *\r\n * @example\r\n * ```ts\r\n * luaFormat(\"Hello %s, you are level %d\", \"Laot\", 42);\r\n * // → \"Hello Laot, you are level 42\"\r\n *\r\n * luaFormat(\"Accuracy: %f%%\", 99.5);\r\n * // → \"Accuracy: 99.5%\"\r\n *\r\n * luaFormat(\"Safe: %s %d\", undefined, NaN);\r\n * // → \"Safe: 0\"\r\n * ```\r\n */\r\nexport function luaFormat(template: string, ...args: FormatArg[]): string {\r\n\tlet argIndex = 0;\r\n\r\n\treturn template.replace(/%([sdfi%])/g, (match, specifier: string) => {\r\n\t\tif (specifier === \"%\") return \"%\";\r\n\r\n\t\tconst raw = args[argIndex++];\r\n\r\n\t\tswitch (specifier) {\r\n\t\t\tcase \"s\":\r\n\t\t\t\treturn String(raw ?? \"\");\r\n\r\n\t\t\tcase \"d\":\r\n\t\t\tcase \"i\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : Math.floor(num));\r\n\t\t\t}\r\n\r\n\t\t\tcase \"f\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : num);\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn match;\r\n\t\t}\r\n\t});\r\n}\r\n\r\n// ─── Dot-Notation Resolver ───\r\n\r\n/**\r\n * Walks a nested locale object by splitting the key on `.`\r\n * Returns the string value at the end of the path, or `undefined` if any segment is missing.\r\n *\r\n * `\"client.greeting\"` → looks up `locales.client.greeting`\r\n * `\"flat_key\"` → looks up `locales.flat_key` directly\r\n */\r\nfunction resolveKey(locales: LocaleRecord, key: string): string | undefined {\r\n\tconst segments = key.split(\".\");\r\n\tlet current: LocaleRecord | string = locales;\r\n\r\n\tfor (const segment of segments) {\r\n\t\tif (typeof current !== \"object\" || current === null) return undefined;\r\n\t\tconst next: string | LocaleRecord | undefined = current[segment];\r\n\t\tif (next === undefined) return undefined;\r\n\t\tcurrent = next;\r\n\t}\r\n\r\n\treturn typeof current === \"string\" ? current : undefined;\r\n}\r\n\r\n// ─── Translator Factory ───\r\n\r\n/**\r\n * Creates a `_U(key, fallback, ...args)` translator bound to a locale record.\r\n *\r\n * The key supports dot-notation to traverse nested locale objects.\r\n * If the key isn't found, the fallback string is returned as-is.\r\n * Any extra args are passed through `luaFormat` for placeholder substitution.\r\n *\r\n * @example\r\n * ```ts\r\n * const _U = createTranslator({\r\n * locales: {\r\n * client: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * server: {\r\n * error: \"Error: %s\",\r\n * },\r\n * flat_key: \"Plain message\",\r\n * },\r\n * });\r\n *\r\n * _U(\"client.greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"client.level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _U(\"flat_key\", \"MISSING\"); // → \"Plain message\"\r\n * _U(\"no.key\", \"Not found\"); // → \"Not found\"\r\n * ```\r\n */\r\nexport function createTranslator(options: TranslatorOptions): TranslatorFn {\r\n\tconst { locales } = options;\r\n\r\n\treturn (key: string, fallback: string, ...args: FormatArg[]): string => {\r\n\t\tconst template = resolveKey(locales, key);\r\n\r\n\t\tif (template === undefined) {\r\n\t\t\treturn fallback;\r\n\t\t}\r\n\r\n\t\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n\t};\r\n}\r\n\r\n// ─── Deep Merge ───\r\n\r\n/**\r\n * Deep-merges multiple locale records into one.\r\n * Later records override earlier ones on key conflicts.\r\n * Nested objects are merged recursively, not replaced entirely.\r\n *\r\n * @example\r\n * ```ts\r\n * const base = { client: { greeting: \"Hello %s!\", level: \"Level %d\" } };\r\n * const overrides = { client: { greeting: \"Hey %s!\" } };\r\n *\r\n * const merged = mergeLocales(base, overrides);\r\n * // merged.client.greeting → \"Hey %s!\"\r\n * // merged.client.level → \"Level %d\" (preserved from base)\r\n * ```\r\n */\r\nexport function mergeLocales(...records: LocaleRecord[]): LocaleRecord {\r\n\tconst result: LocaleRecord = {};\r\n\r\n\tfor (const record of records) {\r\n\t\tfor (const [key, value] of Object.entries(record)) {\r\n\t\t\tconst existing = result[key];\r\n\r\n\t\t\tif (typeof value === \"object\" && typeof existing === \"object\" && existing !== undefined) {\r\n\t\t\t\tresult[key] = mergeLocales(existing, value);\r\n\t\t\t} else {\r\n\t\t\t\tresult[key] = value;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/client.ts","../src/listener.ts","../src/utils.ts"],"sourcesContent":["import type { NuiEventMap, FetchNuiOptions, FetchNuiFactoryOptions } from \"./types\";\r\n\r\n// ─── Resource Name ───\r\n\r\n/**\r\n * Grabs the resource name from FiveM's injected global.\r\n * Falls back to `\"nui-frame-app\"` when running outside the game (local dev, tests).\r\n */\r\nfunction getResourceName(): string {\r\n\tif (typeof window !== \"undefined\" && window.GetParentResourceName) {\r\n\t\treturn window.GetParentResourceName();\r\n\t}\r\n\treturn \"nui-frame-app\";\r\n}\r\n\r\n// ─── FetchNui Factory ───\r\n\r\n/**\r\n * Creates a typed `fetchNui` function for your event map.\r\n * POSTs JSON to `https://<resourceName>/<event>`, matching `RegisterNUICallback` on Lua side.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyEvents extends NuiEventMap {\r\n * getPlayer: { request: { id: number }; response: { name: string } };\r\n * notify: { request: { msg: string }; response: void };\r\n * }\r\n *\r\n * const fetchNui = createFetchNui<MyEvents>();\r\n * const player = await fetchNui(\"getPlayer\", { id: 1 });\r\n *\r\n * // With mocks + debug\r\n * const fetchNui = createFetchNui<MyEvents>({\r\n * debug: true,\r\n * mockData: {\r\n * getPlayer: { name: \"DevPlayer\" },\r\n * notify: (req) => { console.log(\"Mock:\", req.msg); },\r\n * },\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * RegisterNUICallback(\"getPlayer\", function(data, cb)\r\n * local player = GetPlayerData(data.id)\r\n * cb({ name = player.name })\r\n * end)\r\n * ```\r\n */\r\nexport function createFetchNui<TMap extends NuiEventMap>(factoryOptions?: FetchNuiFactoryOptions<TMap>) {\r\n\tconst debug = factoryOptions?.debug ?? false;\r\n\tconst mockData = factoryOptions?.mockData;\r\n\r\n\treturn async function fetchNui<K extends keyof TMap & string>(\r\n\t\tevent: K,\r\n\t\t...args: TMap[K][\"request\"] extends void\r\n\t\t\t? [data?: TMap[K][\"request\"], options?: FetchNuiOptions]\r\n\t\t\t: [data: TMap[K][\"request\"], options?: FetchNuiOptions]\r\n\t): Promise<TMap[K][\"response\"]> {\r\n\t\tconst [data, options] = args;\r\n\t\tif (debug) {\r\n\t\t\tconsole.log(`[NUIX] → ${event}`, data ?? {});\r\n\t\t}\r\n\r\n\t\t// ─── Mock Mode ───\r\n\r\n\t\tif (mockData && event in mockData) {\r\n\t\t\tconst mock = mockData[event];\r\n\r\n\t\t\tif (mock === undefined) {\r\n\t\t\t\tthrow new Error(`[NUIX] Mock data for \"${event}\" is undefined`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result =\r\n\t\t\t\ttypeof mock === \"function\"\r\n\t\t\t\t\t? (mock as (req: TMap[K][\"request\"]) => TMap[K][\"response\"])(data as TMap[K][\"request\"])\r\n\t\t\t\t\t: mock;\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event} (mock)`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result as TMap[K][\"response\"];\r\n\t\t}\r\n\r\n\t\t// ─── Real Fetch ───\r\n\r\n\t\tconst url = `https://${getResourceName()}/${event}`;\r\n\r\n\t\tconst controller = options?.timeout ? new AbortController() : undefined;\r\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n\t\tif (controller && options?.timeout) {\r\n\t\t\ttimeoutId = setTimeout(() => controller.abort(), options.timeout);\r\n\t\t}\r\n\r\n\t\ttry {\r\n\t\t\tconst response = await fetch(url, {\r\n\t\t\t\tmethod: \"POST\",\r\n\t\t\t\theaders: { \"Content-Type\": \"application/json; charset=UTF-8\" },\r\n\t\t\t\tbody: JSON.stringify(data ?? {}),\r\n\t\t\t\tsignal: controller?.signal,\r\n\t\t\t});\r\n\r\n\t\t\tif (!response.ok) {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") failed with HTTP ${response.status}`);\r\n\t\t\t}\r\n\r\n\t\t\tconst result = (await response.json()) as TMap[K][\"response\"];\r\n\r\n\t\t\tif (debug) {\r\n\t\t\t\tconsole.log(`[NUIX] ← ${event}`, result);\r\n\t\t\t}\r\n\r\n\t\t\treturn result;\r\n\t\t} catch (error) {\r\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\r\n\t\t\t\tthrow new Error(`[NUIX] fetchNui(\"${event}\") timed out after ${options?.timeout}ms`);\r\n\t\t\t}\r\n\t\t\tthrow error;\r\n\t\t} finally {\r\n\t\t\tif (timeoutId !== undefined) clearTimeout(timeoutId);\r\n\t\t}\r\n\t};\r\n}\r\n","import type { NuiEventMap, NuiMessagePayload, NuiMessageHandler, UnsubscribeFn } from \"./types\";\r\n\r\n// ─── NUI Message Listener ───\r\n\r\n/**\r\n * Listens for NUI messages from Lua (`SendNUIMessage`).\r\n *\r\n * **Per-action** — filters by action name, `data` is fully typed:\r\n * ```ts\r\n * onNuiMessage<Events, \"showMenu\">(\"showMenu\", (data) => {\r\n * console.log(data.items); // ✅ typed as string[]\r\n * });\r\n * ```\r\n *\r\n * **Switch-case** — single listener for all actions:\r\n * ```ts\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * case \"showMenu\":\r\n * openMenu(data.items);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n *\r\n * Lua side:\r\n * ```lua\r\n * SendNUIMessage({ action = \"showMenu\", data = { items = {\"Pistol\", \"Rifle\"} } })\r\n * ```\r\n */\r\n\r\n// Per-action overload\r\nexport function onNuiMessage<TMap extends NuiEventMap, K extends keyof TMap & string>(\r\n\taction: K,\r\n\thandler: NuiMessageHandler<TMap[K][\"request\"]>,\r\n): UnsubscribeFn;\r\n\r\n// Switch-case overload\r\nexport function onNuiMessage<TMap extends NuiEventMap>(\r\n\thandler: (action: keyof TMap & string, data: any) => void,\r\n): UnsubscribeFn;\r\n\r\n// Implementation\r\nexport function onNuiMessage(\r\n\tactionOrHandler: string | ((action: string, data: unknown) => void),\r\n\thandler?: (data: unknown) => void,\r\n): UnsubscribeFn {\r\n\tconst listener = (event: MessageEvent<NuiMessagePayload>) => {\r\n\t\tconst payload = event.data;\r\n\r\n\t\tif (!payload || typeof payload !== \"object\") return;\r\n\t\tif (!payload.action) return;\r\n\r\n\t\tif (typeof actionOrHandler === \"string\") {\r\n\t\t\tif (payload.action !== actionOrHandler) return;\r\n\t\t\thandler!(payload.data);\r\n\t\t} else {\r\n\t\t\tactionOrHandler(payload.action, payload.data);\r\n\t\t}\r\n\t};\r\n\r\n\twindow.addEventListener(\"message\", listener);\r\n\r\n\treturn () => {\r\n\t\twindow.removeEventListener(\"message\", listener);\r\n\t};\r\n}\r\n","import type { FormatArg, LocaleRecord, TranslatorFn, TranslatorOptions } from \"./types\";\r\n\r\n// ─── Lua-Style Formatter ───\r\n\r\n/**\r\n * Formats a string using Lua-style placeholders.\r\n *\r\n * Specifiers:\r\n * - `%s` — string (null/undefined becomes empty string)\r\n * - `%d` / `%i` — integer (floors the value, NaN becomes 0)\r\n * - `%f` — float (NaN becomes 0)\r\n * - `%%` — literal percent sign\r\n *\r\n * @example\r\n * ```ts\r\n * luaFormat(\"Hello %s, you are level %d\", \"Laot\", 42);\r\n * // → \"Hello Laot, you are level 42\"\r\n *\r\n * luaFormat(\"Accuracy: %f%%\", 99.5);\r\n * // → \"Accuracy: 99.5%\"\r\n *\r\n * luaFormat(\"Safe: %s %d\", undefined, NaN);\r\n * // → \"Safe: 0\"\r\n * ```\r\n */\r\nexport function luaFormat(template: string, ...args: FormatArg[]): string {\r\n\tlet argIndex = 0;\r\n\r\n\treturn template.replace(/%([sdfi%])/g, (match, specifier: string) => {\r\n\t\tif (specifier === \"%\") return \"%\";\r\n\r\n\t\tconst raw = args[argIndex++];\r\n\r\n\t\tswitch (specifier) {\r\n\t\t\tcase \"s\":\r\n\t\t\t\treturn String(raw ?? \"\");\r\n\r\n\t\t\tcase \"d\":\r\n\t\t\tcase \"i\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : Math.floor(num));\r\n\t\t\t}\r\n\r\n\t\t\tcase \"f\": {\r\n\t\t\t\tconst num = Number(raw);\r\n\t\t\t\treturn String(Number.isNaN(num) ? 0 : num);\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn match;\r\n\t\t}\r\n\t});\r\n}\r\n\r\n// ─── Dot-Notation Resolver ───\r\n\r\n/**\r\n * Walks a nested locale object by splitting the key on `.`\r\n * Returns the string value at the end of the path, or `undefined` if any segment is missing.\r\n *\r\n * `\"client.greeting\"` → looks up `locales.client.greeting`\r\n * `\"flat_key\"` → looks up `locales.flat_key` directly\r\n */\r\nfunction resolveKey(locales: LocaleRecord, key: string): string | undefined {\r\n\tconst segments = key.split(\".\");\r\n\tlet current: LocaleRecord | string = locales;\r\n\r\n\tfor (const segment of segments) {\r\n\t\tif (typeof current !== \"object\" || current === null) return undefined;\r\n\t\tconst next: string | LocaleRecord | undefined = current[segment];\r\n\t\tif (next === undefined) return undefined;\r\n\t\tcurrent = next;\r\n\t}\r\n\r\n\treturn typeof current === \"string\" ? current : undefined;\r\n}\r\n\r\n// ─── Translator Factory ───\r\n\r\n/**\r\n * Creates an isolated translator bound to a specific locale record.\r\n * Use this when you need a separate translator instance, independent of the global `_U`.\r\n *\r\n * @example\r\n * ```ts\r\n * const _T = createTranslator({\r\n * locales: {\r\n * greeting: \"Hello %s!\",\r\n * level: \"Level %d\",\r\n * },\r\n * });\r\n *\r\n * _T(\"greeting\", \"MISSING\", \"Laot\"); // → \"Hello Laot!\"\r\n * _T(\"level\", \"MISSING\", 42); // → \"Level 42\"\r\n * _T(\"no.key\", \"Not found\"); // → \"Not found\"\r\n * ```\r\n */\r\nexport function createTranslator(options: TranslatorOptions): TranslatorFn {\r\n\tconst { locales } = options;\r\n\r\n\treturn (key: string, fallback: string, ...args: FormatArg[]): string => {\r\n\t\tconst template = resolveKey(locales, key);\r\n\r\n\t\tif (template === undefined) {\r\n\t\t\treturn fallback;\r\n\t\t}\r\n\r\n\t\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n\t};\r\n}\r\n\r\n// ─── Global Locale Registry ───\r\n\r\nlet _locales: LocaleRecord = {};\r\n\r\n/**\r\n * Sets the global locale map. Call this when Lua sends locale data to the NUI.\r\n *\r\n * @example\r\n * ```ts\r\n * // Lua side:\r\n * // SendNUIMessage({ action = \"setLocales\", data = locales })\r\n *\r\n * onNuiMessage<Events>((action, data) => {\r\n * switch (action) {\r\n * case \"setLocales\":\r\n * registerLocales(data);\r\n * break;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport function registerLocales(locales: LocaleRecord): void {\r\n\t_locales = locales;\r\n}\r\n\r\n/**\r\n * Merges new entries into the current global locale map without replacing it.\r\n *\r\n * @example\r\n * ```ts\r\n * registerLocales({ ui: { title: \"Dashboard\" } });\r\n * extendLocales({ ui: { subtitle: \"Overview\" } });\r\n *\r\n * _U(\"ui.title\", \"\"); // → \"Dashboard\"\r\n * _U(\"ui.subtitle\", \"\"); // → \"Overview\"\r\n * ```\r\n */\r\nexport function extendLocales(...records: LocaleRecord[]): void {\r\n\t_locales = mergeLocales(_locales, ...records);\r\n}\r\n\r\n/**\r\n * Global translator — reads from the locale map set by `registerLocales` / `extendLocales`.\r\n *\r\n * @param key Dot-notated key, e.g. `\"ui.greeting\"` or flat `\"title\"`\r\n * @param fallback Returned as-is when the key doesn't exist\r\n * @param args Values for `%s`, `%d`, `%f` placeholders\r\n *\r\n * @example\r\n * ```ts\r\n * import { registerLocales, _U } from \"@laot/nuix\";\r\n *\r\n * // After Lua sends locales:\r\n * // { ui: { greeting: \"Hello %s!\", level: \"Level %d\" } }\r\n *\r\n * _U(\"ui.greeting\", \"Hi\", \"Laot\"); // → \"Hello Laot!\"\r\n * _U(\"ui.level\", \"Lv.\", 42); // → \"Level 42\"\r\n * _U(\"missing.key\", \"Fallback\"); // → \"Fallback\"\r\n * ```\r\n */\r\nexport function _U(key: string, fallback: string, ...args: FormatArg[]): string {\r\n\tconst template = resolveKey(_locales, key);\r\n\tif (template === undefined) return fallback;\r\n\treturn args.length > 0 ? luaFormat(template, ...args) : template;\r\n}\r\n\r\n// ─── Deep Merge ───\r\n\r\n/**\r\n * Deep-merges multiple locale records into one.\r\n * Later records override earlier ones on key conflicts.\r\n * Nested objects are merged recursively, not replaced entirely.\r\n *\r\n * @example\r\n * ```ts\r\n * const base = { client: { greeting: \"Hello %s!\", level: \"Level %d\" } };\r\n * const overrides = { client: { greeting: \"Hey %s!\" } };\r\n *\r\n * const merged = mergeLocales(base, overrides);\r\n * // merged.client.greeting → \"Hey %s!\"\r\n * // merged.client.level → \"Level %d\" (preserved from base)\r\n * ```\r\n */\r\nexport function mergeLocales(...records: LocaleRecord[]): LocaleRecord {\r\n\tconst result: LocaleRecord = {};\r\n\r\n\tfor (const record of records) {\r\n\t\tfor (const [key, value] of Object.entries(record)) {\r\n\t\t\tconst existing = result[key];\r\n\r\n\t\t\tif (\r\n\t\t\t\tvalue !== null &&\r\n\t\t\t\texisting !== null &&\r\n\t\t\t\ttypeof value === \"object\" &&\r\n\t\t\t\ttypeof existing === \"object\" &&\r\n\t\t\t\texisting !== undefined\r\n\t\t\t) {\r\n\t\t\t\tresult[key] = mergeLocales(existing, value);\r\n\t\t\t} else {\r\n\t\t\t\tresult[key] = value;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@laot/nuix",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Modular, type-safe TypeScript library for FiveM NUI projects",
5
+ "sideEffects": false,
5
6
  "type": "module",
6
7
  "main": "./dist/index.cjs",
7
8
  "module": "./dist/index.js",
@@ -42,6 +43,9 @@
42
43
  "url": "https://github.com/laot7490/nuix/issues"
43
44
  },
44
45
  "license": "MIT",
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ },
45
49
  "devDependencies": {
46
50
  "tsup": "^8.4.0",
47
51
  "typescript": "^5.7.3"