intor-cli 0.0.8 → 0.0.10

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 (76) hide show
  1. package/README.md +24 -8
  2. package/package.json +4 -4
  3. package/src/build/build-schemas/build-schemas.ts +2 -2
  4. package/src/build/build-types/utils/render-infer-node.ts +1 -1
  5. package/src/build/index.ts +1 -1
  6. package/src/build/types.ts +4 -3
  7. package/src/cli/commands/check.ts +9 -26
  8. package/src/cli/commands/generate.ts +2 -13
  9. package/src/cli/commands/utils/normalize-reader-options.ts +46 -0
  10. package/src/cli/commands/validate.ts +50 -0
  11. package/src/cli/index.ts +2 -0
  12. package/src/core/collect-messages/collect-other-locale-messages.ts +27 -0
  13. package/src/core/collect-messages/index.ts +1 -0
  14. package/src/core/constants/generated.ts +5 -0
  15. package/src/core/constants/index.ts +4 -3
  16. package/src/core/extract-usages/index.ts +1 -1
  17. package/src/core/generated/ensure-and-write.ts +11 -0
  18. package/src/core/generated/index.ts +6 -0
  19. package/src/core/generated/read-schema.ts +33 -0
  20. package/src/core/generated/write-messages-snapshot.ts +27 -0
  21. package/src/core/generated/write-schema.ts +9 -0
  22. package/src/core/generated/write-types.ts +8 -0
  23. package/src/core/index.ts +36 -9
  24. package/src/core/infer-schema/index.ts +2 -1
  25. package/src/core/infer-schema/infer-schemas.ts +5 -1
  26. package/src/core/infer-schema/messages/infer-messages-schema.ts +1 -1
  27. package/src/core/infer-schema/replacements/infer-replacements-schema.ts +1 -1
  28. package/src/core/infer-schema/rich/infer-rich-schema.ts +1 -1
  29. package/src/core/infer-schema/utils/infer-object.ts +8 -6
  30. package/src/core/infer-schema/utils/should-skip-key.ts +15 -0
  31. package/src/core/infer-schema/utils/strip-internal-keys.ts +30 -0
  32. package/src/features/check/check.ts +7 -15
  33. package/src/{core → features/check}/diagnostics/collect.ts +1 -2
  34. package/src/{core → features/check}/diagnostics/rules/enforce-missing-replacements.ts +1 -2
  35. package/src/{core → features/check}/diagnostics/rules/enforce-missing-rich.ts +1 -2
  36. package/src/{core → features/check}/diagnostics/rules/key/empty.ts +1 -1
  37. package/src/{core → features/check}/diagnostics/rules/key/not-found.ts +1 -2
  38. package/src/{core → features/check}/diagnostics/rules/pre-key/not-found.ts +1 -2
  39. package/src/{core → features/check}/diagnostics/rules/replacement/missing.ts +1 -2
  40. package/src/{core → features/check}/diagnostics/rules/replacement/not-allowed.ts +1 -2
  41. package/src/{core → features/check}/diagnostics/rules/replacement/unused.ts +1 -2
  42. package/src/{core → features/check}/diagnostics/rules/rich/missing.ts +1 -2
  43. package/src/{core → features/check}/diagnostics/rules/rich/not-allowed.ts +1 -2
  44. package/src/{core → features/check}/diagnostics/rules/rich/unused.ts +1 -2
  45. package/src/{core → features/check}/diagnostics/types.ts +1 -1
  46. package/src/{core → features/check}/diagnostics/utils/get-schema-node-at-path.ts +1 -1
  47. package/src/{core → features/check}/diagnostics/utils/index-usages-by-key.ts +1 -1
  48. package/src/features/check/print-summary.ts +15 -15
  49. package/src/features/generate/generate.ts +16 -8
  50. package/src/features/generate/print-configs.ts +3 -3
  51. package/src/features/generate/print-overrides.ts +44 -26
  52. package/src/features/generate/print-summary.ts +21 -13
  53. package/src/features/index.ts +1 -0
  54. package/src/features/print.ts +43 -0
  55. package/src/features/validate/index.ts +1 -0
  56. package/src/features/validate/messages/index.ts +1 -0
  57. package/src/features/validate/messages/validate-messages-schema.ts +36 -0
  58. package/src/features/validate/print-summary.ts +65 -0
  59. package/src/features/validate/replacements/index.ts +1 -0
  60. package/src/features/validate/replacements/validate-replacements-schema.ts +51 -0
  61. package/src/features/validate/rich/index.ts +1 -0
  62. package/src/features/validate/rich/validate-rich-schema.ts +48 -0
  63. package/src/features/validate/validate-locale-messages.ts +38 -0
  64. package/src/features/validate/validate.ts +84 -0
  65. package/src/core/constants/generated-files.ts +0 -3
  66. package/src/core/read-generated-schema.ts +0 -42
  67. package/src/core/write-generated-files.ts +0 -51
  68. package/src/features/print-title.ts +0 -7
  69. /package/src/{core → features/check}/diagnostics/group.ts +0 -0
  70. /package/src/{core → features/check}/diagnostics/index.ts +0 -0
  71. /package/src/{core → features/check}/diagnostics/messages.ts +0 -0
  72. /package/src/{core → features/check}/diagnostics/rules/key/index.ts +0 -0
  73. /package/src/{core → features/check}/diagnostics/rules/pre-key/index.ts +0 -0
  74. /package/src/{core → features/check}/diagnostics/rules/replacement/index.ts +0 -0
  75. /package/src/{core → features/check}/diagnostics/rules/rich/index.ts +0 -0
  76. /package/src/{core → features/check}/diagnostics/utils/resolve-key-path.ts +0 -0
@@ -0,0 +1,51 @@
1
+ import type { InferNode } from "../../../core";
2
+ import type { ValidationResult } from "../validate-locale-messages";
3
+ import type { MessageObject } from "intor";
4
+ import { extractInterpolationNames } from "../../../core";
5
+
6
+ export function validateReplacementsSchema(
7
+ schema: InferNode,
8
+ messages: MessageObject,
9
+ path: string,
10
+ result: ValidationResult,
11
+ ) {
12
+ // Only object schemas define required message keys
13
+ if (schema.kind !== "object") return;
14
+
15
+ // Traverse schema-defined keys and validate message presence
16
+ for (const key of Object.keys(schema.properties)) {
17
+ const nextPath = path ? `${path}.${key}` : key;
18
+ const expected = schema.properties[key];
19
+ const value = messages[key];
20
+
21
+ // Schema requires this key, but message does not provide it
22
+ if (value === undefined) continue;
23
+
24
+ // ------------------------------------------------------------
25
+ // Leaf string message: validate interpolation replacements
26
+ // ------------------------------------------------------------
27
+ if (typeof value === "string") {
28
+ if (expected.kind !== "object") continue;
29
+
30
+ const actualNames = extractInterpolationNames(value);
31
+
32
+ // Report any schema-required replacements missing in the message
33
+ for (const name of Object.keys(expected.properties)) {
34
+ if (actualNames.includes(name)) continue;
35
+ result.missingReplacements.push({ key: nextPath, name });
36
+ }
37
+
38
+ continue;
39
+ }
40
+
41
+ // Recurse into nested message objects
42
+ if (typeof value === "object" && value !== null) {
43
+ validateReplacementsSchema(
44
+ expected,
45
+ value as MessageObject,
46
+ nextPath,
47
+ result,
48
+ );
49
+ }
50
+ }
51
+ }
@@ -0,0 +1 @@
1
+ export { validateRichSchema } from "./validate-rich-schema";
@@ -0,0 +1,48 @@
1
+ import type { InferNode } from "../../../core";
2
+ import type { ValidationResult } from "../validate-locale-messages";
3
+ import { tokenize, type MessageObject, type Token } from "intor";
4
+
5
+ export function validateRichSchema(
6
+ schema: InferNode,
7
+ messages: MessageObject,
8
+ path: string,
9
+ result: ValidationResult,
10
+ ) {
11
+ // Only object schemas define expected rich tags
12
+ if (schema.kind !== "object") return;
13
+
14
+ // Traverse schema-defined keys
15
+ for (const key of Object.keys(schema.properties)) {
16
+ const nextPath = path ? `${path}.${key}` : key;
17
+ const expected = schema.properties[key];
18
+ const value = messages[key];
19
+
20
+ // Schema requires this key, but message does not provide it
21
+ if (value === undefined) continue;
22
+
23
+ // --------------------------------------------------
24
+ // Leaf string message: validate rich tags
25
+ // --------------------------------------------------
26
+ if (typeof value === "string") {
27
+ if (expected.kind !== "object") continue;
28
+
29
+ const tokens: Token[] = tokenize(value);
30
+ const actualTags = new Set(
31
+ tokens.filter((t) => t.type === "tag-open").map((t) => t.name),
32
+ );
33
+
34
+ // Report any schema-required rich tags missing in the message
35
+ for (const tag of Object.keys(expected.properties)) {
36
+ if (actualTags.has(tag)) continue;
37
+ result.missingRichTags.push({ key: nextPath, tag });
38
+ }
39
+
40
+ continue;
41
+ }
42
+
43
+ // Recurse into nested message objects
44
+ if (typeof value === "object" && value !== null) {
45
+ validateRichSchema(expected, value as MessageObject, nextPath, result);
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,38 @@
1
+ import type { InferredSchemas } from "../../core";
2
+ import type { MessageObject } from "intor";
3
+ import { validateMessagesSchema } from "./messages";
4
+ import { validateReplacementsSchema } from "./replacements";
5
+ import { validateRichSchema } from "./rich";
6
+
7
+ export interface ValidationResult {
8
+ missingKeys: string[];
9
+ missingReplacements: Array<{ key: string; name: string }>;
10
+ missingRichTags: Array<{ key: string; tag: string }>;
11
+ }
12
+
13
+ /**
14
+ * Validate a locale's messages against inferred semantic schemas.
15
+ */
16
+ export function validateLocaleMessages(
17
+ schemas: InferredSchemas,
18
+ localeMessages: MessageObject,
19
+ ): ValidationResult {
20
+ const result: ValidationResult = {
21
+ missingKeys: [],
22
+ missingReplacements: [],
23
+ missingRichTags: [],
24
+ };
25
+
26
+ validateMessagesSchema(schemas.messagesSchema, localeMessages, "", result);
27
+
28
+ validateReplacementsSchema(
29
+ schemas.replacementsSchema,
30
+ localeMessages,
31
+ "",
32
+ result,
33
+ );
34
+
35
+ validateRichSchema(schemas.richSchema, localeMessages, "", result);
36
+
37
+ return result;
38
+ }
@@ -0,0 +1,84 @@
1
+ /* eslint-disable unicorn/no-process-exit */
2
+ import { discoverConfigs, readSchema, type ExtraExt } from "../../core";
3
+ import { collectOtherLocaleMessages } from "../../core";
4
+ import { printMissingSchema, printTitle } from "../print";
5
+ import { spinner } from "../spinner";
6
+ import { printSummary } from "./print-summary";
7
+ import {
8
+ validateLocaleMessages,
9
+ type ValidationResult,
10
+ } from "./validate-locale-messages";
11
+
12
+ export interface ValidateOptions {
13
+ exts?: Array<ExtraExt>;
14
+ customReaders?: Record<string, string>;
15
+ debug?: boolean;
16
+ }
17
+
18
+ export async function validate({
19
+ exts = [],
20
+ customReaders,
21
+ debug,
22
+ }: ValidateOptions) {
23
+ printTitle("Validating intor translations");
24
+ spinner.start();
25
+
26
+ // -----------------------------------------------------------------------
27
+ // Discover configs from the current workspace
28
+ // -----------------------------------------------------------------------
29
+ const configEntries = await discoverConfigs(debug);
30
+ if (configEntries.length === 0) {
31
+ spinner.stop();
32
+ throw new Error("No Intor config found.");
33
+ }
34
+
35
+ try {
36
+ // -----------------------------------------------------------------------
37
+ // Read generated schema
38
+ // -----------------------------------------------------------------------
39
+ const schema = await readSchema();
40
+
41
+ const resultsByConfig: Record<
42
+ string,
43
+ Record<string, ValidationResult>
44
+ > = {};
45
+
46
+ // Per-config processing
47
+ for (const { config } of configEntries) {
48
+ const schemaConfig = schema.configs.find((c) => c.id === config.id);
49
+ if (!schemaConfig) {
50
+ spinner.stop();
51
+ printMissingSchema(config.id);
52
+ spinner.start();
53
+ continue;
54
+ }
55
+
56
+ // Load all non-default locale messages
57
+ const localeMessages = await collectOtherLocaleMessages(
58
+ config,
59
+ exts,
60
+ customReaders,
61
+ );
62
+
63
+ const results: Record<string, ValidationResult> = {};
64
+ for (const locale of config.supportedLocales) {
65
+ if (locale === config.defaultLocale) continue;
66
+ const messages = localeMessages[locale];
67
+ if (!messages) continue;
68
+ results[locale] = validateLocaleMessages(
69
+ schemaConfig.schemas,
70
+ messages,
71
+ );
72
+ }
73
+
74
+ resultsByConfig[config.id] = results;
75
+ }
76
+
77
+ spinner.stop();
78
+ printSummary(resultsByConfig);
79
+ } catch (error) {
80
+ spinner.stop();
81
+ console.error(error instanceof Error ? error.message : String(error));
82
+ process.exit(1);
83
+ }
84
+ }
@@ -1,3 +0,0 @@
1
- export const DEFAULT_OUT_DIR = ".intor";
2
- export const DEFAULT_TYPES_FILE = "intor-generated-types.d.ts";
3
- export const DEFAULT_SCHEMA_FILE = "intor-generated.schema.json";
@@ -1,42 +0,0 @@
1
- import type { GeneratedSchema } from "../build";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { DEFAULT_OUT_DIR, DEFAULT_SCHEMA_FILE } from "./constants";
5
-
6
- export interface ReadGeneratedSchemaOptions {
7
- cwd?: string;
8
- outDir?: string;
9
- fileName?: string;
10
- }
11
-
12
- export async function readGeneratedSchema(
13
- options: ReadGeneratedSchemaOptions = {},
14
- ): Promise<GeneratedSchema> {
15
- const {
16
- cwd = process.cwd(),
17
- outDir = DEFAULT_OUT_DIR,
18
- fileName = DEFAULT_SCHEMA_FILE,
19
- } = options;
20
-
21
- const schemaPath = path.resolve(cwd, outDir, fileName);
22
-
23
- let raw: string;
24
-
25
- // Try to read
26
- try {
27
- raw = await fs.readFile(schemaPath, "utf8");
28
- } catch {
29
- throw new Error(
30
- `Failed to read intor schema file at "${schemaPath}".\n Have you run "intor generate"?`,
31
- );
32
- }
33
-
34
- // Try to parse
35
- try {
36
- return JSON.parse(raw) as GeneratedSchema;
37
- } catch {
38
- throw new Error(
39
- `Invalid JSON format in intor schema file at "${schemaPath}".`,
40
- );
41
- }
42
- }
@@ -1,51 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import {
4
- DEFAULT_OUT_DIR,
5
- DEFAULT_TYPES_FILE,
6
- DEFAULT_SCHEMA_FILE,
7
- } from "./constants";
8
-
9
- export interface WriteOptions {
10
- cwd?: string;
11
- outDir?: string;
12
- typesFileName?: string;
13
- schemaFileName?: string;
14
- }
15
-
16
- export interface WriteGeneratedFilesResult {
17
- outDir: string;
18
- typesPath: string;
19
- schemaPath: string;
20
- }
21
-
22
- export async function writeGeneratedFiles(
23
- { types, schema }: { types: string; schema: unknown },
24
- options: WriteOptions = {},
25
- ): Promise<WriteGeneratedFilesResult> {
26
- const {
27
- cwd = process.cwd(),
28
- outDir = DEFAULT_OUT_DIR,
29
- typesFileName = DEFAULT_TYPES_FILE,
30
- schemaFileName = DEFAULT_SCHEMA_FILE,
31
- } = options;
32
-
33
- const resolvedOutDir = path.resolve(cwd, outDir);
34
- const typesPath = path.join(resolvedOutDir, typesFileName);
35
- const schemaPath = path.join(resolvedOutDir, schemaFileName);
36
-
37
- // Ensure .intor directory exists
38
- await fs.mkdir(resolvedOutDir, { recursive: true });
39
-
40
- // Write types (as-is)
41
- await fs.writeFile(typesPath, types, "utf8");
42
-
43
- // Write schema (pretty JSON for diff / debug)
44
- await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf8");
45
-
46
- return {
47
- outDir: resolvedOutDir,
48
- typesPath,
49
- schemaPath,
50
- };
51
- }
@@ -1,7 +0,0 @@
1
- import pc from "picocolors";
2
-
3
- export function printTitle(title: string) {
4
- console.log();
5
- console.log(pc.bgBlack(` • ${title} `));
6
- console.log();
7
- }