@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.
- package/README.md +166 -0
- package/ai/genkit/multi-region.plugin.js +5 -3
- package/ai/genkit/tests/multi-region.test.d.ts +1 -0
- package/ai/genkit/tests/multi-region.test.js +5 -2
- package/ai/parser/parser.js +2 -2
- package/ai/prompts/build.js +1 -0
- package/ai/prompts/instructions-formatter.d.ts +15 -2
- package/ai/prompts/instructions-formatter.js +36 -31
- package/ai/prompts/prompt-builder.js +5 -5
- package/ai/prompts/steering.d.ts +3 -2
- package/ai/prompts/steering.js +3 -1
- package/ai/tests/instructions-formatter.test.js +1 -0
- package/api/README.md +403 -0
- package/api/client/client.js +7 -13
- package/api/client/tests/api-client.test.js +10 -10
- package/api/default-error-handlers.js +1 -1
- package/api/response.d.ts +2 -2
- package/api/response.js +22 -33
- package/api/server/api-controller.d.ts +1 -1
- package/api/server/api-controller.js +3 -3
- package/api/server/api-request-token.provider.d.ts +1 -0
- package/api/server/api-request-token.provider.js +1 -0
- package/api/server/middlewares/allowed-methods.middleware.js +2 -1
- package/api/server/middlewares/content-type.middleware.js +2 -1
- package/api/types.d.ts +3 -2
- package/application/README.md +240 -0
- package/application/application.d.ts +1 -1
- package/application/application.js +3 -3
- package/application/providers.d.ts +20 -2
- package/application/providers.js +34 -7
- package/audit/README.md +267 -0
- package/audit/module.d.ts +5 -0
- package/audit/module.js +9 -1
- package/authentication/README.md +288 -0
- package/authentication/client/authentication.service.d.ts +12 -11
- package/authentication/client/authentication.service.js +21 -21
- package/authentication/client/http-client.middleware.js +2 -2
- package/authentication/server/module.d.ts +5 -0
- package/authentication/server/module.js +9 -1
- package/authentication/tests/authentication.api-controller.test.js +1 -1
- package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
- package/authentication/tests/authentication.client-error-handling.test.js +2 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
- package/authentication/tests/authentication.client-service.test.js +1 -1
- package/browser/README.md +401 -0
- package/cancellation/README.md +156 -0
- package/cancellation/tests/coverage.test.d.ts +1 -0
- package/cancellation/tests/coverage.test.js +49 -0
- package/cancellation/tests/leak.test.js +24 -29
- package/cancellation/tests/token.test.d.ts +1 -0
- package/cancellation/tests/token.test.js +136 -0
- package/cancellation/token.d.ts +53 -177
- package/cancellation/token.js +132 -208
- package/circuit-breaker/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- package/context/README.md +174 -0
- package/cookie/README.md +161 -0
- package/css/README.md +157 -0
- package/data-structures/README.md +320 -0
- package/decorators/README.md +140 -0
- package/distributed-loop/README.md +231 -0
- package/distributed-loop/distributed-loop.js +1 -1
- package/document-management/README.md +403 -0
- package/document-management/server/configure.js +5 -1
- package/document-management/server/module.d.ts +1 -1
- package/document-management/server/module.js +1 -1
- package/document-management/server/services/document-management-ancillary.service.js +1 -1
- package/document-management/server/services/document-management.service.js +9 -7
- package/document-management/tests/ai-config-hierarchy.test.js +0 -5
- package/document-management/tests/document-management-ai-overrides.test.js +0 -1
- package/document-management/tests/document-management-core.test.js +2 -7
- package/document-management/tests/document-management.api.test.js +6 -7
- package/document-management/tests/document-statistics.service.test.js +11 -12
- package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
- package/document-management/tests/document.service.test.js +3 -3
- package/document-management/tests/enum-helpers.test.js +2 -3
- package/dom/README.md +213 -0
- package/enumerable/README.md +259 -0
- package/enumeration/README.md +121 -0
- package/errors/README.md +267 -0
- package/examples/document-management/main.d.ts +1 -0
- package/examples/document-management/main.js +14 -11
- package/file/README.md +191 -0
- package/formats/README.md +210 -0
- package/function/README.md +144 -0
- package/http/README.md +318 -0
- package/http/client/adapters/undici.adapter.js +1 -1
- package/http/client/http-client-request.d.ts +6 -5
- package/http/client/http-client-request.js +8 -9
- package/http/server/node/node-http-server.js +1 -2
- package/image-service/README.md +137 -0
- package/injector/README.md +491 -0
- package/intl/README.md +113 -0
- package/json-path/README.md +182 -0
- package/jsx/README.md +154 -0
- package/key-value-store/README.md +191 -0
- package/key-value-store/postgres/module.d.ts +1 -0
- package/key-value-store/postgres/module.js +5 -1
- package/lock/README.md +249 -0
- package/lock/postgres/module.d.ts +1 -0
- package/lock/postgres/module.js +5 -1
- package/lock/web/web-lock.js +119 -47
- package/logger/README.md +287 -0
- package/mail/README.md +256 -0
- package/mail/module.d.ts +5 -1
- package/mail/module.js +11 -6
- package/memory/README.md +144 -0
- package/message-bus/README.md +244 -0
- package/message-bus/message-bus-base.js +1 -1
- package/module/README.md +182 -0
- package/module/module.d.ts +1 -1
- package/module/module.js +77 -17
- package/module/modules/web-server.module.js +3 -4
- package/notification/server/module.d.ts +1 -0
- package/notification/server/module.js +5 -1
- package/notification/tests/notification-flow.test.js +2 -2
- package/notification/tests/notification-type.service.test.js +24 -15
- package/object-storage/README.md +300 -0
- package/openid-connect/README.md +274 -0
- package/orm/README.md +423 -0
- package/orm/decorators.d.ts +5 -1
- package/orm/decorators.js +1 -1
- package/orm/server/drizzle/schema-converter.js +17 -30
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/index.d.ts +1 -6
- package/orm/server/index.js +1 -6
- package/orm/server/migration.d.ts +19 -0
- package/orm/server/migration.js +72 -0
- package/orm/server/repository.d.ts +1 -1
- package/orm/server/transaction.d.ts +5 -10
- package/orm/server/transaction.js +22 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-migration.test.d.ts +1 -0
- package/orm/tests/database-migration.test.js +82 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +9 -6
- package/password/README.md +164 -0
- package/pdf/README.md +246 -0
- package/polyfills.js +1 -0
- package/pool/README.md +198 -0
- package/process/README.md +237 -0
- package/promise/README.md +252 -0
- package/promise/cancelable-promise.js +1 -1
- package/random/README.md +193 -0
- package/rate-limit/postgres/module.d.ts +1 -0
- package/rate-limit/postgres/module.js +5 -1
- package/reflection/README.md +305 -0
- package/reflection/decorator-data.js +11 -12
- package/rpc/README.md +386 -0
- package/rxjs-utils/README.md +262 -0
- package/schema/README.md +342 -0
- package/serializer/README.md +342 -0
- package/signals/implementation/README.md +134 -0
- package/sse/README.md +278 -0
- package/task-queue/README.md +293 -0
- package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.d.ts +1 -0
- package/task-queue/postgres/module.js +5 -1
- package/task-queue/postgres/schemas.d.ts +9 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +4 -13
- package/task-queue/postgres/task-queue.js +462 -355
- package/task-queue/postgres/task.model.d.ts +12 -5
- package/task-queue/postgres/task.model.js +51 -25
- package/task-queue/task-context.d.ts +2 -2
- package/task-queue/task-context.js +8 -8
- package/task-queue/task-queue.d.ts +53 -19
- package/task-queue/task-queue.js +121 -55
- package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
- package/task-queue/tests/cascading-cancellations.test.js +38 -0
- package/task-queue/tests/complex.test.js +45 -229
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +407 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +144 -0
- package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
- package/task-queue/tests/dag-dependencies.test.js +41 -0
- package/task-queue/tests/dependencies.test.js +28 -26
- package/task-queue/tests/extensive-dependencies.test.js +64 -139
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +53 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +61 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
- package/task-queue/tests/queue.test.js +128 -8
- package/task-queue/tests/worker.test.js +39 -16
- package/task-queue/tests/zombie-parent.test.d.ts +1 -0
- package/task-queue/tests/zombie-parent.test.js +45 -0
- package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
- package/task-queue/tests/zombie-recovery.test.js +51 -0
- package/templates/README.md +287 -0
- package/test5.js +5 -5
- package/testing/README.md +157 -0
- package/testing/integration-setup.d.ts +4 -4
- package/testing/integration-setup.js +54 -29
- package/text/README.md +346 -0
- package/text/localization.service.js +2 -2
- package/threading/README.md +238 -0
- package/types/README.md +311 -0
- package/utils/README.md +322 -0
- package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
- package/utils/async-iterable-helpers/observable-iterable.js +4 -8
- package/utils/async-iterable-helpers/take-until.js +4 -4
- package/utils/backoff.js +89 -30
- package/utils/file-reader.js +1 -2
- package/utils/retry-with-backoff.js +1 -1
- package/utils/timer.d.ts +1 -1
- package/utils/timer.js +5 -7
- package/utils/timing.d.ts +1 -1
- package/utils/timing.js +2 -4
- package/utils/z-base32.d.ts +1 -0
- 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
|
|
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
|
|
7
|
-
import { configurePostgresCircuitBreaker
|
|
8
|
-
import { configureDocumentManagement
|
|
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
|
|
15
|
-
import { configurePostgresLock
|
|
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
|
|
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 {
|
|
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
|
|
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?.
|
|
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?.
|
|
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
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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) {
|