@nestjs-dash/translation 0.2.0

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 (54) hide show
  1. package/README.md +131 -0
  2. package/dist/constants.d.ts +3 -0
  3. package/dist/constants.js +3 -0
  4. package/dist/context/translation-context.middleware.d.ts +12 -0
  5. package/dist/context/translation-context.middleware.js +66 -0
  6. package/dist/context/translation-context.service.d.ts +17 -0
  7. package/dist/context/translation-context.service.js +52 -0
  8. package/dist/decorators/locale.decorator.d.ts +1 -0
  9. package/dist/decorators/locale.decorator.js +6 -0
  10. package/dist/decorators/response.decorator.d.ts +7 -0
  11. package/dist/decorators/response.decorator.js +7 -0
  12. package/dist/decorators/translatable-column.decorator.d.ts +3 -0
  13. package/dist/decorators/translatable-column.decorator.js +13 -0
  14. package/dist/decorators/translatable-property.decorator.d.ts +13 -0
  15. package/dist/decorators/translatable-property.decorator.js +20 -0
  16. package/dist/index.d.ts +22 -0
  17. package/dist/index.js +22 -0
  18. package/dist/interceptors/translation.interceptor.d.ts +12 -0
  19. package/dist/interceptors/translation.interceptor.js +43 -0
  20. package/dist/metadata/translation-metadata.storage.d.ts +4 -0
  21. package/dist/metadata/translation-metadata.storage.js +22 -0
  22. package/dist/migration/cli-args.d.ts +12 -0
  23. package/dist/migration/cli-args.js +38 -0
  24. package/dist/migration/cli.d.ts +2 -0
  25. package/dist/migration/cli.js +66 -0
  26. package/dist/migration/migration-dialect.adapter.d.ts +12 -0
  27. package/dist/migration/migration-dialect.adapter.js +1 -0
  28. package/dist/migration/migration-template.d.ts +1 -0
  29. package/dist/migration/migration-template.js +29 -0
  30. package/dist/migration/mysql-migration.adapter.d.ts +7 -0
  31. package/dist/migration/mysql-migration.adapter.js +17 -0
  32. package/dist/migration/postgres-migration.adapter.d.ts +7 -0
  33. package/dist/migration/postgres-migration.adapter.js +17 -0
  34. package/dist/migration/translatable-migration.generator.d.ts +14 -0
  35. package/dist/migration/translatable-migration.generator.js +173 -0
  36. package/dist/query/mysql-query.adapter.d.ts +6 -0
  37. package/dist/query/mysql-query.adapter.js +12 -0
  38. package/dist/query/postgres-query.adapter.d.ts +6 -0
  39. package/dist/query/postgres-query.adapter.js +12 -0
  40. package/dist/query/translation-query.adapter.d.ts +5 -0
  41. package/dist/query/translation-query.adapter.js +1 -0
  42. package/dist/query/translation-query.service.d.ts +15 -0
  43. package/dist/query/translation-query.service.js +69 -0
  44. package/dist/services/translation.service.d.ts +15 -0
  45. package/dist/services/translation.service.js +109 -0
  46. package/dist/testing/fixtures.d.ts +21 -0
  47. package/dist/testing/fixtures.js +53 -0
  48. package/dist/transformers/translation-response.transformer.d.ts +11 -0
  49. package/dist/transformers/translation-response.transformer.js +75 -0
  50. package/dist/translatable.module.d.ts +10 -0
  51. package/dist/translatable.module.js +108 -0
  52. package/dist/types.d.ts +100 -0
  53. package/dist/types.js +1 -0
  54. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @nestjs-dash/translation
2
+
3
+ Database-aware translatable JSON columns for NestJS and TypeORM, inspired by Laravel Translatable.
4
+ Stores every locale of a field in a single JSON/JSONB column (`{ "en": "...", "nl": "...", "fr": "..." }`)
5
+ instead of duplicating columns (`name_en`, `name_nl`) or maintaining separate translation tables, and
6
+ automatically resolves the right locale per request.
7
+
8
+ Ported from the standalone [`nestjs-translatable`](https://github.com/M0D1xD/nestjs-translatable)
9
+ package. This package has no dependency on the admin panel — pairing it with `@nestjs-dash/core`'s
10
+ `translatableInput()`/`translatableColumn()` form/table helpers (see
11
+ [Admin panel integration](#admin-panel-integration)) is optional.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add @nestjs-dash/translation
17
+ ```
18
+
19
+ Requires `@nestjs/common`, `@nestjs/core`, `reflect-metadata`, `rxjs`, and `typeorm` as peers.
20
+
21
+ ## Module setup
22
+
23
+ ```ts
24
+ import { Module } from '@nestjs/common';
25
+ import { TranslatableModule } from '@nestjs-dash/translation';
26
+
27
+ @Module({
28
+ imports: [
29
+ TranslatableModule.forRoot({
30
+ defaultLocale: 'en',
31
+ fallbackLocale: 'en',
32
+ supportedLocales: ['en', 'nl', 'fr'],
33
+ }),
34
+ ],
35
+ })
36
+ export class AppModule {}
37
+ ```
38
+
39
+ `forRoot()`/`forRootAsync()` register globally: a middleware resolves the request's locale
40
+ (`?lang=`, `x-language` header, `Accept-Language`, or a custom `localeResolvers` entry) and a global
41
+ interceptor localizes responses. Only call it once, in your root module.
42
+
43
+ ## Entities
44
+
45
+ ```ts
46
+ import { TranslatableColumn, TranslationMap } from '@nestjs-dash/translation';
47
+
48
+ @Entity('products')
49
+ export class ProductEntity {
50
+ @PrimaryGeneratedColumn('uuid')
51
+ id: string;
52
+
53
+ @TranslatableColumn('jsonb') // 'json' for MySQL/MariaDB, omit the type for MongoDB
54
+ details: TranslationMap;
55
+ }
56
+ ```
57
+
58
+ A `GET` request with `?lang=nl` turns `{ "en": "...", "nl": "..." }` into the plain string `"..."`
59
+ in the response automatically. Use `@RawTranslations()` on a route to get the untouched map instead,
60
+ `@TranslationsWithRaw()` for both, or `@SkipTranslation()` to disable localization for that route.
61
+
62
+ ## Writing translations
63
+
64
+ ```ts
65
+ constructor(private readonly translations: TranslationService) {}
66
+
67
+ async setDutch(id: string, value: string) {
68
+ const product = await this.products.findOneByOrFail({ id });
69
+ this.translations.set(product, 'details', 'nl', value);
70
+ return this.products.save(product);
71
+ }
72
+ ```
73
+
74
+ `TranslationService.set`/`setMany`/`remove` write one locale at a time without clobbering the
75
+ others, validate the locale against `supportedLocales`, and reject `.`/`$` (unsafe for MongoDB
76
+ embedded-document paths).
77
+
78
+ ## Sorting and filtering by translated value in SQL
79
+
80
+ ```ts
81
+ const query = this.products.createQueryBuilder('product');
82
+ this.translationQuery.orderBy(query, { alias: 'product', property: 'details' });
83
+ ```
84
+
85
+ `TranslationQueryService` picks the right adapter (Postgres `->>`/`COALESCE`, MySQL
86
+ `JSON_EXTRACT`/`COALESCE`) based on the query builder's connection, parameterizes locale values, and
87
+ validates `alias`/`property` as safe SQL identifiers before building the expression.
88
+
89
+ ## Migrating existing scalar columns
90
+
91
+ ```bash
92
+ npx nestjs-dash-translation migration:generate \
93
+ --data-source ./dist/data-source.js \
94
+ --locales en,nl,fr \
95
+ --default-locale en \
96
+ --output ./migrations
97
+ ```
98
+
99
+ Backfills each existing scalar value into the JSON column under one locale key before converting
100
+ the column type — see `generateTranslatableMigrations`/`planTranslatableMigrations` for the
101
+ programmatic API.
102
+
103
+ ## Admin panel integration
104
+
105
+ `@nestjs-dash/core` ships two generic helpers for building resource forms/tables around any
106
+ locale-keyed JSON column (`translatable-field.ts`) — not specific to this package, but exactly what
107
+ a `@TranslatableColumn()` value needs:
108
+
109
+ ```ts
110
+ import { translatableColumn, translatableInput } from '@nestjs-dash/core';
111
+
112
+ static override form(form: Schema) {
113
+ return form.components([
114
+ translatableInput('details', ['en', 'nl', 'fr']),
115
+ ]);
116
+ }
117
+
118
+ static override table(table: Table) {
119
+ return table.columns([
120
+ translatableColumn('details', 'en'), // shows the fixed 'en' value in the list view
121
+ ]);
122
+ }
123
+ ```
124
+
125
+ `translatableInput` renders one tab per locale (each a `TextInput.make(name).json(locale)` under
126
+ the hood — see `.json(property)` on `TextInput`/`Textarea`), so editing any subset of locales and
127
+ saving never clobbers the others. `translatableColumn` projects one fixed, author-chosen locale
128
+ into a list column — there's no per-request "current admin locale" concept, so pick the locale a
129
+ list view should always display.
130
+
131
+ See `examples/translation` for a runnable end-to-end demo against both Postgres and MySQL.
@@ -0,0 +1,3 @@
1
+ export declare const TRANSLATABLE_MODULE_OPTIONS: unique symbol;
2
+ export declare const TRANSLATABLE_COLUMNS_METADATA: unique symbol;
3
+ export declare const TRANSLATION_ROUTE_OPTIONS: unique symbol;
@@ -0,0 +1,3 @@
1
+ export const TRANSLATABLE_MODULE_OPTIONS = Symbol('TRANSLATABLE_MODULE_OPTIONS');
2
+ export const TRANSLATABLE_COLUMNS_METADATA = Symbol('TRANSLATABLE_COLUMNS_METADATA');
3
+ export const TRANSLATION_ROUTE_OPTIONS = Symbol('TRANSLATION_ROUTE_OPTIONS');
@@ -0,0 +1,12 @@
1
+ import { type NestMiddleware } from '@nestjs/common';
2
+ import type { NextFunction, Request, Response } from 'express';
3
+ import type { TranslatableModuleOptions } from '../types.js';
4
+ import { TranslationContextService } from './translation-context.service.js';
5
+ export declare class TranslationContextMiddleware implements NestMiddleware {
6
+ private readonly options;
7
+ private readonly context;
8
+ constructor(options: TranslatableModuleOptions, context: TranslationContextService);
9
+ use(request: Request, _response: Response, next: NextFunction): void;
10
+ private resolveLocale;
11
+ private normalize;
12
+ }
@@ -0,0 +1,66 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable } from '@nestjs/common';
14
+ import { TRANSLATABLE_MODULE_OPTIONS } from '../constants.js';
15
+ import { TranslationContextService } from './translation-context.service.js';
16
+ let TranslationContextMiddleware = class TranslationContextMiddleware {
17
+ constructor(options, context) {
18
+ this.options = options;
19
+ this.context = context;
20
+ }
21
+ use(request, _response, next) {
22
+ const locale = this.resolveLocale(request);
23
+ Object.assign(request, { translationContext: this.context });
24
+ this.context.run({
25
+ locale,
26
+ fallbackLocale: this.options.fallbackLocale ?? this.options.defaultLocale,
27
+ responseMode: this.options.responseMode ?? 'localized',
28
+ }, next);
29
+ }
30
+ resolveLocale(request) {
31
+ const custom = this.options.localeResolvers
32
+ ?.map((resolver) => resolver.resolve({
33
+ query: request.query,
34
+ headers: request.headers,
35
+ }))
36
+ .find(Boolean);
37
+ const queryName = this.options.queryParameter ?? 'lang';
38
+ const headerName = (this.options.headerName ?? 'x-language').toLowerCase();
39
+ const queryLocale = typeof request.query[queryName] === 'string' ? request.query[queryName] : undefined;
40
+ const headerValue = request.headers[headerName];
41
+ const headerLocale = typeof headerValue === 'string' ? headerValue : undefined;
42
+ const accepted = request.acceptsLanguages()?.[0];
43
+ const acceptedLocale = accepted && accepted !== '*' ? accepted : undefined;
44
+ const candidate = custom ?? queryLocale ?? headerLocale ?? acceptedLocale;
45
+ return this.normalize(candidate);
46
+ }
47
+ normalize(candidate) {
48
+ if (!candidate)
49
+ return this.options.defaultLocale;
50
+ const exact = this.options.supportedLocales.find((locale) => locale.toLowerCase() === candidate.toLowerCase());
51
+ if (exact)
52
+ return exact;
53
+ const base = candidate.split('-')[0];
54
+ const baseMatch = this.options.supportedLocales.find((locale) => locale.toLowerCase() === base?.toLowerCase());
55
+ if (baseMatch)
56
+ return baseMatch;
57
+ return this.options.strictLocales === false ? candidate : this.options.defaultLocale;
58
+ }
59
+ };
60
+ TranslationContextMiddleware = __decorate([
61
+ Injectable(),
62
+ __param(0, Inject(TRANSLATABLE_MODULE_OPTIONS)),
63
+ __param(1, Inject(TranslationContextService)),
64
+ __metadata("design:paramtypes", [Object, TranslationContextService])
65
+ ], TranslationContextMiddleware);
66
+ export { TranslationContextMiddleware };
@@ -0,0 +1,17 @@
1
+ import type { TranslatableModuleOptions, TranslationRequestContext, TranslationResponseMode } from '../types.js';
2
+ /**
3
+ * Fallback for code paths that need a default locale before any
4
+ * TranslationContextService instance is reachable (e.g. a request that
5
+ * never passed through TranslationContextMiddleware). Reflects the
6
+ * defaultLocale of the most recently bootstrapped TranslatableModule.
7
+ */
8
+ export declare function getConfiguredDefaultLocale(): string;
9
+ export declare class TranslationContextService {
10
+ private readonly options;
11
+ private readonly storage;
12
+ constructor(options: TranslatableModuleOptions);
13
+ run<T>(context: TranslationRequestContext, callback: () => T): T;
14
+ get locale(): string;
15
+ get fallbackLocale(): string;
16
+ get responseMode(): TranslationResponseMode;
17
+ }
@@ -0,0 +1,52 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable } from '@nestjs/common';
14
+ import { AsyncLocalStorage } from 'node:async_hooks';
15
+ import { TRANSLATABLE_MODULE_OPTIONS } from '../constants.js';
16
+ let lastConfiguredDefaultLocale;
17
+ /**
18
+ * Fallback for code paths that need a default locale before any
19
+ * TranslationContextService instance is reachable (e.g. a request that
20
+ * never passed through TranslationContextMiddleware). Reflects the
21
+ * defaultLocale of the most recently bootstrapped TranslatableModule.
22
+ */
23
+ export function getConfiguredDefaultLocale() {
24
+ return lastConfiguredDefaultLocale ?? 'en';
25
+ }
26
+ let TranslationContextService = class TranslationContextService {
27
+ constructor(options) {
28
+ this.options = options;
29
+ this.storage = new AsyncLocalStorage();
30
+ lastConfiguredDefaultLocale = options.defaultLocale;
31
+ }
32
+ run(context, callback) {
33
+ return this.storage.run(context, callback);
34
+ }
35
+ get locale() {
36
+ return this.storage.getStore()?.locale ?? this.options.defaultLocale;
37
+ }
38
+ get fallbackLocale() {
39
+ return (this.storage.getStore()?.fallbackLocale ??
40
+ this.options.fallbackLocale ??
41
+ this.options.defaultLocale);
42
+ }
43
+ get responseMode() {
44
+ return this.storage.getStore()?.responseMode ?? this.options.responseMode ?? 'localized';
45
+ }
46
+ };
47
+ TranslationContextService = __decorate([
48
+ Injectable(),
49
+ __param(0, Inject(TRANSLATABLE_MODULE_OPTIONS)),
50
+ __metadata("design:paramtypes", [Object])
51
+ ], TranslationContextService);
52
+ export { TranslationContextService };
@@ -0,0 +1 @@
1
+ export declare const Locale: (...dataOrPipes: unknown[]) => ParameterDecorator;
@@ -0,0 +1,6 @@
1
+ import { createParamDecorator } from '@nestjs/common';
2
+ import { getConfiguredDefaultLocale, } from '../context/translation-context.service.js';
3
+ export const Locale = createParamDecorator((_data, context) => {
4
+ const request = context.switchToHttp().getRequest();
5
+ return request.translationContext?.locale ?? getConfiguredDefaultLocale();
6
+ });
@@ -0,0 +1,7 @@
1
+ import { TRANSLATION_ROUTE_OPTIONS } from '../constants.js';
2
+ import type { TranslationRouteOptions } from '../types.js';
3
+ export declare const TranslationResponse: (options: TranslationRouteOptions) => import("@nestjs/common").CustomDecorator<typeof TRANSLATION_ROUTE_OPTIONS>;
4
+ export declare const SkipTranslation: () => import("@nestjs/common").CustomDecorator<typeof TRANSLATION_ROUTE_OPTIONS>;
5
+ export declare const RawTranslations: () => import("@nestjs/common").CustomDecorator<typeof TRANSLATION_ROUTE_OPTIONS>;
6
+ export declare const LocalizedTranslations: () => import("@nestjs/common").CustomDecorator<typeof TRANSLATION_ROUTE_OPTIONS>;
7
+ export declare const TranslationsWithRaw: () => import("@nestjs/common").CustomDecorator<typeof TRANSLATION_ROUTE_OPTIONS>;
@@ -0,0 +1,7 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+ import { TRANSLATION_ROUTE_OPTIONS } from '../constants.js';
3
+ export const TranslationResponse = (options) => SetMetadata(TRANSLATION_ROUTE_OPTIONS, options);
4
+ export const SkipTranslation = () => TranslationResponse({ skip: true });
5
+ export const RawTranslations = () => TranslationResponse({ mode: 'raw' });
6
+ export const LocalizedTranslations = () => TranslationResponse({ mode: 'localized' });
7
+ export const TranslationsWithRaw = () => TranslationResponse({ mode: 'both' });
@@ -0,0 +1,3 @@
1
+ import type { TranslatableColumnOptions, TranslatableStorageType } from '../types.js';
2
+ export declare function TranslatableColumn(type: TranslatableStorageType): PropertyDecorator;
3
+ export declare function TranslatableColumn(options?: TranslatableColumnOptions): PropertyDecorator;
@@ -0,0 +1,13 @@
1
+ import { Column } from 'typeorm';
2
+ import { registerTranslatableColumn } from '../metadata/translation-metadata.storage.js';
3
+ export function TranslatableColumn(input = {}) {
4
+ const options = typeof input === 'string' ? { column: { type: input } } : input;
5
+ return (target, propertyKey) => {
6
+ const columnDecorator = options.column ? Column(options.column) : Column();
7
+ columnDecorator(target, propertyKey);
8
+ registerTranslatableColumn(target.constructor, {
9
+ propertyKey,
10
+ ...options.translation,
11
+ });
12
+ };
13
+ }
@@ -0,0 +1,13 @@
1
+ import type { TranslationBehaviorOptions } from '../types.js';
2
+ /**
3
+ * Marks a property as translatable without composing TypeORM's `@Column()`.
4
+ *
5
+ * `@TranslatableColumn()` is for entity classes backed by a real database
6
+ * column. Use `@TranslatableProperty()` on plain DTO/response classes —
7
+ * e.g. a `nestjs-paginate` Swagger response type, or anything produced by
8
+ * `class-transformer`'s `plainToInstance()` — that carry a `TranslationMap`
9
+ * value but aren't the original entity, so `TranslationResponseTransformer`
10
+ * still recognizes and localizes them even though the entity's own
11
+ * `@TranslatableColumn()` metadata doesn't apply to that class.
12
+ */
13
+ export declare function TranslatableProperty(options?: TranslationBehaviorOptions): PropertyDecorator;
@@ -0,0 +1,20 @@
1
+ import { registerTranslatableColumn } from '../metadata/translation-metadata.storage.js';
2
+ /**
3
+ * Marks a property as translatable without composing TypeORM's `@Column()`.
4
+ *
5
+ * `@TranslatableColumn()` is for entity classes backed by a real database
6
+ * column. Use `@TranslatableProperty()` on plain DTO/response classes —
7
+ * e.g. a `nestjs-paginate` Swagger response type, or anything produced by
8
+ * `class-transformer`'s `plainToInstance()` — that carry a `TranslationMap`
9
+ * value but aren't the original entity, so `TranslationResponseTransformer`
10
+ * still recognizes and localizes them even though the entity's own
11
+ * `@TranslatableColumn()` metadata doesn't apply to that class.
12
+ */
13
+ export function TranslatableProperty(options = {}) {
14
+ return (target, propertyKey) => {
15
+ registerTranslatableColumn(target.constructor, {
16
+ propertyKey,
17
+ ...options,
18
+ });
19
+ };
20
+ }
@@ -0,0 +1,22 @@
1
+ export * from './constants.js';
2
+ export * from './context/translation-context.service.js';
3
+ export * from './decorators/locale.decorator.js';
4
+ export * from './decorators/response.decorator.js';
5
+ export * from './decorators/translatable-column.decorator.js';
6
+ export * from './decorators/translatable-property.decorator.js';
7
+ export * from './interceptors/translation.interceptor.js';
8
+ export * from './metadata/translation-metadata.storage.js';
9
+ export * from './migration/cli-args.js';
10
+ export * from './migration/migration-dialect.adapter.js';
11
+ export * from './migration/migration-template.js';
12
+ export * from './migration/mysql-migration.adapter.js';
13
+ export * from './migration/postgres-migration.adapter.js';
14
+ export * from './migration/translatable-migration.generator.js';
15
+ export * from './query/mysql-query.adapter.js';
16
+ export * from './query/postgres-query.adapter.js';
17
+ export * from './query/translation-query.adapter.js';
18
+ export * from './query/translation-query.service.js';
19
+ export * from './services/translation.service.js';
20
+ export * from './transformers/translation-response.transformer.js';
21
+ export * from './translatable.module.js';
22
+ export * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ export * from './constants.js';
2
+ export * from './context/translation-context.service.js';
3
+ export * from './decorators/locale.decorator.js';
4
+ export * from './decorators/response.decorator.js';
5
+ export * from './decorators/translatable-column.decorator.js';
6
+ export * from './decorators/translatable-property.decorator.js';
7
+ export * from './interceptors/translation.interceptor.js';
8
+ export * from './metadata/translation-metadata.storage.js';
9
+ export * from './migration/cli-args.js';
10
+ export * from './migration/migration-dialect.adapter.js';
11
+ export * from './migration/migration-template.js';
12
+ export * from './migration/mysql-migration.adapter.js';
13
+ export * from './migration/postgres-migration.adapter.js';
14
+ export * from './migration/translatable-migration.generator.js';
15
+ export * from './query/mysql-query.adapter.js';
16
+ export * from './query/postgres-query.adapter.js';
17
+ export * from './query/translation-query.adapter.js';
18
+ export * from './query/translation-query.service.js';
19
+ export * from './services/translation.service.js';
20
+ export * from './transformers/translation-response.transformer.js';
21
+ export * from './translatable.module.js';
22
+ export * from './types.js';
@@ -0,0 +1,12 @@
1
+ import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { type Observable } from 'rxjs';
4
+ import { TranslationContextService } from '../context/translation-context.service.js';
5
+ import { TranslationResponseTransformer } from '../transformers/translation-response.transformer.js';
6
+ export declare class TranslationInterceptor implements NestInterceptor {
7
+ private readonly reflector;
8
+ private readonly transformer;
9
+ private readonly context;
10
+ constructor(reflector: Reflector, transformer: TranslationResponseTransformer, context: TranslationContextService);
11
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
12
+ }
@@ -0,0 +1,43 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable, } from '@nestjs/common';
14
+ import { Reflector } from '@nestjs/core';
15
+ import { map } from 'rxjs';
16
+ import { TRANSLATION_ROUTE_OPTIONS } from '../constants.js';
17
+ import { TranslationContextService } from '../context/translation-context.service.js';
18
+ import { TranslationResponseTransformer } from '../transformers/translation-response.transformer.js';
19
+ let TranslationInterceptor = class TranslationInterceptor {
20
+ constructor(reflector, transformer, context) {
21
+ this.reflector = reflector;
22
+ this.transformer = transformer;
23
+ this.context = context;
24
+ }
25
+ intercept(context, next) {
26
+ const route = this.reflector.getAllAndOverride(TRANSLATION_ROUTE_OPTIONS, [context.getHandler(), context.getClass()]);
27
+ if (route?.skip)
28
+ return next.handle();
29
+ return next
30
+ .handle()
31
+ .pipe(map((value) => this.transformer.transform(value, route?.mode ?? this.context.responseMode, route?.locale ?? this.context.locale)));
32
+ }
33
+ };
34
+ TranslationInterceptor = __decorate([
35
+ Injectable(),
36
+ __param(0, Inject(Reflector)),
37
+ __param(1, Inject(TranslationResponseTransformer)),
38
+ __param(2, Inject(TranslationContextService)),
39
+ __metadata("design:paramtypes", [Reflector,
40
+ TranslationResponseTransformer,
41
+ TranslationContextService])
42
+ ], TranslationInterceptor);
43
+ export { TranslationInterceptor };
@@ -0,0 +1,4 @@
1
+ import type { TranslatableColumnMetadata } from '../types.js';
2
+ export declare function registerTranslatableColumn(target: object, metadata: TranslatableColumnMetadata): void;
3
+ export declare function getTranslatableColumns(target: object): TranslatableColumnMetadata[];
4
+ export declare function isTranslatableEntity(target: object): boolean;
@@ -0,0 +1,22 @@
1
+ const storage = new WeakMap();
2
+ export function registerTranslatableColumn(target, metadata) {
3
+ const fields = storage.get(target) ?? new Map();
4
+ fields.set(metadata.propertyKey, metadata);
5
+ storage.set(target, fields);
6
+ }
7
+ export function getTranslatableColumns(target) {
8
+ const merged = new Map();
9
+ let current = target;
10
+ while (current && current !== Function.prototype) {
11
+ const fields = storage.get(current);
12
+ fields?.forEach((value, key) => {
13
+ if (!merged.has(key))
14
+ merged.set(key, value);
15
+ });
16
+ current = Object.getPrototypeOf(current);
17
+ }
18
+ return [...merged.values()];
19
+ }
20
+ export function isTranslatableEntity(target) {
21
+ return getTranslatableColumns(target).length > 0;
22
+ }
@@ -0,0 +1,12 @@
1
+ export declare class CliUsageError extends Error {
2
+ }
3
+ export interface ParsedCliArgs {
4
+ command: string;
5
+ dataSourcePath: string;
6
+ supportedLocales: string[];
7
+ defaultLocale: string;
8
+ outputDir: string;
9
+ name: string;
10
+ dryRun: boolean;
11
+ }
12
+ export declare function parseCliArgs(argv: string[]): ParsedCliArgs;
@@ -0,0 +1,38 @@
1
+ import { parseArgs } from 'node:util';
2
+ export class CliUsageError extends Error {
3
+ }
4
+ export function parseCliArgs(argv) {
5
+ const [command, ...rest] = argv;
6
+ if (command !== 'migration:generate') {
7
+ throw new CliUsageError(`Unknown command "${command ?? ''}". Expected "migration:generate".`);
8
+ }
9
+ const { values } = parseArgs({
10
+ args: rest,
11
+ options: {
12
+ 'data-source': { type: 'string', short: 'd' },
13
+ locales: { type: 'string', short: 'l' },
14
+ 'default-locale': { type: 'string' },
15
+ output: { type: 'string', short: 'o', default: './migrations' },
16
+ name: { type: 'string', short: 'n', default: 'TranslatableColumns' },
17
+ 'dry-run': { type: 'boolean', default: false },
18
+ },
19
+ });
20
+ if (!values['data-source'])
21
+ throw new CliUsageError('--data-source is required.');
22
+ if (!values.locales)
23
+ throw new CliUsageError('--locales is required.');
24
+ if (!values['default-locale'])
25
+ throw new CliUsageError('--default-locale is required.');
26
+ return {
27
+ command,
28
+ dataSourcePath: values['data-source'],
29
+ supportedLocales: values.locales
30
+ .split(',')
31
+ .map((locale) => locale.trim())
32
+ .filter(Boolean),
33
+ defaultLocale: values['default-locale'],
34
+ outputDir: values.output,
35
+ name: values.name,
36
+ dryRun: Boolean(values['dry-run']),
37
+ };
38
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import * as path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { DataSource } from 'typeorm';
5
+ import { CliUsageError, parseCliArgs } from './cli-args.js';
6
+ import { generateTranslatableMigrations } from './translatable-migration.generator.js';
7
+ const USAGE = `
8
+ Usage: nestjs-dash-translation migration:generate --data-source <path> --locales <csv> --default-locale <locale> [options]
9
+
10
+ Options:
11
+ -d, --data-source <path> Path to a compiled module exporting a TypeORM DataSource (required)
12
+ -l, --locales <csv> Comma-separated list of supported locales (required)
13
+ --default-locale <code> Default locale used to backfill existing scalar values (required)
14
+ -o, --output <dir> Output directory for the migration file (default: ./migrations)
15
+ -n, --name <name> Base name for the migration class/file (default: TranslatableColumns)
16
+ --dry-run Compute the plan without writing a file
17
+ `;
18
+ async function main() {
19
+ const args = parseCliArgs(process.argv.slice(2));
20
+ const dataSource = await loadDataSource(args.dataSourcePath);
21
+ const plan = await generateTranslatableMigrations({
22
+ dataSource,
23
+ supportedLocales: args.supportedLocales,
24
+ defaultLocale: args.defaultLocale,
25
+ outputDir: args.outputDir,
26
+ name: args.name,
27
+ dryRun: args.dryRun,
28
+ });
29
+ for (const column of plan.columns) {
30
+ const detail = column.reason
31
+ ? ` (${column.reason})`
32
+ : column.sourceLocale
33
+ ? ` (source locale: ${column.sourceLocale})`
34
+ : '';
35
+ console.log(` ${column.action.padEnd(7)} ${column.table}.${column.column}${detail}`);
36
+ }
37
+ if (plan.upStatements.length === 0) {
38
+ console.log('Nothing to migrate.');
39
+ return;
40
+ }
41
+ console.log(plan.written
42
+ ? `Migration written to ${plan.filePath}`
43
+ : `Dry run: would write ${plan.fileName}`);
44
+ }
45
+ async function loadDataSource(modulePath) {
46
+ const resolved = path.resolve(process.cwd(), modulePath);
47
+ const loaded = (await import(pathToFileURL(resolved).href));
48
+ const candidate = loaded.default ??
49
+ loaded.dataSource ??
50
+ Object.values(loaded).find((value) => value instanceof DataSource);
51
+ if (!(candidate instanceof DataSource)) {
52
+ throw new Error(`Could not find a DataSource export in "${modulePath}". Export it as ` +
53
+ '`export default dataSource` or `export const dataSource = ...`.');
54
+ }
55
+ return candidate;
56
+ }
57
+ main().catch((error) => {
58
+ if (error instanceof CliUsageError) {
59
+ console.error(error.message);
60
+ console.error(USAGE);
61
+ }
62
+ else {
63
+ console.error(error instanceof Error ? error.message : error);
64
+ }
65
+ process.exitCode = 1;
66
+ });
@@ -0,0 +1,12 @@
1
+ export interface MigrationDialectAdapter {
2
+ readonly driver: string;
3
+ buildJsonExpression(columnRef: string, locale: string): string;
4
+ extractJsonExpression(columnRef: string, locale: string): string;
5
+ /**
6
+ * Statement that enforces NOT NULL on an already-populated column. Must be
7
+ * safe to run after the column has been backfilled — the two dialects use
8
+ * incompatible syntax (Postgres: `ALTER COLUMN ... SET NOT NULL`, MySQL:
9
+ * `MODIFY COLUMN ... <type> NOT NULL`).
10
+ */
11
+ setNotNullExpression(qualifiedTable: string, columnRef: string, columnType: string): string;
12
+ }
@@ -0,0 +1 @@
1
+ export {};