@orthacms/i18n-server 0.0.0-reserve.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +13 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +15 -0
  6. package/dist/lib/content/controllers/get-entry-locales.controller.d.ts +15 -0
  7. package/dist/lib/content/controllers/get-entry-locales.controller.d.ts.map +1 -0
  8. package/dist/lib/content/controllers/get-entry-locales.controller.js +45 -0
  9. package/dist/lib/content/controllers/locale-summary.controller.d.ts +30 -0
  10. package/dist/lib/content/controllers/locale-summary.controller.d.ts.map +1 -0
  11. package/dist/lib/content/controllers/locale-summary.controller.js +61 -0
  12. package/dist/lib/content/controllers/resolve-type.d.ts +9 -0
  13. package/dist/lib/content/controllers/resolve-type.d.ts.map +1 -0
  14. package/dist/lib/content/controllers/resolve-type.js +20 -0
  15. package/dist/lib/content/dto/locale-summary.dto.d.ts +10 -0
  16. package/dist/lib/content/dto/locale-summary.dto.d.ts.map +1 -0
  17. package/dist/lib/content/dto/locale-summary.dto.js +30 -0
  18. package/dist/lib/content/locale-group-lock.d.ts +34 -0
  19. package/dist/lib/content/locale-group-lock.d.ts.map +1 -0
  20. package/dist/lib/content/locale-group-lock.js +38 -0
  21. package/dist/lib/content/services/entry-locale-extension.service.d.ts +188 -0
  22. package/dist/lib/content/services/entry-locale-extension.service.d.ts.map +1 -0
  23. package/dist/lib/content/services/entry-locale-extension.service.js +662 -0
  24. package/dist/lib/content/services/locale-group.service.d.ts +87 -0
  25. package/dist/lib/content/services/locale-group.service.d.ts.map +1 -0
  26. package/dist/lib/content/services/locale-group.service.js +135 -0
  27. package/dist/lib/copilot/i18n-tool.provider.d.ts +53 -0
  28. package/dist/lib/copilot/i18n-tool.provider.d.ts.map +1 -0
  29. package/dist/lib/copilot/i18n-tool.provider.js +157 -0
  30. package/dist/lib/copilot/translation-proposal.applier.d.ts +87 -0
  31. package/dist/lib/copilot/translation-proposal.applier.d.ts.map +1 -0
  32. package/dist/lib/copilot/translation-proposal.applier.js +242 -0
  33. package/dist/lib/copilot/translation-proposal.provider.d.ts +114 -0
  34. package/dist/lib/copilot/translation-proposal.provider.d.ts.map +1 -0
  35. package/dist/lib/copilot/translation-proposal.provider.js +419 -0
  36. package/dist/lib/domain/errors/index.d.ts +4 -0
  37. package/dist/lib/domain/errors/index.d.ts.map +1 -0
  38. package/dist/lib/domain/errors/index.js +9 -0
  39. package/dist/lib/domain/errors/invalid-locale-set.error.d.ts +10 -0
  40. package/dist/lib/domain/errors/invalid-locale-set.error.d.ts.map +1 -0
  41. package/dist/lib/domain/errors/invalid-locale-set.error.js +16 -0
  42. package/dist/lib/domain/errors/invalid-locale.error.d.ts +10 -0
  43. package/dist/lib/domain/errors/invalid-locale.error.d.ts.map +1 -0
  44. package/dist/lib/domain/errors/invalid-locale.error.js +16 -0
  45. package/dist/lib/domain/errors/unknown-locale.error.d.ts +11 -0
  46. package/dist/lib/domain/errors/unknown-locale.error.d.ts.map +1 -0
  47. package/dist/lib/domain/errors/unknown-locale.error.js +18 -0
  48. package/dist/lib/domain/locale-policy.d.ts +42 -0
  49. package/dist/lib/domain/locale-policy.d.ts.map +1 -0
  50. package/dist/lib/domain/locale-policy.js +61 -0
  51. package/dist/lib/domain/value-objects/locale-set.d.ts +41 -0
  52. package/dist/lib/domain/value-objects/locale-set.d.ts.map +1 -0
  53. package/dist/lib/domain/value-objects/locale-set.js +89 -0
  54. package/dist/lib/domain/value-objects/locale.d.ts +55 -0
  55. package/dist/lib/domain/value-objects/locale.d.ts.map +1 -0
  56. package/dist/lib/domain/value-objects/locale.js +73 -0
  57. package/dist/lib/i18n.constants.d.ts +33 -0
  58. package/dist/lib/i18n.constants.d.ts.map +1 -0
  59. package/dist/lib/i18n.constants.js +89 -0
  60. package/dist/lib/i18n.module.d.ts +14 -0
  61. package/dist/lib/i18n.module.d.ts.map +1 -0
  62. package/dist/lib/i18n.module.js +82 -0
  63. package/dist/lib/insights/http/controllers/localization-coverage.controller.d.ts +23 -0
  64. package/dist/lib/insights/http/controllers/localization-coverage.controller.d.ts.map +1 -0
  65. package/dist/lib/insights/http/controllers/localization-coverage.controller.js +46 -0
  66. package/dist/lib/insights/infrastructure/queries/localization-coverage.query.d.ts +59 -0
  67. package/dist/lib/insights/infrastructure/queries/localization-coverage.query.d.ts.map +1 -0
  68. package/dist/lib/insights/infrastructure/queries/localization-coverage.query.js +179 -0
  69. package/dist/lib/insights/types/i18n-insights-view.d.ts +83 -0
  70. package/dist/lib/insights/types/i18n-insights-view.d.ts.map +1 -0
  71. package/dist/lib/insights/types/i18n-insights-view.js +10 -0
  72. package/dist/lib/locales/controllers/list-locales.controller.d.ts +26 -0
  73. package/dist/lib/locales/controllers/list-locales.controller.d.ts.map +1 -0
  74. package/dist/lib/locales/controllers/list-locales.controller.js +48 -0
  75. package/dist/lib/locales/services/locale-registry.service.d.ts +26 -0
  76. package/dist/lib/locales/services/locale-registry.service.d.ts.map +1 -0
  77. package/dist/lib/locales/services/locale-registry.service.js +70 -0
  78. package/dist/lib/locales/services/orphaned-locale.checker.d.ts +60 -0
  79. package/dist/lib/locales/services/orphaned-locale.checker.d.ts.map +1 -0
  80. package/dist/lib/locales/services/orphaned-locale.checker.js +114 -0
  81. package/dist/lib/types/locale.d.ts +68 -0
  82. package/dist/lib/types/locale.d.ts.map +1 -0
  83. package/dist/lib/types/locale.js +8 -0
  84. package/dist/lib/utils/i18n-plugin.d.ts +41 -0
  85. package/dist/lib/utils/i18n-plugin.d.ts.map +1 -0
  86. package/dist/lib/utils/i18n-plugin.js +53 -0
  87. package/package.json +47 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ortha CMS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @orthacms/i18n-server
2
+
3
+ Part of [Ortha CMS](https://github.com/ortha-source/ortha-cms).
4
+
5
+ ```sh
6
+ npm install @orthacms/i18n-server
7
+ ```
@@ -0,0 +1,13 @@
1
+ /** Public API of @orthacms/i18n-server. */
2
+ export { I18nServerPlugin } from './lib/utils/i18n-plugin';
3
+ export type { I18nServerPluginType } from './lib/utils/i18n-plugin';
4
+ export type { I18nPluginConfig, LocaleDef, LocaleDir, OrphanedLocalePolicy } from './lib/types/locale';
5
+ export { LOCALE_DIR, inferLocaleDir } from './lib/i18n.constants';
6
+ export { OrphanedLocaleChecker } from './lib/locales/services/orphaned-locale.checker';
7
+ export type { OrphanedLocaleReport } from './lib/locales/services/orphaned-locale.checker';
8
+ export { I18nModule } from './lib/i18n.module';
9
+ export { LocaleRegistryService } from './lib/locales/services/locale-registry.service';
10
+ export type { LocalesView } from './lib/locales/controllers/list-locales.controller';
11
+ export type { EntryLocaleItem, EntryLocalesView, LocaleSummaryItem, LocaleSummaryView } from './lib/content/services/locale-group.service';
12
+ export type { ContentTypeCoverageView, I18nCoverageView, LocaleCoverageView } from './lib/insights/types/i18n-insights-view';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAE3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,YAAY,EACR,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,oBAAoB,EACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gDAAgD,CAAC;AACvF,YAAY,EAAE,oBAAoB,EAAE,MAAM,gDAAgD,CAAC;AAE3F,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,gDAAgD,CAAC;AACvF,YAAY,EAAE,WAAW,EAAE,MAAM,mDAAmD,CAAC;AACrF,YAAY,EACR,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACpB,MAAM,6CAA6C,CAAC;AACrD,YAAY,EACR,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,EACrB,MAAM,yCAAyC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ /** Public API of @orthacms/i18n-server. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.LocaleRegistryService = exports.I18nModule = exports.OrphanedLocaleChecker = exports.inferLocaleDir = exports.LOCALE_DIR = exports.I18nServerPlugin = void 0;
5
+ var i18n_plugin_1 = require("./lib/utils/i18n-plugin");
6
+ Object.defineProperty(exports, "I18nServerPlugin", { enumerable: true, get: function () { return i18n_plugin_1.I18nServerPlugin; } });
7
+ var i18n_constants_1 = require("./lib/i18n.constants");
8
+ Object.defineProperty(exports, "LOCALE_DIR", { enumerable: true, get: function () { return i18n_constants_1.LOCALE_DIR; } });
9
+ Object.defineProperty(exports, "inferLocaleDir", { enumerable: true, get: function () { return i18n_constants_1.inferLocaleDir; } });
10
+ var orphaned_locale_checker_1 = require("./lib/locales/services/orphaned-locale.checker");
11
+ Object.defineProperty(exports, "OrphanedLocaleChecker", { enumerable: true, get: function () { return orphaned_locale_checker_1.OrphanedLocaleChecker; } });
12
+ var i18n_module_1 = require("./lib/i18n.module");
13
+ Object.defineProperty(exports, "I18nModule", { enumerable: true, get: function () { return i18n_module_1.I18nModule; } });
14
+ var locale_registry_service_1 = require("./lib/locales/services/locale-registry.service");
15
+ Object.defineProperty(exports, "LocaleRegistryService", { enumerable: true, get: function () { return locale_registry_service_1.LocaleRegistryService; } });
@@ -0,0 +1,15 @@
1
+ import type { ContentTypeRegistry } from '@orthacms/content-server';
2
+ import { LocaleGroupService, type EntryLocalesView } from '../services/locale-group.service';
3
+ /**
4
+ * `GET /api/i18n/content/:typeName/:id/locales` — the locale panel of one
5
+ * entry: every configured locale with the group's row in it (id, status,
6
+ * updatedAt) or null. Workspace-scoped like every entry read; gated on
7
+ * `content:read`.
8
+ */
9
+ export declare class GetEntryLocalesController {
10
+ private readonly registry;
11
+ private readonly groups;
12
+ constructor(registry: ContentTypeRegistry, groups: LocaleGroupService);
13
+ get(typeName: string, id: string, workspaceId: string): Promise<EntryLocalesView>;
14
+ }
15
+ //# sourceMappingURL=get-entry-locales.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-entry-locales.controller.d.ts","sourceRoot":"","sources":["../../../../src/lib/content/controllers/get-entry-locales.controller.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EACH,kBAAkB,EAClB,KAAK,gBAAgB,EACxB,MAAM,kCAAkC,CAAC;AAG1C;;;;;GAKG;AACH,qBAGa,yBAAyB;IAG9B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,kBAAkB;IAI/C,GAAG,CACoB,QAAQ,EAAE,MAAM,EACP,EAAE,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GACxC,OAAO,CAAC,gBAAgB,CAAC;CAI/B"}
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetEntryLocalesController = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const identity_server_1 = require("@orthacms/identity-server");
7
+ const workspaces_server_1 = require("@orthacms/workspaces-server");
8
+ const content_server_1 = require("@orthacms/content-server");
9
+ const locale_group_service_1 = require("../services/locale-group.service");
10
+ const resolve_type_1 = require("./resolve-type");
11
+ /**
12
+ * `GET /api/i18n/content/:typeName/:id/locales` — the locale panel of one
13
+ * entry: every configured locale with the group's row in it (id, status,
14
+ * updatedAt) or null. Workspace-scoped like every entry read; gated on
15
+ * `content:read`.
16
+ */
17
+ let GetEntryLocalesController = class GetEntryLocalesController {
18
+ registry;
19
+ groups;
20
+ constructor(registry, groups) {
21
+ this.registry = registry;
22
+ this.groups = groups;
23
+ }
24
+ get(typeName, id, workspaceId) {
25
+ const type = (0, resolve_type_1.resolveI18nType)(this.registry, typeName);
26
+ return this.groups.entryLocales(type, id, workspaceId);
27
+ }
28
+ };
29
+ exports.GetEntryLocalesController = GetEntryLocalesController;
30
+ tslib_1.__decorate([
31
+ (0, common_1.Get)(':typeName/:id/locales'),
32
+ tslib_1.__param(0, (0, common_1.Param)('typeName')),
33
+ tslib_1.__param(1, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
34
+ tslib_1.__param(2, (0, workspaces_server_1.CurrentWorkspace)()),
35
+ tslib_1.__metadata("design:type", Function),
36
+ tslib_1.__metadata("design:paramtypes", [String, String, String]),
37
+ tslib_1.__metadata("design:returntype", Promise)
38
+ ], GetEntryLocalesController.prototype, "get", null);
39
+ exports.GetEntryLocalesController = GetEntryLocalesController = tslib_1.__decorate([
40
+ (0, common_1.UseGuards)(identity_server_1.PermissionsGuard, workspaces_server_1.WorkspaceGuard),
41
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.CONTENT_READ),
42
+ (0, common_1.Controller)('i18n/content'),
43
+ tslib_1.__param(0, (0, content_server_1.InjectContentRegistry)()),
44
+ tslib_1.__metadata("design:paramtypes", [Function, locale_group_service_1.LocaleGroupService])
45
+ ], GetEntryLocalesController);
@@ -0,0 +1,30 @@
1
+ import type { ContentTypeRegistry } from '@orthacms/content-server';
2
+ import { LocaleSummaryDto } from '../dto/locale-summary.dto';
3
+ import { LocaleGroupService, type LocaleSummaryView } from '../services/locale-group.service';
4
+ /**
5
+ * `POST /api/i18n/content/:typeName/locale-summary` — the records table's
6
+ * batched read: per requested translation-group id, the group's live members
7
+ * with per-row status. Gated on `content:read`, workspace-scoped.
8
+ *
9
+ * **A POST that is a read**, because a page of group uuids outgrows a query
10
+ * string — so it answers `200`, not a `201` that would claim it created
11
+ * something.
12
+ *
13
+ * It still carries `OriginGuard`, unlike the reads that are shaped like reads.
14
+ * The guard keys off the **verb** a browser sees, not off what the handler
15
+ * happens to do with it: `POST` is the shape every cross-site form and
16
+ * `fetch` uses, this route is reachable with nothing but the session cookie,
17
+ * and "it only reads" is a property of today's handler that no test pins and
18
+ * a later edit could quietly retract. The guard is free for every legitimate
19
+ * caller — the admin sends its `Origin`, non-browser clients send none — so
20
+ * exempting it bought nothing and left the contract ("state-changing routes
21
+ * are origin-checked") disagreeing with the code. The body's shape is the
22
+ * batching decision; the guard is not part of it.
23
+ */
24
+ export declare class LocaleSummaryController {
25
+ private readonly registry;
26
+ private readonly groups;
27
+ constructor(registry: ContentTypeRegistry, groups: LocaleGroupService);
28
+ summarize(typeName: string, body: LocaleSummaryDto, workspaceId: string): Promise<LocaleSummaryView>;
29
+ }
30
+ //# sourceMappingURL=locale-summary.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-summary.controller.d.ts","sourceRoot":"","sources":["../../../../src/lib/content/controllers/locale-summary.controller.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EACH,kBAAkB,EAClB,KAAK,iBAAiB,EACzB,MAAM,kCAAkC,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAGa,uBAAuB;IAG5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,kBAAkB;IAK/C,SAAS,CACc,QAAQ,EAAE,MAAM,EAC3B,IAAI,EAAE,gBAAgB,EACV,WAAW,EAAE,MAAM,GACxC,OAAO,CAAC,iBAAiB,CAAC;CAIhC"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocaleSummaryController = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const identity_server_1 = require("@orthacms/identity-server");
7
+ const workspaces_server_1 = require("@orthacms/workspaces-server");
8
+ const content_server_1 = require("@orthacms/content-server");
9
+ const locale_summary_dto_1 = require("../dto/locale-summary.dto");
10
+ const locale_group_service_1 = require("../services/locale-group.service");
11
+ const resolve_type_1 = require("./resolve-type");
12
+ /**
13
+ * `POST /api/i18n/content/:typeName/locale-summary` — the records table's
14
+ * batched read: per requested translation-group id, the group's live members
15
+ * with per-row status. Gated on `content:read`, workspace-scoped.
16
+ *
17
+ * **A POST that is a read**, because a page of group uuids outgrows a query
18
+ * string — so it answers `200`, not a `201` that would claim it created
19
+ * something.
20
+ *
21
+ * It still carries `OriginGuard`, unlike the reads that are shaped like reads.
22
+ * The guard keys off the **verb** a browser sees, not off what the handler
23
+ * happens to do with it: `POST` is the shape every cross-site form and
24
+ * `fetch` uses, this route is reachable with nothing but the session cookie,
25
+ * and "it only reads" is a property of today's handler that no test pins and
26
+ * a later edit could quietly retract. The guard is free for every legitimate
27
+ * caller — the admin sends its `Origin`, non-browser clients send none — so
28
+ * exempting it bought nothing and left the contract ("state-changing routes
29
+ * are origin-checked") disagreeing with the code. The body's shape is the
30
+ * batching decision; the guard is not part of it.
31
+ */
32
+ let LocaleSummaryController = class LocaleSummaryController {
33
+ registry;
34
+ groups;
35
+ constructor(registry, groups) {
36
+ this.registry = registry;
37
+ this.groups = groups;
38
+ }
39
+ summarize(typeName, body, workspaceId) {
40
+ const type = (0, resolve_type_1.resolveI18nType)(this.registry, typeName);
41
+ return this.groups.summaries(type, body.groupIds, workspaceId);
42
+ }
43
+ };
44
+ exports.LocaleSummaryController = LocaleSummaryController;
45
+ tslib_1.__decorate([
46
+ (0, common_1.Post)(':typeName/locale-summary'),
47
+ (0, common_1.HttpCode)(200),
48
+ tslib_1.__param(0, (0, common_1.Param)('typeName')),
49
+ tslib_1.__param(1, (0, common_1.Body)()),
50
+ tslib_1.__param(2, (0, workspaces_server_1.CurrentWorkspace)()),
51
+ tslib_1.__metadata("design:type", Function),
52
+ tslib_1.__metadata("design:paramtypes", [String, locale_summary_dto_1.LocaleSummaryDto, String]),
53
+ tslib_1.__metadata("design:returntype", Promise)
54
+ ], LocaleSummaryController.prototype, "summarize", null);
55
+ exports.LocaleSummaryController = LocaleSummaryController = tslib_1.__decorate([
56
+ (0, common_1.UseGuards)(identity_server_1.OriginGuard, identity_server_1.PermissionsGuard, workspaces_server_1.WorkspaceGuard),
57
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.CONTENT_READ),
58
+ (0, common_1.Controller)('i18n/content'),
59
+ tslib_1.__param(0, (0, content_server_1.InjectContentRegistry)()),
60
+ tslib_1.__metadata("design:paramtypes", [Function, locale_group_service_1.LocaleGroupService])
61
+ ], LocaleSummaryController);
@@ -0,0 +1,9 @@
1
+ import type { AnyContentType, ContentTypeRegistry } from '@orthacms/content-server';
2
+ /**
3
+ * Resolve a route's `:typeName` to a registered **i18n** content type: 404
4
+ * for an unknown name (mirrors the content routes, no enumeration signal
5
+ * beyond what `/api/content-schema` already serves), 400 for a type that
6
+ * isn't localized (a caller bug, not a missing resource).
7
+ */
8
+ export declare function resolveI18nType(registry: ContentTypeRegistry, typeName: string): AnyContentType;
9
+ //# sourceMappingURL=resolve-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-type.d.ts","sourceRoot":"","sources":["../../../../src/lib/content/controllers/resolve-type.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACR,cAAc,EACd,mBAAmB,EACtB,MAAM,0BAA0B,CAAC;AAElC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC3B,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,MAAM,GACjB,cAAc,CAWhB"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveI18nType = resolveI18nType;
4
+ const common_1 = require("@nestjs/common");
5
+ /**
6
+ * Resolve a route's `:typeName` to a registered **i18n** content type: 404
7
+ * for an unknown name (mirrors the content routes, no enumeration signal
8
+ * beyond what `/api/content-schema` already serves), 400 for a type that
9
+ * isn't localized (a caller bug, not a missing resource).
10
+ */
11
+ function resolveI18nType(registry, typeName) {
12
+ const type = registry.get(typeName);
13
+ if (!type) {
14
+ throw new common_1.NotFoundException(`Unknown content type "${typeName}".`);
15
+ }
16
+ if (!type.i18n) {
17
+ throw new common_1.BadRequestException(`Content type "${typeName}" is not localized.`);
18
+ }
19
+ return type;
20
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Body for `POST /api/i18n/content/:typeName/locale-summary` — the records
3
+ * table's batched per-page read (POST, not GET: a page of uuids outgrows a
4
+ * query string). Capped so a request can't fan out unbounded.
5
+ */
6
+ export declare class LocaleSummaryDto {
7
+ /** Translation-group ids to summarize (one page's worth). */
8
+ groupIds: string[];
9
+ }
10
+ //# sourceMappingURL=locale-summary.dto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-summary.dto.d.ts","sourceRoot":"","sources":["../../../../src/lib/content/dto/locale-summary.dto.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,qBAAa,gBAAgB;IACzB,6DAA6D;IAW7D,QAAQ,EAAG,MAAM,EAAE,CAAC;CACvB"}
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocaleSummaryDto = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const swagger_1 = require("@nestjs/swagger");
6
+ const class_validator_1 = require("class-validator");
7
+ const i18n_constants_1 = require("../../i18n.constants");
8
+ /**
9
+ * Body for `POST /api/i18n/content/:typeName/locale-summary` — the records
10
+ * table's batched per-page read (POST, not GET: a page of uuids outgrows a
11
+ * query string). Capped so a request can't fan out unbounded.
12
+ */
13
+ class LocaleSummaryDto {
14
+ /** Translation-group ids to summarize (one page's worth). */
15
+ groupIds;
16
+ }
17
+ exports.LocaleSummaryDto = LocaleSummaryDto;
18
+ tslib_1.__decorate([
19
+ (0, swagger_1.ApiProperty)({
20
+ type: [String],
21
+ format: 'uuid',
22
+ maxItems: i18n_constants_1.LOCALE_SUMMARY_MAX_GROUPS,
23
+ example: ['c0a80101-7f3d-4c2b-9a1e-6d5b4c3a2f10'],
24
+ description: `Translation-group ids to summarize — one page's worth, capped at ${i18n_constants_1.LOCALE_SUMMARY_MAX_GROUPS}.`
25
+ }),
26
+ (0, class_validator_1.IsArray)(),
27
+ (0, class_validator_1.ArrayMaxSize)(i18n_constants_1.LOCALE_SUMMARY_MAX_GROUPS),
28
+ (0, class_validator_1.IsUUID)(undefined, { each: true }),
29
+ tslib_1.__metadata("design:type", Array)
30
+ ], LocaleSummaryDto.prototype, "groupIds", void 0);
@@ -0,0 +1,34 @@
1
+ import type { Database } from '@orthacms/database';
2
+ /**
3
+ * The minimal Drizzle executor the group lock needs: the root client or an
4
+ * open transaction (both expose `execute`). Mirrors
5
+ * `@orthacms/workspaces-server`'s `LockExecutor`, so content-server can pass
6
+ * the entry write's own `tx` straight through.
7
+ */
8
+ export type LockExecutor = Pick<Database, 'execute'>;
9
+ /**
10
+ * Serializes writes across the members of one translation group, using a
11
+ * transaction-scoped Postgres advisory lock keyed on the `locale_group_id`.
12
+ * The lock auto-releases at commit/rollback.
13
+ *
14
+ * **Why an advisory lock rather than ordering the row locks.** The shared-field
15
+ * sync locks every sibling `FOR UPDATE`, and ordering *that* statement does not
16
+ * make the group deadlock-free: by the time the sync runs, the transaction
17
+ * already holds a row lock on the entry being saved, taken by the entries
18
+ * pipeline's own `UPDATE` before any i18n code ran. Two concurrent saves on two
19
+ * locales of one record therefore each hold a row the other is about to
20
+ * request, whatever order the sibling `SELECT … FOR UPDATE` uses — Postgres
21
+ * detects the cycle and aborts one with `deadlock detected` (SQLSTATE 40P01),
22
+ * which reaches the client as a 500 on an ordinary save.
23
+ *
24
+ * Taking this lock from the extension's `beforeWrite` hook — the first
25
+ * statement in the write transaction, ahead of the row `UPDATE` — imposes a
26
+ * total order on the group instead: the second saver waits at the lock, and by
27
+ * the time it proceeds the first has committed and released every row. Waiting
28
+ * is the correct outcome for two edits to one record; deadlocking is not.
29
+ *
30
+ * Exclusive rather than shared because every taker is a writer: a save reaches
31
+ * here only when it has shared columns or a syncing relation to propagate.
32
+ */
33
+ export declare function lockLocaleGroup(executor: LockExecutor, localeGroupId: string): Promise<unknown>;
34
+ //# sourceMappingURL=locale-group-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-group-lock.d.ts","sourceRoot":"","sources":["../../../src/lib/content/locale-group-lock.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAUrD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAC3B,QAAQ,EAAE,YAAY,EACtB,aAAa,EAAE,MAAM,GACtB,OAAO,CAAC,OAAO,CAAC,CAIlB"}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lockLocaleGroup = lockLocaleGroup;
4
+ const drizzle_orm_1 = require("drizzle-orm");
5
+ /**
6
+ * A private lock class (the first `pg_advisory_xact_lock` key) namespacing the
7
+ * per-translation-group advisory lock below, so its hashed group-id key can't
8
+ * collide with any other advisory lock the app takes (workspace content `WK`,
9
+ * revision append `RE`, relation append `RL`). Arbitrary but fixed.
10
+ */
11
+ const LOCALE_GROUP_LOCK_CLASS = 0x4c47; // 'LG'
12
+ /**
13
+ * Serializes writes across the members of one translation group, using a
14
+ * transaction-scoped Postgres advisory lock keyed on the `locale_group_id`.
15
+ * The lock auto-releases at commit/rollback.
16
+ *
17
+ * **Why an advisory lock rather than ordering the row locks.** The shared-field
18
+ * sync locks every sibling `FOR UPDATE`, and ordering *that* statement does not
19
+ * make the group deadlock-free: by the time the sync runs, the transaction
20
+ * already holds a row lock on the entry being saved, taken by the entries
21
+ * pipeline's own `UPDATE` before any i18n code ran. Two concurrent saves on two
22
+ * locales of one record therefore each hold a row the other is about to
23
+ * request, whatever order the sibling `SELECT … FOR UPDATE` uses — Postgres
24
+ * detects the cycle and aborts one with `deadlock detected` (SQLSTATE 40P01),
25
+ * which reaches the client as a 500 on an ordinary save.
26
+ *
27
+ * Taking this lock from the extension's `beforeWrite` hook — the first
28
+ * statement in the write transaction, ahead of the row `UPDATE` — imposes a
29
+ * total order on the group instead: the second saver waits at the lock, and by
30
+ * the time it proceeds the first has committed and released every row. Waiting
31
+ * is the correct outcome for two edits to one record; deadlocking is not.
32
+ *
33
+ * Exclusive rather than shared because every taker is a writer: a save reaches
34
+ * here only when it has shared columns or a syncing relation to propagate.
35
+ */
36
+ function lockLocaleGroup(executor, localeGroupId) {
37
+ return executor.execute((0, drizzle_orm_1.sql) `select pg_advisory_xact_lock(${LOCALE_GROUP_LOCK_CLASS}, hashtext(${localeGroupId}))`);
38
+ }
@@ -0,0 +1,188 @@
1
+ import { type SQL } from 'drizzle-orm';
2
+ import { type Database } from '@orthacms/database';
3
+ import { EntryValidationService, RelationLinkService, type AnyContentType, type ContentEntryExtension, type EntryFilterExtension, type EntryScopeParams, type EntryTransaction, type EntryWriteContext, type EntryWriteFanout } from '@orthacms/content-server';
4
+ import { LocaleRegistryService } from '../../locales/services/locale-registry.service';
5
+ /**
6
+ * The i18n plugin's binding of content-server's `CONTENT_ENTRY_EXTENSION`
7
+ * port — where every locale *behavior* lives, so the content library itself
8
+ * stays locale-agnostic:
9
+ *
10
+ * - **List scoping** — the active-locale filter (strict; the default locale
11
+ * when the request names none), plus the relation picker's
12
+ * `localeFallback=default` union.
13
+ * - **Create stamping** — validates the requested slug and stamps the
14
+ * `locale` envelope column (the group id comes from the column default: a
15
+ * plain create starts its own translation group).
16
+ * - **Shared-field sync** — on update, non-`localized` field values propagate
17
+ * to every sibling row of the translation group, inside the same
18
+ * transaction; published siblings are re-validated so a draft edit can
19
+ * never silently invalidate live content. The rewritten siblings are handed
20
+ * back to the entries pipeline, which appends a **revision** for each — their
21
+ * values changed, so their history has to say so.
22
+ * - **Virtual filters** — `hasLocale` / `missingLocale` / `localeCount`,
23
+ * resolved to EXISTS / count subqueries over the group (ridden by the
24
+ * `(locale_group_id, locale)` unique index).
25
+ *
26
+ * Every method no-ops for non-i18n types, as the port contract requires.
27
+ */
28
+ export declare class EntryLocaleExtensionService implements ContentEntryExtension {
29
+ private readonly db;
30
+ private readonly locales;
31
+ private readonly validation;
32
+ private readonly relations;
33
+ constructor(db: Database, locales: LocaleRegistryService, validation: EntryValidationService, relations: RelationLinkService);
34
+ /** @inheritdoc */
35
+ listScope(type: AnyContentType, workspaceId: string, params: EntryScopeParams): SQL | undefined;
36
+ /**
37
+ * @inheritdoc
38
+ *
39
+ * Stamps the validated `locale` (defaulting when absent). When a
40
+ * `localeGroupId` is given the new row **joins that existing group** (a
41
+ * sibling translation) — verified to name a real group in the workspace
42
+ * first (else 404), so a typo can't spawn a stray one-row group. Absent →
43
+ * the column default (`gen_random_uuid()`) starts a fresh group.
44
+ */
45
+ createColumns(type: AnyContentType, workspaceId: string, params: EntryScopeParams): Promise<Record<string, unknown>>;
46
+ /**
47
+ * Assert a translation group has ≥1 live row in this workspace, so a
48
+ * sibling attaches to a real group. Runs before the create transaction —
49
+ * the (benign) TOCTOU window is covered by the row staying valid and the
50
+ * `(locale_group_id, locale)` unique index still guarding duplicates.
51
+ */
52
+ private assertGroupExists;
53
+ /**
54
+ * @inheritdoc
55
+ *
56
+ * Takes the translation group's advisory lock, ahead of every row lock the
57
+ * write will acquire. See {@link lockLocaleGroup} for why ordering the
58
+ * sibling `FOR UPDATE` cannot replace this: the edited row is already
59
+ * locked by the pipeline's own `UPDATE` before any i18n code runs, so two
60
+ * saves in one group invert their lock order no matter how the sync sorts
61
+ * its siblings.
62
+ *
63
+ * No group id means a create starting a fresh group — nothing to contend
64
+ * on, so nothing to lock.
65
+ */
66
+ beforeWrite(tx: EntryTransaction, type: AnyContentType, params: EntryScopeParams): Promise<void>;
67
+ /** @inheritdoc */
68
+ afterUpdate(tx: EntryTransaction, type: AnyContentType, row: Record<string, unknown>, values: Record<string, unknown>, workspaceId: string, context: EntryWriteContext): Promise<Record<string, unknown>[]>;
69
+ /** @inheritdoc */
70
+ describeFanout(type: AnyContentType, entryId: string, values: Record<string, unknown>, workspaceId: string): Promise<EntryWriteFanout | undefined>;
71
+ /**
72
+ * Whether this field's value reaches the row's siblings when it is saved —
73
+ * the predicate behind {@link describeFanout}, stated once against the same
74
+ * three rules {@link propagateToSiblings} applies.
75
+ *
76
+ * It is broader than {@link sharedColumns}, deliberately: that method
77
+ * answers "which columns copy across verbatim", because shared columns,
78
+ * mirrored FKs and join-backed link sets are *applied* by three different
79
+ * mechanisms. To a person being told what their change will do, the three
80
+ * are one fact — the value they set here lands over there.
81
+ *
82
+ * A **mirrored** relation counts even though each sibling ends up holding a
83
+ * different id: the sibling's link is being repointed at the record this
84
+ * save chose, which is a change to that row whatever id expresses it.
85
+ */
86
+ private travelsToSiblings;
87
+ /**
88
+ * Push this row's state onto every sibling in its translation group, and
89
+ * return the rows actually rewritten (the pipeline appends a revision for
90
+ * each — their content moved in this transaction, and without a version
91
+ * their history would skip the change and a later restore would silently
92
+ * undo it).
93
+ *
94
+ * Three kinds of state travel, each computed once and applied per sibling:
95
+ * shared **columns** (identical everywhere), **mirrored** single-relation
96
+ * FKs (resolved into each sibling's own locale), and the **link sets** of
97
+ * join-backed relations. A sibling whose every one of those already matches
98
+ * is left completely untouched — that guard is what stops one save
99
+ * re-versioning the whole group.
100
+ */
101
+ private propagateToSiblings;
102
+ /**
103
+ * Fill a **newly created** sibling's propagating relations from the group
104
+ * it just joined — the half of the sync that runs inward.
105
+ *
106
+ * A translation is created through the ordinary create route with a
107
+ * `localeGroupId`, and a create body carries no relation links at all: they
108
+ * are join-backed (never in the values bag) or per-locale (deliberately
109
+ * dropped by the client, since the source's id names another language's
110
+ * row). So without this a translation is born with none of the record's
111
+ * links, which is exactly the manual per-locale re-linking this sync
112
+ * exists to remove.
113
+ *
114
+ * The donor is one existing sibling — any of them agree on a shared link,
115
+ * and a mirrored one resolves through the same translation group whichever
116
+ * is asked — picked deterministically (the default locale where the group
117
+ * has it) so two creates never inherit from different rows.
118
+ *
119
+ * Columns it derives are also written back onto `row` **in place**: the
120
+ * caller snapshots that object as the new entry's first revision *after*
121
+ * this call, so a database-only write would leave version 1 describing
122
+ * something the row never held.
123
+ */
124
+ private inheritRelationsFromGroup;
125
+ /**
126
+ * Per locale, the **mirrored** single-relation FK columns a sibling in that
127
+ * locale should hold: the source's linked record, resolved to that record's
128
+ * own translation in the sibling's language.
129
+ *
130
+ * An unresolvable link becomes `null` rather than being skipped. Leaving
131
+ * the sibling's existing value would keep it pointing at the *previous*
132
+ * record's translation — silently wrong data, which is worse than an empty
133
+ * field that the editor can see and the publish gate can block on.
134
+ */
135
+ private mirroredColumnsByLocale;
136
+ /**
137
+ * Per join-backed relation field, per locale, the ordered link ids a
138
+ * sibling in that locale should hold.
139
+ *
140
+ * A **shared** relation hands every sibling the source's ids verbatim — the
141
+ * target has no locales, so there is one right row for the whole group. A
142
+ * **mirrored** one maps each id through its translation group into the
143
+ * sibling's language, preserving the source's order and dropping links
144
+ * whose target has no translation there (a content gap on the target, not a
145
+ * reason to fail this save).
146
+ */
147
+ private linkSetsByLocale;
148
+ /** @inheritdoc */
149
+ filterExtension(type: AnyContentType): EntryFilterExtension | undefined;
150
+ /**
151
+ * Translate one virtual locale rule into SQL. All three fields quantify
152
+ * over the row's translation group via a self-EXISTS/count subquery,
153
+ * workspace-scoped, (on paranoid types) live-rows-only, and **restricted to
154
+ * the configured locale set** — the same restriction the coverage query
155
+ * applies (`configuredScope`).
156
+ *
157
+ * That last one is what keeps the records table and the Translation
158
+ * coverage card telling the same story. A row in a slug the host has since
159
+ * dropped is not coverage of anything: coverage filters it out, so leaving
160
+ * it in here made `localeCount` count higher than the number of configured
161
+ * locales, and a record the card reported as fully localized answered
162
+ * neither `localeCount eq 3` (the configured total) nor any `hasLocale`
163
+ * question consistent with it. Two views of one record, disagreeing.
164
+ *
165
+ * `missingLocale in [a, b]` is a `notExists` over the union, so it means
166
+ * "missing **all** of these", not "missing any" — the union has no matching
167
+ * sibling only when every named locale is absent. It is the useful reading
168
+ * for the records table ("show me what has none of my target languages");
169
+ * "missing any" is expressed as an `or` of `missingLocale eq` rules.
170
+ */
171
+ private resolveLocaleRule;
172
+ /**
173
+ * The column bag of the fields whose value is **identical in every locale**,
174
+ * drawn from a save's coerced values — the part of the sync that is the
175
+ * same for every sibling.
176
+ *
177
+ * Scalars and media qualify unless marked `localized`. A relation qualifies
178
+ * only when it is column-backed *and* {@link RELATION_LOCALE_SYNC.Shared}:
179
+ * a join-backed relation has no column to copy (its links go through
180
+ * {@link linkSetsByLocale}), and a mirrored one stores a different id in
181
+ * every locale by design (see {@link mirroredColumnsByLocale}), so copying
182
+ * one verbatim would be exactly the cross-locale link the writer rejects.
183
+ *
184
+ * `null` when the type has no such field at all.
185
+ */
186
+ private sharedColumns;
187
+ }
188
+ //# sourceMappingURL=entry-locale-extension.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-locale-extension.service.d.ts","sourceRoot":"","sources":["../../../../src/lib/content/services/entry-locale-extension.service.ts"],"names":[],"mappings":"AAMA,OAAO,EAYH,KAAK,GAAG,EACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EACH,sBAAsB,EACtB,mBAAmB,EAMnB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAE1B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACxB,MAAM,0BAA0B,CAAC;AASlC,OAAO,EAAE,qBAAqB,EAAE,MAAM,gDAAgD,CAAC;AAuCvF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBACa,2BAA4B,YAAW,qBAAqB;IAE/C,OAAO,CAAC,QAAQ,CAAC,EAAE;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAK3B,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAPS,EAAE,EAAE,QAAQ,EAC9B,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,sBAAsB,EAKlC,SAAS,EAAE,mBAAmB;IAGnD,kBAAkB;IAClB,SAAS,CACL,IAAI,EAAE,cAAc,EACpB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,GACzB,GAAG,GAAG,SAAS;IAmClB;;;;;;;;OAQG;IACG,aAAa,CACf,IAAI,EAAE,cAAc,EACpB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAgBnC;;;;;OAKG;YACW,iBAAiB;IAwB/B;;;;;;;;;;;;OAYG;IACG,WAAW,CACb,EAAE,EAAE,gBAAgB,EACpB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC;IAKhB,kBAAkB;IACZ,WAAW,CACb,EAAE,EAAE,gBAAgB,EACpB,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,iBAAiB,GAC3B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAmBrC,kBAAkB;IACZ,cAAc,CAChB,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAkDxC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;;;;;;OAaG;YACW,mBAAmB;IAiMjC;;;;;;;;;;;;;;;;;;;;;OAqBG;YACW,yBAAyB;IAmEvC;;;;;;;;;OASG;YACW,uBAAuB;IA0CrC;;;;;;;;;;OAUG;YACW,gBAAgB;IA6C9B,kBAAkB;IAClB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,oBAAoB,GAAG,SAAS;IAcvE;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,iBAAiB;IAmE/B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,aAAa;CAoBxB"}