@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.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/lib/content/controllers/get-entry-locales.controller.d.ts +15 -0
- package/dist/lib/content/controllers/get-entry-locales.controller.d.ts.map +1 -0
- package/dist/lib/content/controllers/get-entry-locales.controller.js +45 -0
- package/dist/lib/content/controllers/locale-summary.controller.d.ts +30 -0
- package/dist/lib/content/controllers/locale-summary.controller.d.ts.map +1 -0
- package/dist/lib/content/controllers/locale-summary.controller.js +61 -0
- package/dist/lib/content/controllers/resolve-type.d.ts +9 -0
- package/dist/lib/content/controllers/resolve-type.d.ts.map +1 -0
- package/dist/lib/content/controllers/resolve-type.js +20 -0
- package/dist/lib/content/dto/locale-summary.dto.d.ts +10 -0
- package/dist/lib/content/dto/locale-summary.dto.d.ts.map +1 -0
- package/dist/lib/content/dto/locale-summary.dto.js +30 -0
- package/dist/lib/content/locale-group-lock.d.ts +34 -0
- package/dist/lib/content/locale-group-lock.d.ts.map +1 -0
- package/dist/lib/content/locale-group-lock.js +38 -0
- package/dist/lib/content/services/entry-locale-extension.service.d.ts +188 -0
- package/dist/lib/content/services/entry-locale-extension.service.d.ts.map +1 -0
- package/dist/lib/content/services/entry-locale-extension.service.js +662 -0
- package/dist/lib/content/services/locale-group.service.d.ts +87 -0
- package/dist/lib/content/services/locale-group.service.d.ts.map +1 -0
- package/dist/lib/content/services/locale-group.service.js +135 -0
- package/dist/lib/copilot/i18n-tool.provider.d.ts +53 -0
- package/dist/lib/copilot/i18n-tool.provider.d.ts.map +1 -0
- package/dist/lib/copilot/i18n-tool.provider.js +157 -0
- package/dist/lib/copilot/translation-proposal.applier.d.ts +87 -0
- package/dist/lib/copilot/translation-proposal.applier.d.ts.map +1 -0
- package/dist/lib/copilot/translation-proposal.applier.js +242 -0
- package/dist/lib/copilot/translation-proposal.provider.d.ts +114 -0
- package/dist/lib/copilot/translation-proposal.provider.d.ts.map +1 -0
- package/dist/lib/copilot/translation-proposal.provider.js +419 -0
- package/dist/lib/domain/errors/index.d.ts +4 -0
- package/dist/lib/domain/errors/index.d.ts.map +1 -0
- package/dist/lib/domain/errors/index.js +9 -0
- package/dist/lib/domain/errors/invalid-locale-set.error.d.ts +10 -0
- package/dist/lib/domain/errors/invalid-locale-set.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/invalid-locale-set.error.js +16 -0
- package/dist/lib/domain/errors/invalid-locale.error.d.ts +10 -0
- package/dist/lib/domain/errors/invalid-locale.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/invalid-locale.error.js +16 -0
- package/dist/lib/domain/errors/unknown-locale.error.d.ts +11 -0
- package/dist/lib/domain/errors/unknown-locale.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/unknown-locale.error.js +18 -0
- package/dist/lib/domain/locale-policy.d.ts +42 -0
- package/dist/lib/domain/locale-policy.d.ts.map +1 -0
- package/dist/lib/domain/locale-policy.js +61 -0
- package/dist/lib/domain/value-objects/locale-set.d.ts +41 -0
- package/dist/lib/domain/value-objects/locale-set.d.ts.map +1 -0
- package/dist/lib/domain/value-objects/locale-set.js +89 -0
- package/dist/lib/domain/value-objects/locale.d.ts +55 -0
- package/dist/lib/domain/value-objects/locale.d.ts.map +1 -0
- package/dist/lib/domain/value-objects/locale.js +73 -0
- package/dist/lib/i18n.constants.d.ts +33 -0
- package/dist/lib/i18n.constants.d.ts.map +1 -0
- package/dist/lib/i18n.constants.js +89 -0
- package/dist/lib/i18n.module.d.ts +14 -0
- package/dist/lib/i18n.module.d.ts.map +1 -0
- package/dist/lib/i18n.module.js +82 -0
- package/dist/lib/insights/http/controllers/localization-coverage.controller.d.ts +23 -0
- package/dist/lib/insights/http/controllers/localization-coverage.controller.d.ts.map +1 -0
- package/dist/lib/insights/http/controllers/localization-coverage.controller.js +46 -0
- package/dist/lib/insights/infrastructure/queries/localization-coverage.query.d.ts +59 -0
- package/dist/lib/insights/infrastructure/queries/localization-coverage.query.d.ts.map +1 -0
- package/dist/lib/insights/infrastructure/queries/localization-coverage.query.js +179 -0
- package/dist/lib/insights/types/i18n-insights-view.d.ts +83 -0
- package/dist/lib/insights/types/i18n-insights-view.d.ts.map +1 -0
- package/dist/lib/insights/types/i18n-insights-view.js +10 -0
- package/dist/lib/locales/controllers/list-locales.controller.d.ts +26 -0
- package/dist/lib/locales/controllers/list-locales.controller.d.ts.map +1 -0
- package/dist/lib/locales/controllers/list-locales.controller.js +48 -0
- package/dist/lib/locales/services/locale-registry.service.d.ts +26 -0
- package/dist/lib/locales/services/locale-registry.service.d.ts.map +1 -0
- package/dist/lib/locales/services/locale-registry.service.js +70 -0
- package/dist/lib/locales/services/orphaned-locale.checker.d.ts +60 -0
- package/dist/lib/locales/services/orphaned-locale.checker.d.ts.map +1 -0
- package/dist/lib/locales/services/orphaned-locale.checker.js +114 -0
- package/dist/lib/types/locale.d.ts +68 -0
- package/dist/lib/types/locale.d.ts.map +1 -0
- package/dist/lib/types/locale.js +8 -0
- package/dist/lib/utils/i18n-plugin.d.ts +41 -0
- package/dist/lib/utils/i18n-plugin.d.ts.map +1 -0
- package/dist/lib/utils/i18n-plugin.js +53 -0
- package/package.json +47 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalizationCoverageQuery = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
7
|
+
const database_1 = require("@orthacms/database");
|
|
8
|
+
const content_server_1 = require("@orthacms/content-server");
|
|
9
|
+
const locale_registry_service_1 = require("../../../locales/services/locale-registry.service");
|
|
10
|
+
/** Reads a generated table's columns by property name. */
|
|
11
|
+
function columnsOf(type) {
|
|
12
|
+
return type.table;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Live read-model for the localization coverage widget.
|
|
16
|
+
*
|
|
17
|
+
* **This lives in i18n-server, not content-server, and that is not arbitrary.**
|
|
18
|
+
* Coverage is a question about a set the content plugin deliberately does not
|
|
19
|
+
* know: the *configured* locales. content-server owns the `locale` column's
|
|
20
|
+
* shape and nothing about what a locale means, so it can report which slugs
|
|
21
|
+
* happen to appear in the data but not which ones are missing — and "missing"
|
|
22
|
+
* is the entire widget. `LocaleRegistryService` is the source of that set, and
|
|
23
|
+
* it is here.
|
|
24
|
+
*
|
|
25
|
+
* Like content's own insights query this aggregates **on demand**, per type,
|
|
26
|
+
* with no projection: a localized collection is its own generated
|
|
27
|
+
* `content_<name>` table, so a workspace-wide answer is inherently a fan-out.
|
|
28
|
+
*/
|
|
29
|
+
let LocalizationCoverageQuery = class LocalizationCoverageQuery {
|
|
30
|
+
db;
|
|
31
|
+
registry;
|
|
32
|
+
locales;
|
|
33
|
+
constructor(db, registry, locales) {
|
|
34
|
+
this.db = db;
|
|
35
|
+
this.registry = registry;
|
|
36
|
+
this.locales = locales;
|
|
37
|
+
}
|
|
38
|
+
/** Coverage across every localized content type in the workspace. */
|
|
39
|
+
async coverage(workspaceId) {
|
|
40
|
+
const configured = this.locales.all();
|
|
41
|
+
const slugs = configured.map((locale) => locale.slug);
|
|
42
|
+
const localizedTypes = this.registry.all().filter((type) => type.i18n);
|
|
43
|
+
/** Records holding a row in each locale, by slug. */
|
|
44
|
+
const translated = new Map(slugs.map((slug) => [slug, 0]));
|
|
45
|
+
const types = [];
|
|
46
|
+
let records = 0;
|
|
47
|
+
let localized = 0;
|
|
48
|
+
let single = 0;
|
|
49
|
+
for (const type of localizedTypes) {
|
|
50
|
+
for (const row of await this.perLocale(type, workspaceId, slugs)) {
|
|
51
|
+
translated.set(row.locale, (translated.get(row.locale) ?? 0) + row.translated);
|
|
52
|
+
}
|
|
53
|
+
const spread = await this.groupSpread(type, workspaceId, slugs, configured.length);
|
|
54
|
+
records += spread.records;
|
|
55
|
+
localized += spread.complete;
|
|
56
|
+
single += spread.single;
|
|
57
|
+
// A type the workspace has never used is noise on a chart about
|
|
58
|
+
// where the outstanding work sits — and listing it would push the
|
|
59
|
+
// types that do hold work further down.
|
|
60
|
+
if (spread.records === 0)
|
|
61
|
+
continue;
|
|
62
|
+
types.push({
|
|
63
|
+
name: type.name,
|
|
64
|
+
label: type.label,
|
|
65
|
+
records: spread.records,
|
|
66
|
+
localized: spread.complete,
|
|
67
|
+
notLocalized: configured.length > 1 ? spread.single : 0,
|
|
68
|
+
requiresLocalization: spread.records - spread.complete
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
types.sort((a, b) => b.records - a.records);
|
|
72
|
+
const locales = configured.map((locale) => {
|
|
73
|
+
const covered = translated.get(locale.slug) ?? 0;
|
|
74
|
+
return {
|
|
75
|
+
locale: locale.slug,
|
|
76
|
+
name: locale.name,
|
|
77
|
+
isDefault: locale.isDefault ?? false,
|
|
78
|
+
translated: covered,
|
|
79
|
+
missing: records - covered
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
locales,
|
|
84
|
+
records,
|
|
85
|
+
localized,
|
|
86
|
+
// With one locale configured there is nowhere to translate to, so
|
|
87
|
+
// every record trivially covers "all" of them — reporting those
|
|
88
|
+
// same records as "not localized" would state both at once.
|
|
89
|
+
notLocalized: configured.length > 1 ? single : 0,
|
|
90
|
+
requiresLocalization: records - localized,
|
|
91
|
+
types
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Records holding a row in each locale, for one type.
|
|
96
|
+
*
|
|
97
|
+
* `count(distinct locale_group_id)` rather than `count(*)`: the
|
|
98
|
+
* `(locale_group_id, locale)` unique index makes those equal in a healthy
|
|
99
|
+
* table, and the distinct keeps the figure honest against a paranoid type
|
|
100
|
+
* where the index is partial and a trashed row could otherwise be counted
|
|
101
|
+
* twice.
|
|
102
|
+
*/
|
|
103
|
+
async perLocale(type, workspaceId, slugs) {
|
|
104
|
+
const columns = columnsOf(type);
|
|
105
|
+
// The generated tables are reached as an untyped column bag, so both
|
|
106
|
+
// the projection and the grouping go through `sql` fragments — a bare
|
|
107
|
+
// `AnyColumn` carries no dialect and doesn't satisfy the builder's
|
|
108
|
+
// overloads.
|
|
109
|
+
const locale = (0, drizzle_orm_1.sql) `${columns['locale']}`;
|
|
110
|
+
return this.db
|
|
111
|
+
.select({
|
|
112
|
+
locale,
|
|
113
|
+
translated: (0, drizzle_orm_1.sql) `cast(count(distinct ${columns['localeGroupId']}) as int)`
|
|
114
|
+
})
|
|
115
|
+
.from(type.table)
|
|
116
|
+
.where(this.configuredScope(type, workspaceId, slugs))
|
|
117
|
+
.groupBy(locale);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* How many translation groups this type has, how many are complete, and how
|
|
121
|
+
* many sit in a single language.
|
|
122
|
+
*
|
|
123
|
+
* One grouped subquery folded by an outer aggregate — the alternative,
|
|
124
|
+
* reading one row per group and counting in JS, would pull the workspace's
|
|
125
|
+
* whole content set over the wire to produce three integers.
|
|
126
|
+
*/
|
|
127
|
+
async groupSpread(type, workspaceId, slugs, configuredCount) {
|
|
128
|
+
const columns = columnsOf(type);
|
|
129
|
+
const groups = this.db
|
|
130
|
+
.select({
|
|
131
|
+
covered: (0, drizzle_orm_1.sql) `count(distinct ${columns['locale']})`.as('covered')
|
|
132
|
+
})
|
|
133
|
+
.from(type.table)
|
|
134
|
+
.where(this.configuredScope(type, workspaceId, slugs))
|
|
135
|
+
.groupBy((0, drizzle_orm_1.sql) `${columns['localeGroupId']}`)
|
|
136
|
+
.as('groups');
|
|
137
|
+
// The subquery alias carries no column type through, so both filters
|
|
138
|
+
// are raw fragments rather than `eq(...)`, whose overloads can't
|
|
139
|
+
// resolve one.
|
|
140
|
+
const [row] = await this.db
|
|
141
|
+
.select({
|
|
142
|
+
records: (0, drizzle_orm_1.sql) `cast(count(*) as int)`,
|
|
143
|
+
complete: (0, drizzle_orm_1.sql) `cast(count(*) filter (
|
|
144
|
+
where ${groups.covered} = ${configuredCount}
|
|
145
|
+
) as int)`,
|
|
146
|
+
single: (0, drizzle_orm_1.sql) `cast(count(*) filter (
|
|
147
|
+
where ${groups.covered} = 1
|
|
148
|
+
) as int)`
|
|
149
|
+
})
|
|
150
|
+
.from(groups);
|
|
151
|
+
return {
|
|
152
|
+
records: row?.records ?? 0,
|
|
153
|
+
complete: row?.complete ?? 0,
|
|
154
|
+
single: row?.single ?? 0
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Workspace scope, the soft-delete guard, **and** the configured locale set.
|
|
159
|
+
*
|
|
160
|
+
* The locale filter is what makes "complete" mean what it says. A row in a
|
|
161
|
+
* slug the host has since removed from its config is not coverage of
|
|
162
|
+
* anything, and left in it would push a group's distinct-locale count past
|
|
163
|
+
* the configured total — so a record could be over-covered and still not
|
|
164
|
+
* counted as complete. Filtering here also drops a group made entirely of
|
|
165
|
+
* such rows, which is the right answer rather than a record with zero
|
|
166
|
+
* languages.
|
|
167
|
+
*/
|
|
168
|
+
configuredScope(type, workspaceId, slugs) {
|
|
169
|
+
const columns = columnsOf(type);
|
|
170
|
+
return (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(columns['workspaceId'], workspaceId), (0, drizzle_orm_1.inArray)(columns['locale'], slugs), type.paranoid ? (0, drizzle_orm_1.isNull)(columns['deletedAt']) : undefined);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
exports.LocalizationCoverageQuery = LocalizationCoverageQuery;
|
|
174
|
+
exports.LocalizationCoverageQuery = LocalizationCoverageQuery = tslib_1.__decorate([
|
|
175
|
+
(0, common_1.Injectable)(),
|
|
176
|
+
tslib_1.__param(0, (0, database_1.InjectDatabase)()),
|
|
177
|
+
tslib_1.__param(1, (0, content_server_1.InjectContentRegistry)()),
|
|
178
|
+
tslib_1.__metadata("design:paramtypes", [Object, Function, locale_registry_service_1.LocaleRegistryService])
|
|
179
|
+
], LocalizationCoverageQuery);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-side view shapes for the localization Insights widget.
|
|
3
|
+
*
|
|
4
|
+
* A **transport contract**, restated verbatim by `i18n-admin` (the admin can't
|
|
5
|
+
* import a server package across the module boundary). Derived live from the
|
|
6
|
+
* localized collection tables — this plugin owns no table and keeps no
|
|
7
|
+
* projection.
|
|
8
|
+
*/
|
|
9
|
+
/** How much of the workspace's localized content exists in one locale. */
|
|
10
|
+
export interface LocaleCoverageView {
|
|
11
|
+
/** The configured locale slug. */
|
|
12
|
+
locale: string;
|
|
13
|
+
/** Its display name, from the host's config. */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Whether it is the configured default locale. */
|
|
16
|
+
isDefault: boolean;
|
|
17
|
+
/** Records that have a row in this locale. */
|
|
18
|
+
translated: number;
|
|
19
|
+
/** Records that do not. */
|
|
20
|
+
missing: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One localized content type's own coverage.
|
|
24
|
+
*
|
|
25
|
+
* The same four figures as the workspace envelope, scoped to a type — which is
|
|
26
|
+
* what turns "122 records need translating" into "and 88 of them are articles".
|
|
27
|
+
* A workspace-wide total says work exists; only the per-type split says where.
|
|
28
|
+
*/
|
|
29
|
+
export interface ContentTypeCoverageView {
|
|
30
|
+
/** The type's machine name. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Its human label. */
|
|
33
|
+
label: string;
|
|
34
|
+
/** Translation groups of this type. */
|
|
35
|
+
records: number;
|
|
36
|
+
/** Of those, the ones present in every configured locale. */
|
|
37
|
+
localized: number;
|
|
38
|
+
/** Of those, the ones present in exactly one locale. */
|
|
39
|
+
notLocalized: number;
|
|
40
|
+
/** Of those, the ones missing at least one locale. */
|
|
41
|
+
requiresLocalization: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Localization coverage across every `i18n: true` content type.
|
|
45
|
+
*
|
|
46
|
+
* The unit is a **record**, not a row: a localized entry is one row per
|
|
47
|
+
* language sharing a `locale_group_id`, so counting rows would report a
|
|
48
|
+
* workspace of 40 stories in 3 languages as 120 things and make every
|
|
49
|
+
* percentage meaningless. Every figure below counts translation groups.
|
|
50
|
+
*
|
|
51
|
+
* `notLocalized` is a **subset** of `requiresLocalization`, not a second slice
|
|
52
|
+
* of it — a record in one language of four both "has no translations" and
|
|
53
|
+
* "needs some". They are separate because they are separate jobs: one is
|
|
54
|
+
* starting a translation, the other is finishing it.
|
|
55
|
+
*/
|
|
56
|
+
export interface I18nCoverageView {
|
|
57
|
+
/** One entry per configured locale, in config order. */
|
|
58
|
+
locales: LocaleCoverageView[];
|
|
59
|
+
/** Translation groups across every localized type. */
|
|
60
|
+
records: number;
|
|
61
|
+
/** Records that exist in **every** configured locale. */
|
|
62
|
+
localized: number;
|
|
63
|
+
/**
|
|
64
|
+
* Records that exist in exactly one locale — nothing translated yet.
|
|
65
|
+
*
|
|
66
|
+
* Always `0` when the deployment configures a single locale: there is
|
|
67
|
+
* nowhere to translate to, so every record would otherwise be reported as
|
|
68
|
+
* both fully localized and not localized at all.
|
|
69
|
+
*/
|
|
70
|
+
notLocalized: number;
|
|
71
|
+
/** Records missing at least one configured locale (`records - localized`). */
|
|
72
|
+
requiresLocalization: number;
|
|
73
|
+
/**
|
|
74
|
+
* Per-type coverage, most records first.
|
|
75
|
+
*
|
|
76
|
+
* A type the workspace has never used is **omitted** rather than listed at
|
|
77
|
+
* zero — the same rule the content pipeline applies. A permanently empty row
|
|
78
|
+
* is noise on a chart about where the outstanding work sits, and it would
|
|
79
|
+
* push the types that do hold work further down the list.
|
|
80
|
+
*/
|
|
81
|
+
types: ContentTypeCoverageView[];
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=i18n-insights-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n-insights-view.d.ts","sourceRoot":"","sources":["../../../../src/lib/insights/types/i18n-insights-view.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IAC/B,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACpC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,oBAAoB,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC7B,wDAAwD;IACxD,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;;;OAOG;IACH,KAAK,EAAE,uBAAuB,EAAE,CAAC;CACpC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Read-side view shapes for the localization Insights widget.
|
|
4
|
+
*
|
|
5
|
+
* A **transport contract**, restated verbatim by `i18n-admin` (the admin can't
|
|
6
|
+
* import a server package across the module boundary). Derived live from the
|
|
7
|
+
* localized collection tables — this plugin owns no table and keeps no
|
|
8
|
+
* projection.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LocaleDef } from '../../types/locale';
|
|
2
|
+
import { LocaleRegistryService } from '../services/locale-registry.service';
|
|
3
|
+
/** The `GET /api/i18n/locales` response envelope. */
|
|
4
|
+
export interface LocalesView {
|
|
5
|
+
/** Configured locales in display order; exactly one `isDefault`. */
|
|
6
|
+
items: LocaleDef[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* `GET /api/i18n/locales` — the configured locales, in display order. The
|
|
10
|
+
* admin's single source for the locale switcher / widget; any authenticated
|
|
11
|
+
* session may read it (the global `AuthGuard` applies; locales carry no
|
|
12
|
+
* per-workspace data).
|
|
13
|
+
*
|
|
14
|
+
* Each item carries everything a consumer needs to render the language
|
|
15
|
+
* *correctly*, not just to name it: `slug` doubles as the HTML `lang` value
|
|
16
|
+
* (it is a BCP-47 tag by contract) and `dir` is the resolved text direction.
|
|
17
|
+
* Together they are what lets the entry editor, the preview, and a published
|
|
18
|
+
* page mark up a translation as the language it is — which is the whole of
|
|
19
|
+
* WCAG 3.1.2 and, for an RTL locale, of 1.3.2.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ListLocalesController {
|
|
22
|
+
private readonly locales;
|
|
23
|
+
constructor(locales: LocaleRegistryService);
|
|
24
|
+
list(): LocalesView;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=list-locales.controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-locales.controller.d.ts","sourceRoot":"","sources":["../../../../src/lib/locales/controllers/list-locales.controller.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAE5E,qDAAqD;AACrD,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,KAAK,EAAE,SAAS,EAAE,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,qBACa,qBAAqB;IAClB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,qBAAqB;IAG3D,IAAI,IAAI,WAAW;CAYtB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ListLocalesController = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const locale_registry_service_1 = require("../services/locale-registry.service");
|
|
7
|
+
/**
|
|
8
|
+
* `GET /api/i18n/locales` — the configured locales, in display order. The
|
|
9
|
+
* admin's single source for the locale switcher / widget; any authenticated
|
|
10
|
+
* session may read it (the global `AuthGuard` applies; locales carry no
|
|
11
|
+
* per-workspace data).
|
|
12
|
+
*
|
|
13
|
+
* Each item carries everything a consumer needs to render the language
|
|
14
|
+
* *correctly*, not just to name it: `slug` doubles as the HTML `lang` value
|
|
15
|
+
* (it is a BCP-47 tag by contract) and `dir` is the resolved text direction.
|
|
16
|
+
* Together they are what lets the entry editor, the preview, and a published
|
|
17
|
+
* page mark up a translation as the language it is — which is the whole of
|
|
18
|
+
* WCAG 3.1.2 and, for an RTL locale, of 1.3.2.
|
|
19
|
+
*/
|
|
20
|
+
let ListLocalesController = class ListLocalesController {
|
|
21
|
+
locales;
|
|
22
|
+
constructor(locales) {
|
|
23
|
+
this.locales = locales;
|
|
24
|
+
}
|
|
25
|
+
list() {
|
|
26
|
+
return {
|
|
27
|
+
items: this.locales.all().map(({ slug, name, isDefault, dir }) => ({
|
|
28
|
+
slug,
|
|
29
|
+
name,
|
|
30
|
+
isDefault: isDefault ?? false,
|
|
31
|
+
// The registry always resolves a direction; the fallback is
|
|
32
|
+
// for type flow only.
|
|
33
|
+
dir: dir ?? 'ltr'
|
|
34
|
+
}))
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
exports.ListLocalesController = ListLocalesController;
|
|
39
|
+
tslib_1.__decorate([
|
|
40
|
+
(0, common_1.Get)('locales'),
|
|
41
|
+
tslib_1.__metadata("design:type", Function),
|
|
42
|
+
tslib_1.__metadata("design:paramtypes", []),
|
|
43
|
+
tslib_1.__metadata("design:returntype", Object)
|
|
44
|
+
], ListLocalesController.prototype, "list", null);
|
|
45
|
+
exports.ListLocalesController = ListLocalesController = tslib_1.__decorate([
|
|
46
|
+
(0, common_1.Controller)('i18n'),
|
|
47
|
+
tslib_1.__metadata("design:paramtypes", [locale_registry_service_1.LocaleRegistryService])
|
|
48
|
+
], ListLocalesController);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { I18nPluginConfig, LocaleDef } from '../../types/locale';
|
|
2
|
+
/**
|
|
3
|
+
* Read access to the validated locale configuration — the runtime authority
|
|
4
|
+
* every other i18n service consults. It wraps the domain {@link LocaleSet} /
|
|
5
|
+
* {@link LocalePolicy} (rebuilt from the already-validated config), so every
|
|
6
|
+
* service resolves locales through the same domain rule; it exposes the plain
|
|
7
|
+
* {@link LocaleDef} shape its callers expect and maps the domain's
|
|
8
|
+
* {@link UnknownLocaleError} to HTTP 400.
|
|
9
|
+
*/
|
|
10
|
+
export declare class LocaleRegistryService {
|
|
11
|
+
private readonly locales;
|
|
12
|
+
private readonly policy;
|
|
13
|
+
constructor(config: I18nPluginConfig);
|
|
14
|
+
/** All configured locales, in config (display) order. */
|
|
15
|
+
all(): LocaleDef[];
|
|
16
|
+
/** One locale by slug, or undefined. */
|
|
17
|
+
get(slug: string): LocaleDef | undefined;
|
|
18
|
+
/** The default locale (exactly one exists). */
|
|
19
|
+
default(): LocaleDef;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a request's locale slug: absent → the default; unknown → 400.
|
|
22
|
+
* The uniform gate every request-facing path funnels through.
|
|
23
|
+
*/
|
|
24
|
+
resolve(slug: string | undefined): LocaleDef;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=locale-registry.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-registry.service.d.ts","sourceRoot":"","sources":["../../../../src/lib/locales/services/locale-registry.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAkBtE;;;;;;;GAOG;AACH,qBACa,qBAAqB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAY;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;gBAEL,MAAM,EAAE,gBAAgB;IAKzD,yDAAyD;IACzD,GAAG,IAAI,SAAS,EAAE;IAIlB,wCAAwC;IACxC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAKxC,+CAA+C;IAC/C,OAAO,IAAI,SAAS;IAIpB;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;CAU/C"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocaleRegistryService = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const i18n_constants_1 = require("../../i18n.constants");
|
|
7
|
+
const locale_policy_1 = require("../../domain/locale-policy");
|
|
8
|
+
const errors_1 = require("../../domain/errors");
|
|
9
|
+
const locale_set_1 = require("../../domain/value-objects/locale-set");
|
|
10
|
+
/** Maps a domain {@link Locale} back to the plain wire/config shape. */
|
|
11
|
+
function toDef(locale) {
|
|
12
|
+
return {
|
|
13
|
+
slug: locale.slug,
|
|
14
|
+
name: locale.name,
|
|
15
|
+
isDefault: locale.isDefault,
|
|
16
|
+
// Always resolved by the value object (declared or inferred), so every
|
|
17
|
+
// caller gets a direction rather than having to decide for itself.
|
|
18
|
+
dir: locale.dir
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read access to the validated locale configuration — the runtime authority
|
|
23
|
+
* every other i18n service consults. It wraps the domain {@link LocaleSet} /
|
|
24
|
+
* {@link LocalePolicy} (rebuilt from the already-validated config), so every
|
|
25
|
+
* service resolves locales through the same domain rule; it exposes the plain
|
|
26
|
+
* {@link LocaleDef} shape its callers expect and maps the domain's
|
|
27
|
+
* {@link UnknownLocaleError} to HTTP 400.
|
|
28
|
+
*/
|
|
29
|
+
let LocaleRegistryService = class LocaleRegistryService {
|
|
30
|
+
locales;
|
|
31
|
+
policy;
|
|
32
|
+
constructor(config) {
|
|
33
|
+
this.locales = locale_set_1.LocaleSet.fromDefs(config.locales);
|
|
34
|
+
this.policy = new locale_policy_1.LocalePolicy(this.locales);
|
|
35
|
+
}
|
|
36
|
+
/** All configured locales, in config (display) order. */
|
|
37
|
+
all() {
|
|
38
|
+
return this.locales.all().map(toDef);
|
|
39
|
+
}
|
|
40
|
+
/** One locale by slug, or undefined. */
|
|
41
|
+
get(slug) {
|
|
42
|
+
const locale = this.locales.get(slug);
|
|
43
|
+
return locale ? toDef(locale) : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** The default locale (exactly one exists). */
|
|
46
|
+
default() {
|
|
47
|
+
return toDef(this.locales.default());
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a request's locale slug: absent → the default; unknown → 400.
|
|
51
|
+
* The uniform gate every request-facing path funnels through.
|
|
52
|
+
*/
|
|
53
|
+
resolve(slug) {
|
|
54
|
+
try {
|
|
55
|
+
return toDef(this.policy.resolve(slug));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof errors_1.UnknownLocaleError) {
|
|
59
|
+
throw new common_1.BadRequestException(error.message);
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
exports.LocaleRegistryService = LocaleRegistryService;
|
|
66
|
+
exports.LocaleRegistryService = LocaleRegistryService = tslib_1.__decorate([
|
|
67
|
+
(0, common_1.Injectable)(),
|
|
68
|
+
tslib_1.__param(0, (0, common_1.Inject)(i18n_constants_1.I18N_CONFIG)),
|
|
69
|
+
tslib_1.__metadata("design:paramtypes", [Object])
|
|
70
|
+
], LocaleRegistryService);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type OnApplicationBootstrap } from '@nestjs/common';
|
|
2
|
+
import { type Database } from '@orthacms/database';
|
|
3
|
+
import { type ContentTypeRegistry } from '@orthacms/content-server';
|
|
4
|
+
import type { I18nPluginConfig } from '../../types/locale';
|
|
5
|
+
import { LocaleRegistryService } from './locale-registry.service';
|
|
6
|
+
/** One localized content type's rows in a slug the config no longer declares. */
|
|
7
|
+
export interface OrphanedLocaleReport {
|
|
8
|
+
/** The content type holding the rows. */
|
|
9
|
+
typeName: string;
|
|
10
|
+
/** The unconfigured locale slug. */
|
|
11
|
+
locale: string;
|
|
12
|
+
/** How many rows sit in it. */
|
|
13
|
+
rows: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Boot-time guard against the one locale change that fails silently: **removing
|
|
17
|
+
* a locale from the config while its rows still exist**.
|
|
18
|
+
*
|
|
19
|
+
* Adding a locale is safe and self-announcing — existing groups gain a missing
|
|
20
|
+
* slot, coverage reports `translated: 0`, the panel shows `entry: null`.
|
|
21
|
+
* Removing one is neither. Nothing deletes the rows and nothing migrates them,
|
|
22
|
+
* and from that moment every read path hides them: `?locale=de` becomes a 400
|
|
23
|
+
* (`resolve` no longer knows the slug), the locale panel iterates the
|
|
24
|
+
* configured set, the coverage query and the virtual filters restrict to it,
|
|
25
|
+
* and the batched summary drops them. The German content is intact, reachable
|
|
26
|
+
* only by `psql`, and the product never mentions it again — including to the
|
|
27
|
+
* person who made the change, who sees a smaller locale switcher and nothing
|
|
28
|
+
* else. `LocaleSet.remove` guards the *default* against exactly this class of
|
|
29
|
+
* mistake, but the plugin never calls it: the config is a literal array, so
|
|
30
|
+
* there is no removal operation to guard, only a diff nobody computes.
|
|
31
|
+
*
|
|
32
|
+
* This computes it, once, at boot: for every localized content type, the
|
|
33
|
+
* distinct locale slugs present that the config does not declare, with a row
|
|
34
|
+
* count. Under the default `'fail'` policy the boot aborts, which puts the
|
|
35
|
+
* decision (migrate the rows, or restore the locale) in front of whoever
|
|
36
|
+
* changed the config while it is still their change; `'warn'` logs the same
|
|
37
|
+
* report and continues, for a deployment knowingly mid-migration.
|
|
38
|
+
*
|
|
39
|
+
* The scan is one `GROUP BY locale` per localized type, on a column the
|
|
40
|
+
* `(locale_group_id, locale)` index already covers.
|
|
41
|
+
*/
|
|
42
|
+
export declare class OrphanedLocaleChecker implements OnApplicationBootstrap {
|
|
43
|
+
private readonly db;
|
|
44
|
+
private readonly registry;
|
|
45
|
+
private readonly config;
|
|
46
|
+
private readonly locales;
|
|
47
|
+
private readonly logger;
|
|
48
|
+
constructor(db: Database, registry: ContentTypeRegistry, config: I18nPluginConfig, locales: LocaleRegistryService);
|
|
49
|
+
/** @inheritdoc */
|
|
50
|
+
onApplicationBootstrap(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Rows in an unconfigured locale, per localized content type. Public so a
|
|
53
|
+
* migration script or an operator endpoint can ask the same question
|
|
54
|
+
* without re-deriving the rule.
|
|
55
|
+
*/
|
|
56
|
+
findOrphans(): Promise<OrphanedLocaleReport[]>;
|
|
57
|
+
/** The unconfigured locales present on one type, with row counts. */
|
|
58
|
+
private orphansOf;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=orphaned-locale.checker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphaned-locale.checker.d.ts","sourceRoot":"","sources":["../../../../src/lib/locales/services/orphaned-locale.checker.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,sBAAsB,EAC9B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAGH,KAAK,mBAAmB,EAC3B,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAKlE,iFAAiF;AACjF,MAAM,WAAW,oBAAoB;IACjC,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBACa,qBAAsB,YAAW,sBAAsB;IAI1C,OAAO,CAAC,QAAQ,CAAC,EAAE;IAErC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACJ,OAAO,CAAC,QAAQ,CAAC,MAAM;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO;IAP5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;gBAG1B,EAAE,EAAE,QAAQ,EAE9B,QAAQ,EAAE,mBAAmB,EACR,MAAM,EAAE,gBAAgB,EAC7C,OAAO,EAAE,qBAAqB;IAGnD,kBAAkB;IACZ,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B7C;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAUpD,qEAAqE;YACvD,SAAS;CAwB1B"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var OrphanedLocaleChecker_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.OrphanedLocaleChecker = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
8
|
+
const database_1 = require("@orthacms/database");
|
|
9
|
+
const content_server_1 = require("@orthacms/content-server");
|
|
10
|
+
const i18n_constants_1 = require("../../i18n.constants");
|
|
11
|
+
const locale_registry_service_1 = require("./locale-registry.service");
|
|
12
|
+
/**
|
|
13
|
+
* Boot-time guard against the one locale change that fails silently: **removing
|
|
14
|
+
* a locale from the config while its rows still exist**.
|
|
15
|
+
*
|
|
16
|
+
* Adding a locale is safe and self-announcing — existing groups gain a missing
|
|
17
|
+
* slot, coverage reports `translated: 0`, the panel shows `entry: null`.
|
|
18
|
+
* Removing one is neither. Nothing deletes the rows and nothing migrates them,
|
|
19
|
+
* and from that moment every read path hides them: `?locale=de` becomes a 400
|
|
20
|
+
* (`resolve` no longer knows the slug), the locale panel iterates the
|
|
21
|
+
* configured set, the coverage query and the virtual filters restrict to it,
|
|
22
|
+
* and the batched summary drops them. The German content is intact, reachable
|
|
23
|
+
* only by `psql`, and the product never mentions it again — including to the
|
|
24
|
+
* person who made the change, who sees a smaller locale switcher and nothing
|
|
25
|
+
* else. `LocaleSet.remove` guards the *default* against exactly this class of
|
|
26
|
+
* mistake, but the plugin never calls it: the config is a literal array, so
|
|
27
|
+
* there is no removal operation to guard, only a diff nobody computes.
|
|
28
|
+
*
|
|
29
|
+
* This computes it, once, at boot: for every localized content type, the
|
|
30
|
+
* distinct locale slugs present that the config does not declare, with a row
|
|
31
|
+
* count. Under the default `'fail'` policy the boot aborts, which puts the
|
|
32
|
+
* decision (migrate the rows, or restore the locale) in front of whoever
|
|
33
|
+
* changed the config while it is still their change; `'warn'` logs the same
|
|
34
|
+
* report and continues, for a deployment knowingly mid-migration.
|
|
35
|
+
*
|
|
36
|
+
* The scan is one `GROUP BY locale` per localized type, on a column the
|
|
37
|
+
* `(locale_group_id, locale)` index already covers.
|
|
38
|
+
*/
|
|
39
|
+
let OrphanedLocaleChecker = OrphanedLocaleChecker_1 = class OrphanedLocaleChecker {
|
|
40
|
+
db;
|
|
41
|
+
registry;
|
|
42
|
+
config;
|
|
43
|
+
locales;
|
|
44
|
+
logger = new common_1.Logger(OrphanedLocaleChecker_1.name);
|
|
45
|
+
constructor(db, registry, config, locales) {
|
|
46
|
+
this.db = db;
|
|
47
|
+
this.registry = registry;
|
|
48
|
+
this.config = config;
|
|
49
|
+
this.locales = locales;
|
|
50
|
+
}
|
|
51
|
+
/** @inheritdoc */
|
|
52
|
+
async onApplicationBootstrap() {
|
|
53
|
+
const orphans = await this.findOrphans();
|
|
54
|
+
if (!orphans.length)
|
|
55
|
+
return;
|
|
56
|
+
const detail = orphans
|
|
57
|
+
.map((orphan) => `${orphan.typeName}: ${orphan.rows} row(s) in "${orphan.locale}"`)
|
|
58
|
+
.join('; ');
|
|
59
|
+
const configured = this.locales
|
|
60
|
+
.all()
|
|
61
|
+
.map((locale) => locale.slug)
|
|
62
|
+
.join(', ');
|
|
63
|
+
const message = `Entry rows exist in locales this deployment no longer configures ` +
|
|
64
|
+
`(configured: ${configured}). They are invisible to every read ` +
|
|
65
|
+
`path — migrate or delete them, or restore the locale. ${detail}`;
|
|
66
|
+
if ((this.config.orphanedLocales ?? 'fail') === 'warn') {
|
|
67
|
+
this.logger.warn(message);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`${message} Set plugins.i18n.orphanedLocales to 'warn' to boot anyway.`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Rows in an unconfigured locale, per localized content type. Public so a
|
|
74
|
+
* migration script or an operator endpoint can ask the same question
|
|
75
|
+
* without re-deriving the rule.
|
|
76
|
+
*/
|
|
77
|
+
async findOrphans() {
|
|
78
|
+
const configured = this.locales.all().map((locale) => locale.slug);
|
|
79
|
+
const reports = [];
|
|
80
|
+
for (const type of this.registry.all()) {
|
|
81
|
+
if (!type.i18n)
|
|
82
|
+
continue;
|
|
83
|
+
reports.push(...(await this.orphansOf(type, configured)));
|
|
84
|
+
}
|
|
85
|
+
return reports;
|
|
86
|
+
}
|
|
87
|
+
/** The unconfigured locales present on one type, with row counts. */
|
|
88
|
+
async orphansOf(type, configured) {
|
|
89
|
+
const table = type.table;
|
|
90
|
+
// Soft-deleted rows are counted too: a restore would bring back a row
|
|
91
|
+
// in a locale nothing can show, which is the same problem deferred.
|
|
92
|
+
const rows = (await this.db
|
|
93
|
+
.select({
|
|
94
|
+
locale: (0, drizzle_orm_1.sql) `${table['locale']}`,
|
|
95
|
+
rows: (0, drizzle_orm_1.sql) `count(*)::int`
|
|
96
|
+
})
|
|
97
|
+
.from(type.table)
|
|
98
|
+
.where((0, drizzle_orm_1.notInArray)(table['locale'], configured))
|
|
99
|
+
.groupBy((0, drizzle_orm_1.sql) `${table['locale']}`));
|
|
100
|
+
return rows.map((row) => ({
|
|
101
|
+
typeName: type.name,
|
|
102
|
+
locale: row.locale,
|
|
103
|
+
rows: row.rows
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
exports.OrphanedLocaleChecker = OrphanedLocaleChecker;
|
|
108
|
+
exports.OrphanedLocaleChecker = OrphanedLocaleChecker = OrphanedLocaleChecker_1 = tslib_1.__decorate([
|
|
109
|
+
(0, common_1.Injectable)(),
|
|
110
|
+
tslib_1.__param(0, (0, database_1.InjectDatabase)()),
|
|
111
|
+
tslib_1.__param(1, (0, content_server_1.InjectContentRegistry)()),
|
|
112
|
+
tslib_1.__param(2, (0, common_1.Inject)(i18n_constants_1.I18N_CONFIG)),
|
|
113
|
+
tslib_1.__metadata("design:paramtypes", [Object, Function, Object, locale_registry_service_1.LocaleRegistryService])
|
|
114
|
+
], OrphanedLocaleChecker);
|