@tstdl/base 0.93.139 → 0.93.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +166 -0
  2. package/ai/genkit/multi-region.plugin.js +5 -3
  3. package/ai/genkit/tests/multi-region.test.d.ts +1 -0
  4. package/ai/genkit/tests/multi-region.test.js +5 -2
  5. package/ai/parser/parser.js +2 -2
  6. package/ai/prompts/build.js +1 -0
  7. package/ai/prompts/instructions-formatter.d.ts +15 -2
  8. package/ai/prompts/instructions-formatter.js +36 -31
  9. package/ai/prompts/prompt-builder.js +5 -5
  10. package/ai/prompts/steering.d.ts +3 -2
  11. package/ai/prompts/steering.js +3 -1
  12. package/ai/tests/instructions-formatter.test.js +1 -0
  13. package/api/README.md +403 -0
  14. package/api/client/client.js +7 -13
  15. package/api/client/tests/api-client.test.js +10 -10
  16. package/api/default-error-handlers.js +1 -1
  17. package/api/response.d.ts +2 -2
  18. package/api/response.js +22 -33
  19. package/api/server/api-controller.d.ts +1 -1
  20. package/api/server/api-controller.js +3 -3
  21. package/api/server/api-request-token.provider.d.ts +1 -0
  22. package/api/server/api-request-token.provider.js +1 -0
  23. package/api/server/middlewares/allowed-methods.middleware.js +2 -1
  24. package/api/server/middlewares/content-type.middleware.js +2 -1
  25. package/api/types.d.ts +3 -2
  26. package/application/README.md +240 -0
  27. package/application/application.d.ts +1 -1
  28. package/application/application.js +3 -3
  29. package/application/providers.d.ts +20 -2
  30. package/application/providers.js +34 -7
  31. package/audit/README.md +267 -0
  32. package/audit/module.d.ts +5 -0
  33. package/audit/module.js +9 -1
  34. package/authentication/README.md +288 -0
  35. package/authentication/client/authentication.service.d.ts +12 -11
  36. package/authentication/client/authentication.service.js +21 -21
  37. package/authentication/client/http-client.middleware.js +2 -2
  38. package/authentication/server/module.d.ts +5 -0
  39. package/authentication/server/module.js +9 -1
  40. package/authentication/tests/authentication.api-controller.test.js +1 -1
  41. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  42. package/authentication/tests/authentication.client-error-handling.test.js +2 -1
  43. package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
  44. package/authentication/tests/authentication.client-service.test.js +1 -1
  45. package/browser/README.md +401 -0
  46. package/cancellation/README.md +156 -0
  47. package/cancellation/tests/coverage.test.d.ts +1 -0
  48. package/cancellation/tests/coverage.test.js +49 -0
  49. package/cancellation/tests/leak.test.js +24 -29
  50. package/cancellation/tests/token.test.d.ts +1 -0
  51. package/cancellation/tests/token.test.js +136 -0
  52. package/cancellation/token.d.ts +53 -177
  53. package/cancellation/token.js +132 -208
  54. package/circuit-breaker/postgres/module.d.ts +1 -0
  55. package/circuit-breaker/postgres/module.js +5 -1
  56. package/context/README.md +174 -0
  57. package/cookie/README.md +161 -0
  58. package/css/README.md +157 -0
  59. package/data-structures/README.md +320 -0
  60. package/decorators/README.md +140 -0
  61. package/distributed-loop/README.md +231 -0
  62. package/distributed-loop/distributed-loop.js +1 -1
  63. package/document-management/README.md +403 -0
  64. package/document-management/server/configure.js +5 -1
  65. package/document-management/server/module.d.ts +1 -1
  66. package/document-management/server/module.js +1 -1
  67. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  68. package/document-management/server/services/document-management.service.js +9 -7
  69. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  70. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  71. package/document-management/tests/document-management-core.test.js +2 -7
  72. package/document-management/tests/document-management.api.test.js +6 -7
  73. package/document-management/tests/document-statistics.service.test.js +11 -12
  74. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  75. package/document-management/tests/document.service.test.js +3 -3
  76. package/document-management/tests/enum-helpers.test.js +2 -3
  77. package/dom/README.md +213 -0
  78. package/enumerable/README.md +259 -0
  79. package/enumeration/README.md +121 -0
  80. package/errors/README.md +267 -0
  81. package/examples/document-management/main.d.ts +1 -0
  82. package/examples/document-management/main.js +14 -11
  83. package/file/README.md +191 -0
  84. package/formats/README.md +210 -0
  85. package/function/README.md +144 -0
  86. package/http/README.md +318 -0
  87. package/http/client/adapters/undici.adapter.js +1 -1
  88. package/http/client/http-client-request.d.ts +6 -5
  89. package/http/client/http-client-request.js +8 -9
  90. package/http/server/node/node-http-server.js +1 -2
  91. package/image-service/README.md +137 -0
  92. package/injector/README.md +491 -0
  93. package/intl/README.md +113 -0
  94. package/json-path/README.md +182 -0
  95. package/jsx/README.md +154 -0
  96. package/key-value-store/README.md +191 -0
  97. package/key-value-store/postgres/module.d.ts +1 -0
  98. package/key-value-store/postgres/module.js +5 -1
  99. package/lock/README.md +249 -0
  100. package/lock/postgres/module.d.ts +1 -0
  101. package/lock/postgres/module.js +5 -1
  102. package/lock/web/web-lock.js +119 -47
  103. package/logger/README.md +287 -0
  104. package/mail/README.md +256 -0
  105. package/mail/module.d.ts +5 -1
  106. package/mail/module.js +11 -6
  107. package/memory/README.md +144 -0
  108. package/message-bus/README.md +244 -0
  109. package/message-bus/message-bus-base.js +1 -1
  110. package/module/README.md +182 -0
  111. package/module/module.d.ts +1 -1
  112. package/module/module.js +77 -17
  113. package/module/modules/web-server.module.js +3 -4
  114. package/notification/server/module.d.ts +1 -0
  115. package/notification/server/module.js +5 -1
  116. package/notification/tests/notification-flow.test.js +2 -2
  117. package/notification/tests/notification-type.service.test.js +24 -15
  118. package/object-storage/README.md +300 -0
  119. package/openid-connect/README.md +274 -0
  120. package/orm/README.md +423 -0
  121. package/orm/decorators.d.ts +5 -1
  122. package/orm/decorators.js +1 -1
  123. package/orm/server/drizzle/schema-converter.js +17 -30
  124. package/orm/server/encryption.d.ts +0 -1
  125. package/orm/server/encryption.js +1 -4
  126. package/orm/server/index.d.ts +1 -6
  127. package/orm/server/index.js +1 -6
  128. package/orm/server/migration.d.ts +19 -0
  129. package/orm/server/migration.js +72 -0
  130. package/orm/server/repository.d.ts +1 -1
  131. package/orm/server/transaction.d.ts +5 -10
  132. package/orm/server/transaction.js +22 -26
  133. package/orm/server/transactional.js +3 -3
  134. package/orm/tests/database-migration.test.d.ts +1 -0
  135. package/orm/tests/database-migration.test.js +82 -0
  136. package/orm/tests/encryption.test.js +3 -4
  137. package/orm/utils.d.ts +17 -2
  138. package/orm/utils.js +49 -1
  139. package/package.json +9 -6
  140. package/password/README.md +164 -0
  141. package/pdf/README.md +246 -0
  142. package/polyfills.js +1 -0
  143. package/pool/README.md +198 -0
  144. package/process/README.md +237 -0
  145. package/promise/README.md +252 -0
  146. package/promise/cancelable-promise.js +1 -1
  147. package/random/README.md +193 -0
  148. package/rate-limit/postgres/module.d.ts +1 -0
  149. package/rate-limit/postgres/module.js +5 -1
  150. package/reflection/README.md +305 -0
  151. package/reflection/decorator-data.js +11 -12
  152. package/rpc/README.md +386 -0
  153. package/rxjs-utils/README.md +262 -0
  154. package/schema/README.md +342 -0
  155. package/serializer/README.md +342 -0
  156. package/signals/implementation/README.md +134 -0
  157. package/sse/README.md +278 -0
  158. package/task-queue/README.md +293 -0
  159. package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
  160. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
  161. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  162. package/task-queue/postgres/module.d.ts +1 -0
  163. package/task-queue/postgres/module.js +5 -1
  164. package/task-queue/postgres/schemas.d.ts +9 -6
  165. package/task-queue/postgres/schemas.js +4 -3
  166. package/task-queue/postgres/task-queue.d.ts +4 -13
  167. package/task-queue/postgres/task-queue.js +462 -355
  168. package/task-queue/postgres/task.model.d.ts +12 -5
  169. package/task-queue/postgres/task.model.js +51 -25
  170. package/task-queue/task-context.d.ts +2 -2
  171. package/task-queue/task-context.js +8 -8
  172. package/task-queue/task-queue.d.ts +53 -19
  173. package/task-queue/task-queue.js +121 -55
  174. package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
  175. package/task-queue/tests/cascading-cancellations.test.js +38 -0
  176. package/task-queue/tests/complex.test.js +45 -229
  177. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  178. package/task-queue/tests/coverage-branch.test.js +407 -0
  179. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  180. package/task-queue/tests/coverage-enhancement.test.js +144 -0
  181. package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
  182. package/task-queue/tests/dag-dependencies.test.js +41 -0
  183. package/task-queue/tests/dependencies.test.js +28 -26
  184. package/task-queue/tests/extensive-dependencies.test.js +64 -139
  185. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  186. package/task-queue/tests/fan-out-spawning.test.js +53 -0
  187. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  188. package/task-queue/tests/idempotent-replacement.test.js +61 -0
  189. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  190. package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
  191. package/task-queue/tests/queue.test.js +128 -8
  192. package/task-queue/tests/worker.test.js +39 -16
  193. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  194. package/task-queue/tests/zombie-parent.test.js +45 -0
  195. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  196. package/task-queue/tests/zombie-recovery.test.js +51 -0
  197. package/templates/README.md +287 -0
  198. package/test5.js +5 -5
  199. package/testing/README.md +157 -0
  200. package/testing/integration-setup.d.ts +4 -4
  201. package/testing/integration-setup.js +54 -29
  202. package/text/README.md +346 -0
  203. package/text/localization.service.js +2 -2
  204. package/threading/README.md +238 -0
  205. package/types/README.md +311 -0
  206. package/utils/README.md +322 -0
  207. package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
  208. package/utils/async-iterable-helpers/observable-iterable.js +4 -8
  209. package/utils/async-iterable-helpers/take-until.js +4 -4
  210. package/utils/backoff.js +89 -30
  211. package/utils/file-reader.js +1 -2
  212. package/utils/retry-with-backoff.js +1 -1
  213. package/utils/timer.d.ts +1 -1
  214. package/utils/timer.js +5 -7
  215. package/utils/timing.d.ts +1 -1
  216. package/utils/timing.js +2 -4
  217. package/utils/z-base32.d.ts +1 -0
  218. package/utils/z-base32.js +1 -0
@@ -1,30 +1,31 @@
1
1
  import { sql } from 'drizzle-orm';
2
2
  import { configureApiServer } from '../api/server/index.js';
3
- import { configureAudit, migrateAuditSchema } from '../audit/index.js';
3
+ import { configureAudit } from '../audit/index.js';
4
4
  import { AuthenticationApiClient } from '../authentication/client/api.client.js';
5
5
  import { configureAuthenticationClient } from '../authentication/client/index.js';
6
- import { AuthenticationApiController, configureAuthenticationServer, migrateAuthenticationSchema } from '../authentication/server/index.js';
7
- import { configurePostgresCircuitBreaker, migratePostgresCircuitBreaker } from '../circuit-breaker/postgres/module.js';
8
- import { configureDocumentManagement, migrateDocumentManagementSchema } from '../document-management/server/index.js';
6
+ import { AuthenticationApiController, configureAuthenticationServer } from '../authentication/server/index.js';
7
+ import { configurePostgresCircuitBreaker } from '../circuit-breaker/postgres/module.js';
8
+ import { configureDocumentManagement } from '../document-management/server/index.js';
9
9
  import { configureUndiciHttpClientAdapter } from '../http/client/adapters/undici.adapter.js';
10
10
  import { configureHttpClient } from '../http/client/index.js';
11
11
  import { HttpServer } from '../http/server/index.js';
12
12
  import { configureNodeHttpServer } from '../http/server/node/module.js';
13
13
  import { Injector, runInInjectionContext } from '../injector/index.js';
14
- import { configurePostgresKeyValueStore, migratePostgresKeyValueStoreSchema } from '../key-value-store/postgres/module.js';
15
- import { configurePostgresLock, migratePostgresLockSchema } from '../lock/postgres/module.js';
14
+ import { configurePostgresKeyValueStore } from '../key-value-store/postgres/module.js';
15
+ import { configurePostgresLock } from '../lock/postgres/module.js';
16
16
  import { ConsoleLogTransport, DEFAULT_LOG_LEVEL, LogFormatter, LogLevel, LogManager, LogTransport, PrettyPrintLogFormatter } from '../logger/index.js';
17
17
  import { configureLocalMessageBus } from '../message-bus/index.js';
18
18
  import { configureWebServerModule, WebServerModule } from '../module/modules/web-server.module.js';
19
- import { configureNotification, migrateNotificationSchema } from '../notification/server/index.js';
19
+ import { configureNotification } from '../notification/server/index.js';
20
20
  import { configureS3ObjectStorage } from '../object-storage/s3/index.js';
21
- import { configureOrm, Database } from '../orm/server/index.js';
22
- import { configurePostgresRateLimiter, migratePostgresRateLimiterSchema } from '../rate-limit/postgres/module.js';
21
+ import { configureOrm, Database, runDatabaseMigrations } from '../orm/server/index.js';
22
+ import { getEntitySchema, getEntityTableName } from '../orm/utils.js';
23
+ import { configurePostgresRateLimiter } from '../rate-limit/postgres/module.js';
23
24
  import { configureDefaultSignalsImplementation } from '../signals/implementation/configure.js';
24
- import { configurePostgresTaskQueue, migratePostgresTaskQueueSchema } from '../task-queue/postgres/index.js';
25
+ import { configurePostgresTaskQueue } from '../task-queue/postgres/index.js';
25
26
  import * as configParser from '../utils/config-parser.js';
26
27
  import { objectEntries } from '../utils/object/object.js';
27
- import { isDefined, isNotNull } from '../utils/type-guards.js';
28
+ import { assertDefinedPass, isDefined, isNotNull, isString } from '../utils/type-guards.js';
28
29
  /**
29
30
  * Standard setup for integration tests.
30
31
  */
@@ -51,6 +52,8 @@ export async function setupIntegrationTest(options = {}) {
51
52
  ...options.dbConfig,
52
53
  };
53
54
  // 4. Configure ORM
55
+ // We disable autoMigrate here because APPLICATION_INITIALIZER is not used in integration tests
56
+ // We manually run migrations via runDatabaseMigrations below
54
57
  configureOrm({
55
58
  repositoryConfig: { schema: options.orm?.schema ?? 'test' },
56
59
  connection: dbConfig,
@@ -66,35 +69,29 @@ export async function setupIntegrationTest(options = {}) {
66
69
  await database.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(options.orm.schema)}`);
67
70
  }
68
71
  // 7. Optional Modules
69
- if (options.modules?.messageBus ?? options.modules?.taskQueue ?? options.modules?.authentication ?? options.modules?.test ?? options.modules?.notification) {
72
+ if (options.modules?.messageBus ?? options.modules?.taskQueue ?? options.modules?.authentication ?? options.modules?.notification) {
70
73
  configureLocalMessageBus({ injector });
71
74
  }
72
75
  if (options.modules?.taskQueue) {
73
76
  configurePostgresTaskQueue({ injector });
74
- await runInInjectionContext(injector, migratePostgresTaskQueueSchema);
75
77
  }
76
78
  if (options.modules?.circuitBreaker ?? options.modules?.taskQueue) {
77
79
  configurePostgresCircuitBreaker({ injector });
78
- await runInInjectionContext(injector, migratePostgresCircuitBreaker);
79
80
  }
80
81
  if (options.modules?.rateLimiter ?? options.modules?.taskQueue) {
81
82
  configurePostgresRateLimiter({ injector });
82
- await runInInjectionContext(injector, migratePostgresRateLimiterSchema);
83
83
  }
84
84
  if (options.modules?.keyValueStore ?? options.modules?.authentication) {
85
85
  configurePostgresKeyValueStore({ injector });
86
- await runInInjectionContext(injector, migratePostgresKeyValueStoreSchema);
87
86
  }
88
87
  if (options.modules?.lock ?? options.modules?.authentication) {
89
88
  configurePostgresLock({ injector });
90
- await runInInjectionContext(injector, migratePostgresLockSchema);
91
89
  }
92
- if (options.modules?.signals ?? options.modules?.authentication ?? options.modules?.test ?? options.modules?.notification) {
90
+ if (options.modules?.signals ?? options.modules?.authentication ?? options.modules?.notification) {
93
91
  configureDefaultSignalsImplementation();
94
92
  }
95
93
  if (options.modules?.audit ?? options.modules?.authentication) {
96
94
  configureAudit({ injector });
97
- await runInInjectionContext(injector, migrateAuditSchema);
98
95
  }
99
96
  if (options.modules?.authentication) {
100
97
  configureAuthenticationServer({
@@ -106,11 +103,9 @@ export async function setupIntegrationTest(options = {}) {
106
103
  authenticationAncillaryService: options.authenticationAncillaryService,
107
104
  injector,
108
105
  });
109
- await runInInjectionContext(injector, migrateAuthenticationSchema);
110
106
  }
111
107
  if (options.modules?.notification) {
112
108
  configureNotification({ injector });
113
- await runInInjectionContext(injector, migrateNotificationSchema);
114
109
  }
115
110
  if (options.modules?.documentManagement) {
116
111
  configureDocumentManagement({
@@ -122,8 +117,8 @@ export async function setupIntegrationTest(options = {}) {
122
117
  skipAi: true,
123
118
  injector,
124
119
  });
125
- await runInInjectionContext(injector, migrateDocumentManagementSchema);
126
120
  }
121
+ await runInInjectionContext(injector, runDatabaseMigrations);
127
122
  if (options.modules?.objectStorage) {
128
123
  const bucketPerModule = options.s3?.bucketPerModule ?? configParser.boolean('S3_BUCKET_PER_MODULE', true);
129
124
  configureS3ObjectStorage({
@@ -190,9 +185,18 @@ export async function truncateTables(database, schema, tables) {
190
185
  if (tables.length == 0) {
191
186
  return;
192
187
  }
193
- // Using CASCADE to handle foreign keys automatically
194
- for (const table of tables) {
195
- await database.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier(table)} CASCADE`);
188
+ const lockId = 987654321; // Different lock ID for table maintenance
189
+ await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
190
+ try {
191
+ // Using CASCADE to handle foreign keys automatically
192
+ for (const table of tables) {
193
+ const tableName = isString(table) ? table : getEntityTableName(table);
194
+ const tableSchema = isString(table) ? schema : getEntitySchema(table, schema);
195
+ await database.execute(sql `TRUNCATE TABLE ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} CASCADE`);
196
+ }
197
+ }
198
+ finally {
199
+ await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
196
200
  }
197
201
  }
198
202
  /**
@@ -203,8 +207,17 @@ export async function clearTenantData(database, schema, tables, tenantId) {
203
207
  if (tables.length == 0) {
204
208
  return;
205
209
  }
206
- for (const table of tables) {
207
- await database.execute(sql `DELETE FROM ${sql.identifier(schema)}.${sql.identifier(table)} WHERE tenant_id = ${tenantId}`);
210
+ const lockId = 987654321;
211
+ await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
212
+ try {
213
+ for (const table of tables) {
214
+ const tableName = isString(table) ? table : getEntityTableName(table);
215
+ const tableSchema = isString(table) ? assertDefinedPass(schema, 'Schema not provided') : getEntitySchema(table, schema);
216
+ await database.execute(sql `DELETE FROM ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} WHERE tenant_id = ${tenantId}`);
217
+ }
218
+ }
219
+ finally {
220
+ await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
208
221
  }
209
222
  }
210
223
  /**
@@ -212,7 +225,19 @@ export async function clearTenantData(database, schema, tables, tenantId) {
212
225
  * Useful in beforeAll() cleanups.
213
226
  */
214
227
  export async function dropTables(database, schema, tables) {
215
- for (const table of tables) {
216
- await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier(table)} CASCADE`);
228
+ if (tables.length == 0) {
229
+ return;
230
+ }
231
+ const lockId = 987654321;
232
+ await database.execute(sql `SELECT pg_advisory_lock(${lockId})`);
233
+ try {
234
+ for (const table of tables) {
235
+ const tableName = isString(table) ? table : getEntityTableName(table);
236
+ const tableSchema = isString(table) ? assertDefinedPass(schema, 'Schema not provided') : getEntitySchema(table, schema);
237
+ await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} CASCADE`);
238
+ }
239
+ }
240
+ finally {
241
+ await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
217
242
  }
218
243
  }
package/text/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # @tstdl/base/text
2
+
3
+ A powerful, reactive, and type-safe text localization module for TypeScript applications, built on signals. It provides seamless integration for internationalization (i18n) with automatic UI updates, parameter interpolation, and strong compile-time safety.
4
+
5
+ ## Table of Contents
6
+
7
+ - [✨ Features](#-features)
8
+ - [Core Concepts](#core-concepts)
9
+ - [LocalizationService](#localizationservice)
10
+ - [Localization Definitions](#localization-definitions)
11
+ - [DynamicText](#dynamictext)
12
+ - [Type-Safe Keys](#type-safe-keys)
13
+ - [🚀 Basic Usage](#-basic-usage)
14
+ - [1. Define Localization Structure](#1-define-localization-structure)
15
+ - [2. Create Localization Data](#2-create-localization-data)
16
+ - [3. Register and Use](#3-register-and-use)
17
+ - [🔧 Advanced Topics](#-advanced-topics)
18
+ - [Reactive Localization with Signals](#reactive-localization-with-signals)
19
+ - [Parameterized Text](#parameterized-text)
20
+ - [Localization Functions](#localization-functions)
21
+ - [Localizing Enums](#localizing-enums)
22
+ - [Resolving Nested Dynamic Text](#resolving-nested-dynamic-text)
23
+ - [Common Localizations](#common-localizations)
24
+ - [📚 API](#-api)
25
+
26
+ ## ✨ Features
27
+
28
+ - **Type-Safe Keys**: Leverage TypeScript proxies to provide compile-time safety and autocompletion for localization keys, eliminating magic strings.
29
+ - **Reactive Architecture**: Built on Signals, allowing text to automatically update whenever the active language changes without manual subscription management.
30
+ - **Dynamic Text Support**: Uniformly handle static strings, Signals, and Observables as localizable content.
31
+ - **Parameter Interpolation**: Easily inject values into translation strings (e.g., `Hello {{name}}`).
32
+ - **Functional Localization**: Use functions for complex logic like pluralization or conditional formatting.
33
+ - **Enum Support**: First-class utilities for localizing TypeScript enums.
34
+ - **RxJS Compatibility**: Includes Observable-based alternatives for all reactive methods.
35
+
36
+ ## Core Concepts
37
+
38
+ ### LocalizationService
39
+
40
+ The `LocalizationService` is the singleton central hub. It manages the active language, stores translation data, and performs the actual text resolution. It exposes reactive signals for the current language and available languages.
41
+
42
+ ### Localization Definitions
43
+
44
+ A `Localization` object defines the translations for a specific language. It consists of:
45
+
46
+ - **`language`**: Metadata like code (`en`) and name (`English`).
47
+ - **`keys`**: A nested object structure containing the translation strings or functions.
48
+ - **`enums`**: Definitions for translating TypeScript enums.
49
+
50
+ ### DynamicText
51
+
52
+ `DynamicText` is a type alias for `ReactiveValue<LocalizableText>`. It represents a piece of text that might be:
53
+
54
+ 1. A static string or localization key.
55
+ 2. A `Signal` emitting localization keys.
56
+ 3. An `Observable` emitting localization keys.
57
+
58
+ The module provides utilities to "resolve" a `DynamicText` into a `Signal<string>` that updates if either the source value changes _or_ the active language changes.
59
+
60
+ ### Type-Safe Keys
61
+
62
+ Instead of using dot-notation strings (e.g., `'home.welcome'`), this module uses a proxy object generated by `getLocalizationKeys()`. This allows you to pass around references to keys (e.g., `keys.home.welcome`) that TypeScript understands, enabling refactoring support and compile-time checks.
63
+
64
+ ## 🚀 Basic Usage
65
+
66
+ ### 1. Define Localization Structure
67
+
68
+ Define the shape of your localization keys using TypeScript types. This ensures all languages implement the same keys.
69
+
70
+ ```typescript
71
+ import { type Localization, type LocalizeItem, getLocalizationKeys } from '@tstdl/base/text';
72
+
73
+ // Define the structure
74
+ type AppLocalization = Localization<{
75
+ app: {
76
+ title: LocalizeItem;
77
+ greeting: LocalizeItem;
78
+ };
79
+ }>;
80
+
81
+ // Create the type-safe key proxy
82
+ export const keys = getLocalizationKeys<AppLocalization>();
83
+ ```
84
+
85
+ ### 2. Create Localization Data
86
+
87
+ Implement the localization for your supported languages.
88
+
89
+ ```typescript
90
+ import { type AppLocalization } from './localization-types'; // from step 1
91
+
92
+ export const english: AppLocalization = {
93
+ language: { code: 'en', name: 'English' },
94
+ keys: {
95
+ app: {
96
+ title: 'My Awesome App',
97
+ greeting: 'Hello World',
98
+ },
99
+ },
100
+ enums: [],
101
+ };
102
+
103
+ export const german: AppLocalization = {
104
+ language: { code: 'de', name: 'Deutsch' },
105
+ keys: {
106
+ app: {
107
+ title: 'Meine Tolle App',
108
+ greeting: 'Hallo Welt',
109
+ },
110
+ },
111
+ enums: [],
112
+ };
113
+ ```
114
+
115
+ ### 3. Register and Use
116
+
117
+ Inject the service, register the languages, and resolve text.
118
+
119
+ ```typescript
120
+ import { inject } from '@tstdl/base/injector';
121
+ import { LocalizationService } from '@tstdl/base/text';
122
+ import { keys } from './localization-types';
123
+ import { english, german } from './localizations';
124
+
125
+ const localizationService = inject(LocalizationService);
126
+
127
+ // Register languages
128
+ localizationService.registerLocalization(english, german);
129
+
130
+ // Set active language
131
+ localizationService.setLanguage('en');
132
+
133
+ // Resolve text reactively (returns a Signal)
134
+ const titleSignal = localizationService.localize(keys.app.title);
135
+
136
+ console.log(titleSignal()); // "My Awesome App"
137
+
138
+ // Switch language
139
+ localizationService.setLanguage('de');
140
+
141
+ // Signal updates automatically
142
+ console.log(titleSignal()); // "Meine Tolle App"
143
+ ```
144
+
145
+ ### Modular Localizations
146
+
147
+ `registerLocalization` automatically merges new keys and enums into existing language definitions if the language code matches. This allows different parts of your application or different modules to contribute translations to the same language incrementally.
148
+
149
+ ### Handling Missing Keys and Parameters
150
+
151
+ - **Missing Key**: If a localization key is missing for the active language, the service returns the key wrapped in double underscores (e.g., `__app.title__`) and logs a warning.
152
+ - **Missing Parameter**: If a parameter is missing in the data object but required by the template string, it is replaced by the parameter name wrapped in double underscores (e.g., `Hello, __name__!`).
153
+ - **Missing Observable Value**: When resolving an `Observable` based `DynamicText` using `resolveDynamicText`, the initial value before the first emission is `[MISSING LOCALIZATION KEY]`.
154
+
155
+ ## 🔧 Advanced Topics
156
+
157
+ ### Reactive Localization with Signals
158
+
159
+ The `resolveDynamicText` function is the primary tool for handling text in reactive applications. It accepts static values, Signals, or Observables.
160
+
161
+ ```typescript
162
+ import { signal } from '@tstdl/base/signals';
163
+ import { resolveDynamicText } from '@tstdl/base/text';
164
+ import { keys } from './localization-types';
165
+
166
+ // A signal that determines which key to show
167
+ const statusSignal = signal(keys.status.online);
168
+
169
+ // Result is a signal that updates if statusSignal changes OR language changes
170
+ const textSignal = resolveDynamicText(statusSignal);
171
+ ```
172
+
173
+ ### Parameterized Text
174
+
175
+ You can define parameters in your strings using `{{ paramName }}` syntax. Use `localizationData` to bind values to these parameters.
176
+
177
+ **Definition:**
178
+
179
+ ```typescript
180
+ type AppLocalization = Localization<{
181
+ messages: {
182
+ welcome: LocalizeItem<{ name: string }>;
183
+ };
184
+ }>;
185
+
186
+ const en: AppLocalization = {
187
+ // ...
188
+ keys: {
189
+ messages: {
190
+ welcome: 'Welcome, {{name}}!',
191
+ },
192
+ },
193
+ // ...
194
+ };
195
+ ```
196
+
197
+ **Usage:**
198
+
199
+ ```typescript
200
+ import { localizationData, resolveDynamicText } from '@tstdl/base/text';
201
+ import { keys } from './localization-types';
202
+
203
+ // Create a data object binding the key and parameters
204
+ const data = localizationData(keys.messages.welcome, { name: 'Alice' });
205
+
206
+ const text = resolveDynamicText(data);
207
+ console.log(text()); // "Welcome, Alice!"
208
+ ```
209
+
210
+ ### Localization Functions
211
+
212
+ For complex logic (like pluralization or conditional formatting), use a function instead of a string. Functions receive the parameters and a context object containing the `LocalizationService`.
213
+
214
+ ```typescript
215
+ const en: AppLocalization = {
216
+ // ...
217
+ keys: {
218
+ items: {
219
+ count: ({ count }, { localizationService }) => (count === 1 ? '1 Item' : `${count} Items`),
220
+ },
221
+ },
222
+ // ...
223
+ };
224
+
225
+ // Usage
226
+ const text = resolveDynamicText(localizationData(keys.items.count, { count: 5 }));
227
+ console.log(text()); // "5 Items"
228
+ ```
229
+
230
+ ### Localizing Enums
231
+
232
+ The module provides specific helpers for `enum` types to ensure type safety and ease of use.
233
+
234
+ ```typescript
235
+ import { defineEnum, type EnumType } from '@tstdl/base/enumeration';
236
+ import { enumerationLocalization, localizeEnum } from '@tstdl/base/text';
237
+
238
+ // 1. Define Enum
239
+ const Status = defineEnum('Status', {
240
+ Active: 'active',
241
+ Inactive: 'inactive',
242
+ });
243
+ type Status = EnumType<typeof Status>;
244
+
245
+ // 2. Add to Localization Type
246
+ type AppLocalization = Localization<
247
+ {
248
+ /* ... keys ... */
249
+ },
250
+ [typeof Status] // Add enum type here
251
+ >;
252
+
253
+ // 3. Define Translations
254
+ const en: AppLocalization = {
255
+ // ...
256
+ enums: [
257
+ enumerationLocalization(Status, 'Status', {
258
+ [Status.Active]: 'Active User',
259
+ [Status.Inactive]: 'Inactive User',
260
+ }),
261
+ ],
262
+ };
263
+
264
+ // 4. Usage
265
+ const statusText = localizationService.localizeEnum(Status, Status.Active);
266
+ console.log(statusText()); // "Active User"
267
+ ```
268
+
269
+ ### Resolving Nested Dynamic Text
270
+
271
+ When working with lists of objects where one property is a `DynamicText` (e.g., a navigation menu or a list of options), `resolveNestedDynamicTexts` helps transform the entire array efficiently.
272
+
273
+ ```typescript
274
+ import { resolveNestedDynamicTexts, type DynamicText } from '@tstdl/base/text';
275
+
276
+ type MenuItem = {
277
+ id: string;
278
+ label: DynamicText; // Can be a string, key, or localizationData
279
+ };
280
+
281
+ const menuItems: MenuItem[] = [
282
+ { id: 'home', label: keys.nav.home },
283
+ { id: 'settings', label: keys.nav.settings },
284
+ ];
285
+
286
+ // Returns a Signal<Array<{ id: string, label: string }>>
287
+ // The 'label' property is now the resolved string
288
+ const resolvedMenu = resolveNestedDynamicTexts(menuItems, 'label');
289
+ ```
290
+
291
+ ### Common Localizations
292
+
293
+ The module includes a set of common localizations (Yes, No, Ok, Cancel, etc.) that you can merge into your application to avoid repetition.
294
+
295
+ ```typescript
296
+ import { englishTstdlCommonLocalization, germanTstdlCommonLocalization } from '@tstdl/base/text';
297
+
298
+ localizationService.registerLocalization(englishTstdlCommonLocalization, germanTstdlCommonLocalization);
299
+ ```
300
+
301
+ ## 📚 API
302
+
303
+ ### LocalizationService
304
+
305
+ | Method | Description |
306
+ | :--------------------------------------- | :-------------------------------------------------------------------------- |
307
+ | `registerLocalization(...localizations)` | Registers one or more localization definitions. Merges if language exists. |
308
+ | `setLanguage(code)` | Sets the active language by code or `Language` object. |
309
+ | `getLanguage(code)` | Retrieves a registered `Language` by its code. |
310
+ | `hasLanguage(code)` | Checks if a language is registered. |
311
+ | `localize(data)` | Returns a `Signal<string>` for the given key/data. |
312
+ | `localize$(data)` | Returns an `Observable<string>` for the given key/data. |
313
+ | `localizeOnce(data)` | Returns a `string` (non-reactive) for the current language. |
314
+ | `localizeEnum(enum, value)` | Returns a `Signal<string>` for the enum value. |
315
+ | `localizeEnum$(enum, value)` | Returns an `Observable<string>` for the enum value. |
316
+ | `localizeEnumOnce(enum, value)` | Returns a `string` (non-reactive) for the enum value. |
317
+ | `activeLanguage` | Read-only signal of the currently active `Language`. |
318
+ | `availableLanguages` | Read-only signal of all registered `Language`s. |
319
+
320
+ ### Utilities
321
+
322
+ | Function | Description |
323
+ | :------------------------------------ | :-------------------------------------------------------------------------- |
324
+ | `getLocalizationKeys<T>()` | Returns a proxy object for type-safe access to localization keys. |
325
+ | `localizationData(key, params)` | Creates a typed object containing a key and its parameters. |
326
+ | `resolveDynamicText(text)` | Converts a `DynamicText` into a `Signal<string>`. |
327
+ | `resolveDynamicText$(text)` | Converts a `DynamicText` into an `Observable<string>`. |
328
+ | `resolveDynamicTexts(texts)` | Converts an array of `DynamicText` into a `Signal<string[]>`. |
329
+ | `resolveDynamicTexts$(texts)` | Converts an array of `DynamicText` into an `Observable<string[]>`. |
330
+ | `resolveNestedDynamicText(obj, key)` | Resolves a `DynamicText` property within an object (Signal). |
331
+ | `resolveNestedDynamicText$(obj, key)` | Resolves a `DynamicText` property within an object (Observable). |
332
+ | `resolveNestedDynamicTexts(arr, key)` | Resolves a `DynamicText` property within an array of objects (Signal). |
333
+ | `resolveNestedDynamicTexts$(arr, key)`| Resolves a `DynamicText` property within an array of objects (Observable). |
334
+ | `enumerationLocalization(...)` | Helper to define enum translations in a `Localization` object. |
335
+ | `autoEnumerationLocalization(enum)` | Generates default translations using enum key names as values. |
336
+
337
+ ### Types
338
+
339
+ | Type | Description |
340
+ | :--------------------- | :--------------------------------------------------------------------------------- |
341
+ | `Localization` | The structure for defining translations (language, keys, enums). |
342
+ | `LocalizeItem<Params>` | A translation value: either a string or a `LocalizeFunction`. |
343
+ | `LocalizeFunction<P>` | A function returning a string, receiving parameters and `LocalizeFunctionContext`. |
344
+ | `DynamicText` | `ReactiveValue<LocalizableText>`. The standard input type for localizable content. |
345
+ | `LocalizableText` | `string | LocalizationData`. |
346
+ | `LocalizationData` | Key, `LocalizationDataObject`, or `EnumLocalizationKey`. |
@@ -192,7 +192,7 @@ function buildMappedLocalization({ language, keys, enums }) {
192
192
  const mappedLocalization = {
193
193
  language,
194
194
  keys: new Map(deepObjectEntries(keys)),
195
- enums: new Map(enumsEntries)
195
+ enums: new Map(enumsEntries),
196
196
  };
197
197
  return mappedLocalization;
198
198
  }
@@ -215,7 +215,7 @@ function mergeMappedLocalization(a, b, force = false) {
215
215
  return {
216
216
  language: b.language,
217
217
  keys: new Map([...a.keys, ...b.keys]),
218
- enums: new Map([...a.enums, ...b.enums])
218
+ enums: new Map([...a.enums, ...b.enums]),
219
219
  };
220
220
  }
221
221
  export function isLocalizeItem(value) {