@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
@@ -0,0 +1,41 @@
1
+ import { Locale, type LocaleInput } from './locale';
2
+ /**
3
+ * The configured set of {@link Locale}s — the value object that owns the
4
+ * **collection invariants** no single locale can guard: a non-empty set,
5
+ * unique slugs, and **exactly one default**. Because construction guarantees a
6
+ * default member, {@link default} never returns undefined, and {@link remove}
7
+ * refuses to drop the default. Preserves config (display) order.
8
+ */
9
+ export declare class LocaleSet {
10
+ private readonly locales;
11
+ private readonly bySlug;
12
+ private readonly _default;
13
+ private constructor();
14
+ /**
15
+ * Builds a {@link LocaleSet} from already-validated {@link Locale}s,
16
+ * enforcing the collection invariants ({@link InvalidLocaleSetError} on a
17
+ * violation).
18
+ */
19
+ static create(locales: readonly Locale[]): LocaleSet;
20
+ /**
21
+ * Builds a {@link LocaleSet} from the plugin's raw config defs — validating
22
+ * each locale's format ({@link Locale.create}) then the collection. The one
23
+ * entry point the plugin factory uses, so every locale rule lives here.
24
+ */
25
+ static fromDefs(defs: readonly LocaleInput[]): LocaleSet;
26
+ /** All configured locales, in config (display) order. */
27
+ all(): Locale[];
28
+ /** One locale by slug, or undefined. */
29
+ get(slug: string): Locale | undefined;
30
+ /** Whether `slug` names a configured locale. */
31
+ has(slug: string): boolean;
32
+ /** The default locale (exactly one exists). */
33
+ default(): Locale;
34
+ /**
35
+ * The set with `slug` removed — guarding that only a **member** can be
36
+ * removed and that the **default is never removed** (either throws
37
+ * {@link InvalidLocaleSetError}).
38
+ */
39
+ remove(slug: string): LocaleSet;
40
+ }
41
+ //# sourceMappingURL=locale-set.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-set.d.ts","sourceRoot":"","sources":["../../../../src/lib/domain/value-objects/locale-set.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAEpD;;;;;;GAMG;AACH,qBAAa,SAAS;IAIE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO;IAUP;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS;IAwBpD;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,GAAG,SAAS;IAIxD,yDAAyD;IACzD,GAAG,IAAI,MAAM,EAAE;IAIf,wCAAwC;IACxC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIrC,gDAAgD;IAChD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,+CAA+C;IAC/C,OAAO,IAAI,MAAM;IAIjB;;;;OAIG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS;CAgBlC"}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocaleSet = void 0;
4
+ const errors_1 = require("../errors");
5
+ const locale_1 = require("./locale");
6
+ /**
7
+ * The configured set of {@link Locale}s — the value object that owns the
8
+ * **collection invariants** no single locale can guard: a non-empty set,
9
+ * unique slugs, and **exactly one default**. Because construction guarantees a
10
+ * default member, {@link default} never returns undefined, and {@link remove}
11
+ * refuses to drop the default. Preserves config (display) order.
12
+ */
13
+ class LocaleSet {
14
+ locales;
15
+ bySlug;
16
+ _default;
17
+ constructor(locales) {
18
+ this.locales = locales;
19
+ this.bySlug = new Map(locales.map((locale) => [locale.slug, locale]));
20
+ // `create` guarantees exactly one; the guard is for type flow.
21
+ const fallback = locales.find((locale) => locale.isDefault);
22
+ if (!fallback) {
23
+ throw new errors_1.InvalidLocaleSetError('A locale set has no default.');
24
+ }
25
+ this._default = fallback;
26
+ }
27
+ /**
28
+ * Builds a {@link LocaleSet} from already-validated {@link Locale}s,
29
+ * enforcing the collection invariants ({@link InvalidLocaleSetError} on a
30
+ * violation).
31
+ */
32
+ static create(locales) {
33
+ if (locales.length === 0) {
34
+ throw new errors_1.InvalidLocaleSetError('A locale set requires at least one locale.');
35
+ }
36
+ const seen = new Set();
37
+ for (const locale of locales) {
38
+ if (seen.has(locale.slug)) {
39
+ throw new errors_1.InvalidLocaleSetError(`Duplicate locale slug "${locale.slug}".`);
40
+ }
41
+ seen.add(locale.slug);
42
+ }
43
+ const defaults = locales.filter((locale) => locale.isDefault);
44
+ if (defaults.length !== 1) {
45
+ throw new errors_1.InvalidLocaleSetError(`Exactly one locale must set isDefault (got ${defaults.length}).`);
46
+ }
47
+ return new LocaleSet(locales);
48
+ }
49
+ /**
50
+ * Builds a {@link LocaleSet} from the plugin's raw config defs — validating
51
+ * each locale's format ({@link Locale.create}) then the collection. The one
52
+ * entry point the plugin factory uses, so every locale rule lives here.
53
+ */
54
+ static fromDefs(defs) {
55
+ return LocaleSet.create(defs.map((def) => locale_1.Locale.create(def)));
56
+ }
57
+ /** All configured locales, in config (display) order. */
58
+ all() {
59
+ return [...this.locales];
60
+ }
61
+ /** One locale by slug, or undefined. */
62
+ get(slug) {
63
+ return this.bySlug.get(slug);
64
+ }
65
+ /** Whether `slug` names a configured locale. */
66
+ has(slug) {
67
+ return this.bySlug.has(slug);
68
+ }
69
+ /** The default locale (exactly one exists). */
70
+ default() {
71
+ return this._default;
72
+ }
73
+ /**
74
+ * The set with `slug` removed — guarding that only a **member** can be
75
+ * removed and that the **default is never removed** (either throws
76
+ * {@link InvalidLocaleSetError}).
77
+ */
78
+ remove(slug) {
79
+ const target = this.bySlug.get(slug);
80
+ if (!target) {
81
+ throw new errors_1.InvalidLocaleSetError(`Locale "${slug}" is not a member.`);
82
+ }
83
+ if (target.isDefault) {
84
+ throw new errors_1.InvalidLocaleSetError(`Cannot remove the default locale "${slug}".`);
85
+ }
86
+ return LocaleSet.create(this.locales.filter((locale) => locale.slug !== slug));
87
+ }
88
+ }
89
+ exports.LocaleSet = LocaleSet;
@@ -0,0 +1,55 @@
1
+ import { type LocaleDir } from '../../i18n.constants';
2
+ /** The primitive shape a {@link Locale} is built from (the plugin's config). */
3
+ export interface LocaleInput {
4
+ /** Machine slug stored on entry rows (e.g. `en`, `de`, `pt-br`). */
5
+ slug: string;
6
+ /** Human display name (e.g. "English"). */
7
+ name: string;
8
+ /** Whether this is the default locale. */
9
+ isDefault?: boolean;
10
+ /**
11
+ * Text direction of content written in this locale. Optional in config —
12
+ * omitted, it is **inferred from the slug** (`ar`, `he`, `fa`, … → `rtl`),
13
+ * so a host adding an RTL language gets the right answer without saying so.
14
+ * Declare it to override the inference (a transliterated variant, say).
15
+ */
16
+ dir?: LocaleDir;
17
+ }
18
+ /**
19
+ * A single configured locale — the value object that owns the **slug format
20
+ * rule** (the lowercase BCP-47-ish shape that used to live inline on the
21
+ * plugin factory) and a non-blank display name. Constructing one guarantees a
22
+ * well-formed locale, so every other i18n path can handle a `Locale` instead
23
+ * of a raw string it must re-validate.
24
+ */
25
+ export declare class Locale {
26
+ private readonly _slug;
27
+ private readonly _name;
28
+ private readonly _isDefault;
29
+ private readonly _dir;
30
+ private constructor();
31
+ /**
32
+ * Builds a {@link Locale}, rejecting a malformed slug, a blank name, or a
33
+ * `dir` that is neither `ltr` nor `rtl` with {@link InvalidLocaleError}
34
+ * (the plugin factory surfaces it at construction, request paths as HTTP
35
+ * 400). An omitted `dir` is inferred from the slug rather than assumed
36
+ * `ltr`, so configuring an RTL language is one line and not a silent
37
+ * accessibility failure.
38
+ */
39
+ static create(input: LocaleInput): Locale;
40
+ /** The validated locale slug. */
41
+ get slug(): string;
42
+ /** The display name. */
43
+ get name(): string;
44
+ /** Whether this is the default locale. */
45
+ get isDefault(): boolean;
46
+ /**
47
+ * Text direction of content in this locale — always resolved (declared or
48
+ * inferred), never undefined, so a consumer can set `dir` unconditionally
49
+ * instead of guessing.
50
+ */
51
+ get dir(): LocaleDir;
52
+ /** Structural equality on the slug. */
53
+ equals(other: Locale): boolean;
54
+ }
55
+ //# sourceMappingURL=locale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale.d.ts","sourceRoot":"","sources":["../../../../src/lib/domain/value-objects/locale.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,SAAS,EACjB,MAAM,sBAAsB,CAAC;AAG9B,gFAAgF;AAChF,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,SAAS,CAAC;CACnB;AAED;;;;;;GAMG;AACH,qBAAa,MAAM;IAEX,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJzB,OAAO;IAOP;;;;;;;OAOG;IACH,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM;IA4BzC,iCAAiC;IACjC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,wBAAwB;IACxB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,0CAA0C;IAC1C,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;OAIG;IACH,IAAI,GAAG,IAAI,SAAS,CAEnB;IAED,uCAAuC;IACvC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;CAGjC"}
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Locale = void 0;
4
+ const i18n_constants_1 = require("../../i18n.constants");
5
+ const errors_1 = require("../errors");
6
+ /**
7
+ * A single configured locale — the value object that owns the **slug format
8
+ * rule** (the lowercase BCP-47-ish shape that used to live inline on the
9
+ * plugin factory) and a non-blank display name. Constructing one guarantees a
10
+ * well-formed locale, so every other i18n path can handle a `Locale` instead
11
+ * of a raw string it must re-validate.
12
+ */
13
+ class Locale {
14
+ _slug;
15
+ _name;
16
+ _isDefault;
17
+ _dir;
18
+ constructor(_slug, _name, _isDefault, _dir) {
19
+ this._slug = _slug;
20
+ this._name = _name;
21
+ this._isDefault = _isDefault;
22
+ this._dir = _dir;
23
+ }
24
+ /**
25
+ * Builds a {@link Locale}, rejecting a malformed slug, a blank name, or a
26
+ * `dir` that is neither `ltr` nor `rtl` with {@link InvalidLocaleError}
27
+ * (the plugin factory surfaces it at construction, request paths as HTTP
28
+ * 400). An omitted `dir` is inferred from the slug rather than assumed
29
+ * `ltr`, so configuring an RTL language is one line and not a silent
30
+ * accessibility failure.
31
+ */
32
+ static create(input) {
33
+ const { slug, name, isDefault, dir } = input;
34
+ if (!i18n_constants_1.LOCALE_SLUG_RE.test(slug) ||
35
+ slug.length > i18n_constants_1.LOCALE_SLUG_MAX_LENGTH) {
36
+ throw new errors_1.InvalidLocaleError(`Locale slug "${slug}" is invalid — lowercase 2-3 letter ` +
37
+ `primary tag with optional "-" subtags (e.g. "en", "pt-br").`);
38
+ }
39
+ if (!name.trim()) {
40
+ throw new errors_1.InvalidLocaleError(`Locale "${slug}" has an empty name.`);
41
+ }
42
+ if (dir !== undefined && !i18n_constants_1.LOCALE_DIR.includes(dir)) {
43
+ throw new errors_1.InvalidLocaleError(`Locale "${slug}" has an invalid dir "${dir}" — ` +
44
+ `expected ${i18n_constants_1.LOCALE_DIR.map((d) => `"${d}"`).join(' or ')}.`);
45
+ }
46
+ return new Locale(slug, name, isDefault ?? false, dir ?? (0, i18n_constants_1.inferLocaleDir)(slug));
47
+ }
48
+ /** The validated locale slug. */
49
+ get slug() {
50
+ return this._slug;
51
+ }
52
+ /** The display name. */
53
+ get name() {
54
+ return this._name;
55
+ }
56
+ /** Whether this is the default locale. */
57
+ get isDefault() {
58
+ return this._isDefault;
59
+ }
60
+ /**
61
+ * Text direction of content in this locale — always resolved (declared or
62
+ * inferred), never undefined, so a consumer can set `dir` unconditionally
63
+ * instead of guessing.
64
+ */
65
+ get dir() {
66
+ return this._dir;
67
+ }
68
+ /** Structural equality on the slug. */
69
+ equals(other) {
70
+ return this._slug === other._slug;
71
+ }
72
+ }
73
+ exports.Locale = Locale;
@@ -0,0 +1,33 @@
1
+ /** Shared constants of the i18n server plugin. */
2
+ /**
3
+ * Valid locale slugs: lowercase 2–3 letter primary tag, optional `-`-joined
4
+ * alphanumeric subtags (`en`, `de`, `pt-br`, `zh-hans`).
5
+ */
6
+ export declare const LOCALE_SLUG_RE: RegExp;
7
+ /** Longest accepted locale slug (mirrors BCP-47's practical bound). */
8
+ export declare const LOCALE_SLUG_MAX_LENGTH = 35;
9
+ /** DI token for the validated {@link I18nPluginConfig}. */
10
+ export declare const I18N_CONFIG: unique symbol;
11
+ /** Cap on `groupIds` per locale-summary batch request. */
12
+ export declare const LOCALE_SUMMARY_MAX_GROUPS = 100;
13
+ /** The `localeFallback` value that widens a list to the default locale. */
14
+ export declare const LOCALE_FALLBACK_DEFAULT = "default";
15
+ /**
16
+ * The two text directions a locale can declare — the values of the HTML `dir`
17
+ * attribute a consumer sets on the editor surface, the preview, and the
18
+ * published page (WCAG 1.3.2 Meaningful Sequence, 3.1.2 Language of Parts).
19
+ */
20
+ export declare const LOCALE_DIR: readonly ["ltr", "rtl"];
21
+ /** A configured locale's text direction. */
22
+ export type LocaleDir = (typeof LOCALE_DIR)[number];
23
+ /**
24
+ * The text direction implied by a locale slug, used when the host's config
25
+ * declares none — so adding `{ slug: 'ar', name: 'العربية' }` yields an RTL
26
+ * locale without the host having to know it, while an explicit `dir` always
27
+ * wins.
28
+ *
29
+ * An explicit script subtag decides first (`az-arab` → `rtl`, `sr-latn` →
30
+ * `ltr`); otherwise the primary language subtag does; otherwise `ltr`.
31
+ */
32
+ export declare function inferLocaleDir(slug: string): LocaleDir;
33
+ //# sourceMappingURL=i18n.constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.constants.d.ts","sourceRoot":"","sources":["../../src/lib/i18n.constants.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAElD;;;GAGG;AACH,eAAO,MAAM,cAAc,QAA8B,CAAC;AAE1D,uEAAuE;AACvE,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,2DAA2D;AAC3D,eAAO,MAAM,WAAW,eAAwB,CAAC;AAEjD,0DAA0D;AAC1D,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,2EAA2E;AAC3E,eAAO,MAAM,uBAAuB,YAAY,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,UAAU,yBAA0B,CAAC;AAElD,4CAA4C;AAC5C,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAmDpD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAOtD"}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /** Shared constants of the i18n server plugin. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.LOCALE_DIR = exports.LOCALE_FALLBACK_DEFAULT = exports.LOCALE_SUMMARY_MAX_GROUPS = exports.I18N_CONFIG = exports.LOCALE_SLUG_MAX_LENGTH = exports.LOCALE_SLUG_RE = void 0;
5
+ exports.inferLocaleDir = inferLocaleDir;
6
+ /**
7
+ * Valid locale slugs: lowercase 2–3 letter primary tag, optional `-`-joined
8
+ * alphanumeric subtags (`en`, `de`, `pt-br`, `zh-hans`).
9
+ */
10
+ exports.LOCALE_SLUG_RE = /^[a-z]{2,3}(-[a-z0-9]+)*$/;
11
+ /** Longest accepted locale slug (mirrors BCP-47's practical bound). */
12
+ exports.LOCALE_SLUG_MAX_LENGTH = 35;
13
+ /** DI token for the validated {@link I18nPluginConfig}. */
14
+ exports.I18N_CONFIG = Symbol('I18N_CONFIG');
15
+ /** Cap on `groupIds` per locale-summary batch request. */
16
+ exports.LOCALE_SUMMARY_MAX_GROUPS = 100;
17
+ /** The `localeFallback` value that widens a list to the default locale. */
18
+ exports.LOCALE_FALLBACK_DEFAULT = 'default';
19
+ /**
20
+ * The two text directions a locale can declare — the values of the HTML `dir`
21
+ * attribute a consumer sets on the editor surface, the preview, and the
22
+ * published page (WCAG 1.3.2 Meaningful Sequence, 3.1.2 Language of Parts).
23
+ */
24
+ exports.LOCALE_DIR = ['ltr', 'rtl'];
25
+ /**
26
+ * Script subtags that decide direction outright, whichever language carries
27
+ * them — `sr-latn` is LTR and `az-arab` is RTL regardless of what the language
28
+ * alone would imply. Checked before {@link RTL_LANGUAGE_SUBTAGS}, since an
29
+ * explicit script is the more specific statement.
30
+ */
31
+ const RTL_SCRIPT_SUBTAGS = new Set([
32
+ 'adlm',
33
+ 'arab',
34
+ 'aran',
35
+ 'hebr',
36
+ 'nkoo',
37
+ 'rohg',
38
+ 'syrc',
39
+ 'thaa',
40
+ 'yezi'
41
+ ]);
42
+ /** Script subtags that force LTR even on a language written RTL elsewhere. */
43
+ const LTR_SCRIPT_SUBTAGS = new Set([
44
+ 'cyrl',
45
+ 'latn',
46
+ 'hans',
47
+ 'hant'
48
+ ]);
49
+ /**
50
+ * Primary language subtags written right-to-left. Deliberately a short,
51
+ * conservative list of the languages a CMS is realistically configured with,
52
+ * not a mirror of CLDR: it only supplies the **default** for a locale that
53
+ * declares no `dir`, and a host with an exotic script says so explicitly.
54
+ */
55
+ const RTL_LANGUAGE_SUBTAGS = new Set([
56
+ 'ar', // Arabic
57
+ 'arc', // Aramaic
58
+ 'ckb', // Central Kurdish
59
+ 'dv', // Divehi
60
+ 'fa', // Persian
61
+ 'he', // Hebrew
62
+ 'ks', // Kashmiri
63
+ 'ku', // Kurdish
64
+ 'nqo', // N'Ko
65
+ 'ps', // Pashto
66
+ 'sd', // Sindhi
67
+ 'ug', // Uyghur
68
+ 'ur', // Urdu
69
+ 'yi' // Yiddish
70
+ ]);
71
+ /**
72
+ * The text direction implied by a locale slug, used when the host's config
73
+ * declares none — so adding `{ slug: 'ar', name: 'العربية' }` yields an RTL
74
+ * locale without the host having to know it, while an explicit `dir` always
75
+ * wins.
76
+ *
77
+ * An explicit script subtag decides first (`az-arab` → `rtl`, `sr-latn` →
78
+ * `ltr`); otherwise the primary language subtag does; otherwise `ltr`.
79
+ */
80
+ function inferLocaleDir(slug) {
81
+ const parts = slug.toLowerCase().split('-');
82
+ for (const part of parts.slice(1)) {
83
+ if (RTL_SCRIPT_SUBTAGS.has(part))
84
+ return 'rtl';
85
+ if (LTR_SCRIPT_SUBTAGS.has(part))
86
+ return 'ltr';
87
+ }
88
+ return RTL_LANGUAGE_SUBTAGS.has(parts[0]) ? 'rtl' : 'ltr';
89
+ }
@@ -0,0 +1,14 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import type { I18nPluginConfig } from './types/locale';
3
+ /**
4
+ * NestJS module of the i18n plugin. Registered **global** so its binding of
5
+ * content-server's `CONTENT_ENTRY_EXTENSION` port resolves inside the
6
+ * (equally global) content module without an explicit import — the same
7
+ * inversion as content binding identity's `CONTENT_CATALOG`, roles swapped:
8
+ * content declares the port, this plugin provides the implementation.
9
+ */
10
+ export declare class I18nModule {
11
+ /** Creates the global dynamic module around a validated config. */
12
+ static forRoot(config: I18nPluginConfig): DynamicModule;
13
+ }
14
+ //# sourceMappingURL=i18n.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.module.d.ts","sourceRoot":"","sources":["../../src/lib/i18n.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAU,MAAM,gBAAgB,CAAC;AAIvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAiBvD;;;;;;GAMG;AACH,qBACa,UAAU;IACnB,mEAAmE;IACnE,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,gBAAgB,GAAG,aAAa;CAmD1D"}
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var I18nModule_1;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.I18nModule = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const common_1 = require("@nestjs/common");
7
+ const content_server_1 = require("@orthacms/content-server");
8
+ const copilot_server_1 = require("@orthacms/copilot-server");
9
+ const i18n_constants_1 = require("./i18n.constants");
10
+ const locale_registry_service_1 = require("./locales/services/locale-registry.service");
11
+ const orphaned_locale_checker_1 = require("./locales/services/orphaned-locale.checker");
12
+ const list_locales_controller_1 = require("./locales/controllers/list-locales.controller");
13
+ const entry_locale_extension_service_1 = require("./content/services/entry-locale-extension.service");
14
+ const locale_group_service_1 = require("./content/services/locale-group.service");
15
+ const get_entry_locales_controller_1 = require("./content/controllers/get-entry-locales.controller");
16
+ const locale_summary_controller_1 = require("./content/controllers/locale-summary.controller");
17
+ const localization_coverage_query_1 = require("./insights/infrastructure/queries/localization-coverage.query");
18
+ const localization_coverage_controller_1 = require("./insights/http/controllers/localization-coverage.controller");
19
+ const i18n_tool_provider_1 = require("./copilot/i18n-tool.provider");
20
+ const translation_proposal_provider_1 = require("./copilot/translation-proposal.provider");
21
+ const translation_proposal_applier_1 = require("./copilot/translation-proposal.applier");
22
+ /**
23
+ * NestJS module of the i18n plugin. Registered **global** so its binding of
24
+ * content-server's `CONTENT_ENTRY_EXTENSION` port resolves inside the
25
+ * (equally global) content module without an explicit import — the same
26
+ * inversion as content binding identity's `CONTENT_CATALOG`, roles swapped:
27
+ * content declares the port, this plugin provides the implementation.
28
+ */
29
+ let I18nModule = I18nModule_1 = class I18nModule {
30
+ /** Creates the global dynamic module around a validated config. */
31
+ static forRoot(config) {
32
+ return {
33
+ module: I18nModule_1,
34
+ global: true,
35
+ controllers: [
36
+ list_locales_controller_1.ListLocalesController,
37
+ // Reads only: the per-entry locale panel and the records
38
+ // table's batched group summary. Sibling *creation* goes
39
+ // through `POST /api/content/:type` with a `localeGroupId`
40
+ // (the extension stamps the group), so this plugin owns no
41
+ // entry-write route.
42
+ get_entry_locales_controller_1.GetEntryLocalesController,
43
+ locale_summary_controller_1.LocaleSummaryController,
44
+ // The Insights read-model. Mounted under `insights/` with
45
+ // content's and media's, not this plugin's `i18n/` prefix —
46
+ // one card, one endpoint, grouped by what they are.
47
+ localization_coverage_controller_1.LocalizationCoverageController
48
+ ],
49
+ providers: [
50
+ { provide: i18n_constants_1.I18N_CONFIG, useValue: config },
51
+ locale_registry_service_1.LocaleRegistryService,
52
+ // Boot-time diff of the configured locale set against what the
53
+ // data actually holds — the one locale change (a removal) that
54
+ // otherwise fails silently.
55
+ orphaned_locale_checker_1.OrphanedLocaleChecker,
56
+ locale_group_service_1.LocaleGroupService,
57
+ localization_coverage_query_1.LocalizationCoverageQuery,
58
+ entry_locale_extension_service_1.EntryLocaleExtensionService,
59
+ {
60
+ provide: content_server_1.CONTENT_ENTRY_EXTENSION,
61
+ useExisting: entry_locale_extension_service_1.EntryLocaleExtensionService
62
+ },
63
+ // The copilot's locale tools. Both no-op when no copilot
64
+ // plugin is registered — the registrar injects the registry
65
+ // optionally.
66
+ i18n_tool_provider_1.I18nCopilotToolProvider,
67
+ translation_proposal_provider_1.TranslationProposalToolProvider,
68
+ // The appliers for the kinds those propose tools produce —
69
+ // one per kind, next to them on purpose: a missing applier
70
+ // surfaces only when a change is carried out.
71
+ translation_proposal_applier_1.TranslationProposalApplier,
72
+ translation_proposal_applier_1.BulkTranslationProposalApplier,
73
+ (0, copilot_server_1.copilotAppliersRegistrar)('i18n', translation_proposal_applier_1.TranslationProposalApplier, translation_proposal_applier_1.BulkTranslationProposalApplier)
74
+ ],
75
+ exports: [content_server_1.CONTENT_ENTRY_EXTENSION, locale_registry_service_1.LocaleRegistryService]
76
+ };
77
+ }
78
+ };
79
+ exports.I18nModule = I18nModule;
80
+ exports.I18nModule = I18nModule = I18nModule_1 = tslib_1.__decorate([
81
+ (0, common_1.Module)({})
82
+ ], I18nModule);
@@ -0,0 +1,23 @@
1
+ import { LocalizationCoverageQuery } from '../../infrastructure/queries/localization-coverage.query';
2
+ import type { I18nCoverageView } from '../../types/i18n-insights-view';
3
+ /**
4
+ * `GET /api/insights/i18n/coverage` — how much of the workspace's localized
5
+ * content exists in each configured locale, and how many records are fully
6
+ * translated, untranslated, or part-way.
7
+ *
8
+ * Mounted under `insights/` beside content's and media's widgets rather than
9
+ * under this plugin's own `i18n/` prefix: the Insights page's endpoints are
10
+ * grouped by what they are, and a reader looking for the card's data should
11
+ * find it next to the other cards'.
12
+ *
13
+ * Gated on `content:read` — it counts content, and a reader who may not see
14
+ * entries must not learn how many there are by counting the gaps. Takes no
15
+ * `?days=`: an untranslated record is untranslated regardless of when it was
16
+ * written, so a window could only hide part of the backlog.
17
+ */
18
+ export declare class LocalizationCoverageController {
19
+ private readonly coverage;
20
+ constructor(coverage: LocalizationCoverageQuery);
21
+ localization(workspaceId: string): Promise<I18nCoverageView>;
22
+ }
23
+ //# sourceMappingURL=localization-coverage.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localization-coverage.controller.d.ts","sourceRoot":"","sources":["../../../../../src/lib/insights/http/controllers/localization-coverage.controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,yBAAyB,EAAE,MAAM,0DAA0D,CAAC;AACrG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAEvE;;;;;;;;;;;;;;GAcG;AACH,qBAGa,8BAA8B;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,yBAAyB;IAGhE,YAAY,CACY,WAAW,EAAE,MAAM,GACxC,OAAO,CAAC,gBAAgB,CAAC;CAG/B"}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalizationCoverageController = 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 localization_coverage_query_1 = require("../../infrastructure/queries/localization-coverage.query");
9
+ /**
10
+ * `GET /api/insights/i18n/coverage` — how much of the workspace's localized
11
+ * content exists in each configured locale, and how many records are fully
12
+ * translated, untranslated, or part-way.
13
+ *
14
+ * Mounted under `insights/` beside content's and media's widgets rather than
15
+ * under this plugin's own `i18n/` prefix: the Insights page's endpoints are
16
+ * grouped by what they are, and a reader looking for the card's data should
17
+ * find it next to the other cards'.
18
+ *
19
+ * Gated on `content:read` — it counts content, and a reader who may not see
20
+ * entries must not learn how many there are by counting the gaps. Takes no
21
+ * `?days=`: an untranslated record is untranslated regardless of when it was
22
+ * written, so a window could only hide part of the backlog.
23
+ */
24
+ let LocalizationCoverageController = class LocalizationCoverageController {
25
+ coverage;
26
+ constructor(coverage) {
27
+ this.coverage = coverage;
28
+ }
29
+ localization(workspaceId) {
30
+ return this.coverage.coverage(workspaceId);
31
+ }
32
+ };
33
+ exports.LocalizationCoverageController = LocalizationCoverageController;
34
+ tslib_1.__decorate([
35
+ (0, common_1.Get)('coverage'),
36
+ tslib_1.__param(0, (0, workspaces_server_1.CurrentWorkspace)()),
37
+ tslib_1.__metadata("design:type", Function),
38
+ tslib_1.__metadata("design:paramtypes", [String]),
39
+ tslib_1.__metadata("design:returntype", Promise)
40
+ ], LocalizationCoverageController.prototype, "localization", null);
41
+ exports.LocalizationCoverageController = LocalizationCoverageController = tslib_1.__decorate([
42
+ (0, common_1.UseGuards)(identity_server_1.PermissionsGuard, workspaces_server_1.WorkspaceGuard),
43
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.CONTENT_READ),
44
+ (0, common_1.Controller)('insights/i18n'),
45
+ tslib_1.__metadata("design:paramtypes", [localization_coverage_query_1.LocalizationCoverageQuery])
46
+ ], LocalizationCoverageController);
@@ -0,0 +1,59 @@
1
+ import { type Database } from '@orthacms/database';
2
+ import type { ContentTypeRegistry } from '@orthacms/content-server';
3
+ import { LocaleRegistryService } from '../../../locales/services/locale-registry.service';
4
+ import type { I18nCoverageView } from '../../types/i18n-insights-view';
5
+ /**
6
+ * Live read-model for the localization coverage widget.
7
+ *
8
+ * **This lives in i18n-server, not content-server, and that is not arbitrary.**
9
+ * Coverage is a question about a set the content plugin deliberately does not
10
+ * know: the *configured* locales. content-server owns the `locale` column's
11
+ * shape and nothing about what a locale means, so it can report which slugs
12
+ * happen to appear in the data but not which ones are missing — and "missing"
13
+ * is the entire widget. `LocaleRegistryService` is the source of that set, and
14
+ * it is here.
15
+ *
16
+ * Like content's own insights query this aggregates **on demand**, per type,
17
+ * with no projection: a localized collection is its own generated
18
+ * `content_<name>` table, so a workspace-wide answer is inherently a fan-out.
19
+ */
20
+ export declare class LocalizationCoverageQuery {
21
+ private readonly db;
22
+ private readonly registry;
23
+ private readonly locales;
24
+ constructor(db: Database, registry: ContentTypeRegistry, locales: LocaleRegistryService);
25
+ /** Coverage across every localized content type in the workspace. */
26
+ coverage(workspaceId: string): Promise<I18nCoverageView>;
27
+ /**
28
+ * Records holding a row in each locale, for one type.
29
+ *
30
+ * `count(distinct locale_group_id)` rather than `count(*)`: the
31
+ * `(locale_group_id, locale)` unique index makes those equal in a healthy
32
+ * table, and the distinct keeps the figure honest against a paranoid type
33
+ * where the index is partial and a trashed row could otherwise be counted
34
+ * twice.
35
+ */
36
+ private perLocale;
37
+ /**
38
+ * How many translation groups this type has, how many are complete, and how
39
+ * many sit in a single language.
40
+ *
41
+ * One grouped subquery folded by an outer aggregate — the alternative,
42
+ * reading one row per group and counting in JS, would pull the workspace's
43
+ * whole content set over the wire to produce three integers.
44
+ */
45
+ private groupSpread;
46
+ /**
47
+ * Workspace scope, the soft-delete guard, **and** the configured locale set.
48
+ *
49
+ * The locale filter is what makes "complete" mean what it says. A row in a
50
+ * slug the host has since removed from its config is not coverage of
51
+ * anything, and left in it would push a group's distinct-locale count past
52
+ * the configured total — so a record could be over-covered and still not
53
+ * counted as complete. Filtering here also drops a group made entirely of
54
+ * such rows, which is the right answer rather than a record with zero
55
+ * languages.
56
+ */
57
+ private configuredScope;
58
+ }
59
+ //# sourceMappingURL=localization-coverage.query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localization-coverage.query.d.ts","sourceRoot":"","sources":["../../../../../src/lib/insights/infrastructure/queries/localization-coverage.query.ts"],"names":[],"mappings":"AAWA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,KAAK,EAER,mBAAmB,EACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,qBAAqB,EAAE,MAAM,mDAAmD,CAAC;AAC1F,OAAO,KAAK,EAER,gBAAgB,EAEnB,MAAM,gCAAgC,CAAC;AAUxC;;;;;;;;;;;;;;GAcG;AACH,qBACa,yBAAyB;IAEZ,OAAO,CAAC,QAAQ,CAAC,EAAE;IACZ,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAFW,EAAE,EAAE,QAAQ,EACL,QAAQ,EAAE,mBAAmB,EACtD,OAAO,EAAE,qBAAqB;IAGnD,qEAAqE;IAC/D,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAwE9D;;;;;;;;OAQG;YACW,SAAS;IAqBvB;;;;;;;OAOG;YACW,WAAW;IAwCzB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,eAAe;CAY1B"}