@bleedingdev/modern-js-code-tools 3.2.0-ultramodern.120 → 3.2.0-ultramodern.121

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.
@@ -47,6 +47,10 @@ const external_node_path_namespaceObject = require("node:path");
47
47
  var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
48
48
  const external_oxlint_cjs_namespaceObject = require("./oxlint.cjs");
49
49
  const WORKSPACE_SOURCE_SUCCESS = 'UltraModern i18n and boundary guardrails validated';
50
+ const DEFAULT_LOCALES = [
51
+ 'en',
52
+ 'cs'
53
+ ];
50
54
  const ignoredDirectories = new Set([
51
55
  '.modern',
52
56
  '.modernjs',
@@ -54,6 +58,7 @@ const ignoredDirectories = new Set([
54
58
  'dist',
55
59
  'node_modules'
56
60
  ]);
61
+ const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
57
62
  const normalizePath = (filePath)=>filePath.replaceAll('\\', '/');
58
63
  const relativePath = (root, filePath)=>normalizePath(external_node_path_default().relative(root, filePath));
59
64
  const walk = (directory, files = [])=>{
@@ -72,13 +77,22 @@ const walk = (directory, files = [])=>{
72
77
  return files;
73
78
  };
74
79
  const isSourceFile = (filePath)=>/\.(?:ts|tsx|js|jsx)$/u.test(filePath);
75
- const isLocaleJson = (root, filePath)=>/\/locales\/(?:en|cs)\/[^/]+\.json$/u.test(`/${relativePath(root, filePath)}`);
80
+ const createLocaleJsonMatcher = (locales)=>{
81
+ const pattern = new RegExp(`/locales/(?:${locales.map(escapeRegExp).join('|')})/[^/]+\\.json$`, 'u');
82
+ return (root, filePath)=>pattern.test(`/${relativePath(root, filePath)}`);
83
+ };
84
+ const resolvePluralCategories = (locale, overrides)=>overrides?.[locale] ?? new Intl.PluralRules(locale).resolvedOptions().pluralCategories;
76
85
  const readText = (filePath)=>external_node_fs_default().readFileSync(filePath, 'utf-8');
77
- const checkRuntimeResources = (root, filePath, text)=>{
86
+ const localeImportPattern = (locale)=>new RegExp(`import\\s+(?:\\*\\s+as\\s+)?[A-Za-z_$][\\w$]*\\s+from\\s+['"]\\.\\./locales/${escapeRegExp(locale)}/[^'"]+\\.json['"]`, 'u');
87
+ const checkRuntimeResources = (root, filePath, text, locales)=>{
78
88
  const relative = relativePath(root, filePath);
79
89
  if (!relative.endsWith('/src/modern.runtime.ts')) return;
80
- const importsLocaleResources = /import\s+csResource\s+from\s+['"]\.\.\/locales\/cs\/[^'"]+\.json['"]/u.test(text) && /import\s+enResource\s+from\s+['"]\.\.\/locales\/en\/[^'"]+\.json['"]/u.test(text);
81
- if (!importsLocaleResources || !/initOptions\s*:\s*\{[\s\S]*?\bresources\s*,/u.test(text)) throw new Error(`${relative} must register locale JSON resources in modern.runtime.ts so Worker SSR and hydration use the same first-render translations.`);
90
+ const missingLocales = locales.filter((locale)=>!localeImportPattern(locale).test(text));
91
+ const registersResources = /initOptions\s*:\s*\{[\s\S]*?\bresources\s*[,:}]/u.test(text);
92
+ if (missingLocales.length > 0 || !registersResources) {
93
+ const detail = missingLocales.length > 0 ? `missing locale JSON imports for: ${missingLocales.join(', ')}` : 'initOptions does not register a `resources` entry';
94
+ throw new Error(`${relative} must register locale JSON resources in modern.runtime.ts so Worker SSR and hydration use the same first-render translations (${detail}).`);
95
+ }
82
96
  };
83
97
  const visitLocaleKeys = (value, visitor, pathParts = [])=>{
84
98
  if (!value || 'object' != typeof value || Array.isArray(value)) return;
@@ -91,22 +105,12 @@ const visitLocaleKeys = (value, visitor, pathParts = [])=>{
91
105
  visitLocaleKeys(child, visitor, nextPath);
92
106
  }
93
107
  };
94
- const checkPluralResources = (root, filePath, json)=>{
108
+ const checkPluralResources = (root, filePath, json, requiredSuffixes, pluralSuffixPattern)=>{
95
109
  const relative = relativePath(root, filePath);
96
- const language = relative.split('/locales/')[1]?.split('/')[0];
97
- const requiredSuffixes = 'cs' === language ? [
98
- 'one',
99
- 'few',
100
- 'many',
101
- 'other'
102
- ] : [
103
- 'one',
104
- 'other'
105
- ];
106
110
  const groups = new Map();
107
111
  visitLocaleKeys(json, (key, value, pathParts)=>{
108
112
  if ('string' != typeof value || !value.includes('{{count}}')) return;
109
- const suffixMatch = key.match(/^(.*)_(one|few|many|other)$/u);
113
+ const suffixMatch = key.match(pluralSuffixPattern);
110
114
  if (!suffixMatch) throw new Error(`${relative} key ${pathParts.join('.')} contains {{count}} but is not plural-suffixed.`);
111
115
  const [, base = '', suffix = ''] = suffixMatch;
112
116
  const parentPath = pathParts.slice(0, -1).join('.');
@@ -117,15 +121,30 @@ const checkPluralResources = (root, filePath, json)=>{
117
121
  });
118
122
  for (const [group, suffixes] of groups)for (const suffix of requiredSuffixes)if (!suffixes.has(suffix)) throw new Error(`${relative} plural group ${group} is missing _${suffix}.`);
119
123
  };
120
- const runRuntimeAndLocaleResourceChecks = (root, sourceRoots)=>{
124
+ const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCategories)=>{
125
+ if (0 === locales.length) return;
126
+ const isLocaleJson = createLocaleJsonMatcher(locales);
127
+ const localeCategories = new Map(locales.map((locale)=>[
128
+ locale,
129
+ resolvePluralCategories(locale, pluralCategories)
130
+ ]));
131
+ const pluralSuffixPattern = new RegExp(`^(.*)_(${[
132
+ ...new Set([
133
+ ...localeCategories.values()
134
+ ].flat())
135
+ ].map(escapeRegExp).join('|')})$`, 'u');
121
136
  const files = sourceRoots.flatMap((sourceRoot)=>walk(external_node_path_default().join(root, sourceRoot)));
122
- for (const filePath of files.filter(isSourceFile))checkRuntimeResources(root, filePath, readText(filePath));
123
- for (const filePath of files.filter((filePath)=>isLocaleJson(root, filePath)))checkPluralResources(root, filePath, JSON.parse(readText(filePath)));
137
+ for (const filePath of files.filter(isSourceFile))checkRuntimeResources(root, filePath, readText(filePath), locales);
138
+ for (const filePath of files.filter((filePath)=>isLocaleJson(root, filePath))){
139
+ const relative = relativePath(root, filePath);
140
+ const language = relative.split('/locales/')[1]?.split('/')[0] ?? '';
141
+ checkPluralResources(root, filePath, JSON.parse(readText(filePath)), localeCategories.get(language) ?? [], pluralSuffixPattern);
142
+ }
124
143
  };
125
144
  const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
126
145
  'apps',
127
146
  'verticals'
128
- ] } = {})=>{
147
+ ], locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
129
148
  const oxlintResult = (0, external_oxlint_cjs_namespaceObject.runOxlintRules)({
130
149
  cwd,
131
150
  targets: sourceRoots,
@@ -155,7 +174,7 @@ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
155
174
  return oxlintResult.exitCode;
156
175
  }
157
176
  try {
158
- runRuntimeAndLocaleResourceChecks(cwd, sourceRoots);
177
+ runRuntimeAndLocaleResourceChecks(cwd, sourceRoots, locales, pluralCategories);
159
178
  } catch (error) {
160
179
  console.error(error instanceof Error ? error.message : 'UltraModern workspace source checks failed.');
161
180
  return 1;
@@ -2,6 +2,10 @@ import node_fs from "node:fs";
2
2
  import node_path from "node:path";
3
3
  import { printOxlintOutput, runOxlintRules } from "./oxlint.js";
4
4
  const WORKSPACE_SOURCE_SUCCESS = 'UltraModern i18n and boundary guardrails validated';
5
+ const DEFAULT_LOCALES = [
6
+ 'en',
7
+ 'cs'
8
+ ];
5
9
  const ignoredDirectories = new Set([
6
10
  '.modern',
7
11
  '.modernjs',
@@ -9,6 +13,7 @@ const ignoredDirectories = new Set([
9
13
  'dist',
10
14
  'node_modules'
11
15
  ]);
16
+ const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
12
17
  const normalizePath = (filePath)=>filePath.replaceAll('\\', '/');
13
18
  const relativePath = (root, filePath)=>normalizePath(node_path.relative(root, filePath));
14
19
  const walk = (directory, files = [])=>{
@@ -27,13 +32,22 @@ const walk = (directory, files = [])=>{
27
32
  return files;
28
33
  };
29
34
  const isSourceFile = (filePath)=>/\.(?:ts|tsx|js|jsx)$/u.test(filePath);
30
- const isLocaleJson = (root, filePath)=>/\/locales\/(?:en|cs)\/[^/]+\.json$/u.test(`/${relativePath(root, filePath)}`);
35
+ const createLocaleJsonMatcher = (locales)=>{
36
+ const pattern = new RegExp(`/locales/(?:${locales.map(escapeRegExp).join('|')})/[^/]+\\.json$`, 'u');
37
+ return (root, filePath)=>pattern.test(`/${relativePath(root, filePath)}`);
38
+ };
39
+ const resolvePluralCategories = (locale, overrides)=>overrides?.[locale] ?? new Intl.PluralRules(locale).resolvedOptions().pluralCategories;
31
40
  const readText = (filePath)=>node_fs.readFileSync(filePath, 'utf-8');
32
- const checkRuntimeResources = (root, filePath, text)=>{
41
+ const localeImportPattern = (locale)=>new RegExp(`import\\s+(?:\\*\\s+as\\s+)?[A-Za-z_$][\\w$]*\\s+from\\s+['"]\\.\\./locales/${escapeRegExp(locale)}/[^'"]+\\.json['"]`, 'u');
42
+ const checkRuntimeResources = (root, filePath, text, locales)=>{
33
43
  const relative = relativePath(root, filePath);
34
44
  if (!relative.endsWith('/src/modern.runtime.ts')) return;
35
- const importsLocaleResources = /import\s+csResource\s+from\s+['"]\.\.\/locales\/cs\/[^'"]+\.json['"]/u.test(text) && /import\s+enResource\s+from\s+['"]\.\.\/locales\/en\/[^'"]+\.json['"]/u.test(text);
36
- if (!importsLocaleResources || !/initOptions\s*:\s*\{[\s\S]*?\bresources\s*,/u.test(text)) throw new Error(`${relative} must register locale JSON resources in modern.runtime.ts so Worker SSR and hydration use the same first-render translations.`);
45
+ const missingLocales = locales.filter((locale)=>!localeImportPattern(locale).test(text));
46
+ const registersResources = /initOptions\s*:\s*\{[\s\S]*?\bresources\s*[,:}]/u.test(text);
47
+ if (missingLocales.length > 0 || !registersResources) {
48
+ const detail = missingLocales.length > 0 ? `missing locale JSON imports for: ${missingLocales.join(', ')}` : 'initOptions does not register a `resources` entry';
49
+ throw new Error(`${relative} must register locale JSON resources in modern.runtime.ts so Worker SSR and hydration use the same first-render translations (${detail}).`);
50
+ }
37
51
  };
38
52
  const visitLocaleKeys = (value, visitor, pathParts = [])=>{
39
53
  if (!value || 'object' != typeof value || Array.isArray(value)) return;
@@ -46,22 +60,12 @@ const visitLocaleKeys = (value, visitor, pathParts = [])=>{
46
60
  visitLocaleKeys(child, visitor, nextPath);
47
61
  }
48
62
  };
49
- const checkPluralResources = (root, filePath, json)=>{
63
+ const checkPluralResources = (root, filePath, json, requiredSuffixes, pluralSuffixPattern)=>{
50
64
  const relative = relativePath(root, filePath);
51
- const language = relative.split('/locales/')[1]?.split('/')[0];
52
- const requiredSuffixes = 'cs' === language ? [
53
- 'one',
54
- 'few',
55
- 'many',
56
- 'other'
57
- ] : [
58
- 'one',
59
- 'other'
60
- ];
61
65
  const groups = new Map();
62
66
  visitLocaleKeys(json, (key, value, pathParts)=>{
63
67
  if ('string' != typeof value || !value.includes('{{count}}')) return;
64
- const suffixMatch = key.match(/^(.*)_(one|few|many|other)$/u);
68
+ const suffixMatch = key.match(pluralSuffixPattern);
65
69
  if (!suffixMatch) throw new Error(`${relative} key ${pathParts.join('.')} contains {{count}} but is not plural-suffixed.`);
66
70
  const [, base = '', suffix = ''] = suffixMatch;
67
71
  const parentPath = pathParts.slice(0, -1).join('.');
@@ -72,15 +76,30 @@ const checkPluralResources = (root, filePath, json)=>{
72
76
  });
73
77
  for (const [group, suffixes] of groups)for (const suffix of requiredSuffixes)if (!suffixes.has(suffix)) throw new Error(`${relative} plural group ${group} is missing _${suffix}.`);
74
78
  };
75
- const runRuntimeAndLocaleResourceChecks = (root, sourceRoots)=>{
79
+ const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCategories)=>{
80
+ if (0 === locales.length) return;
81
+ const isLocaleJson = createLocaleJsonMatcher(locales);
82
+ const localeCategories = new Map(locales.map((locale)=>[
83
+ locale,
84
+ resolvePluralCategories(locale, pluralCategories)
85
+ ]));
86
+ const pluralSuffixPattern = new RegExp(`^(.*)_(${[
87
+ ...new Set([
88
+ ...localeCategories.values()
89
+ ].flat())
90
+ ].map(escapeRegExp).join('|')})$`, 'u');
76
91
  const files = sourceRoots.flatMap((sourceRoot)=>walk(node_path.join(root, sourceRoot)));
77
- for (const filePath of files.filter(isSourceFile))checkRuntimeResources(root, filePath, readText(filePath));
78
- for (const filePath of files.filter((filePath)=>isLocaleJson(root, filePath)))checkPluralResources(root, filePath, JSON.parse(readText(filePath)));
92
+ for (const filePath of files.filter(isSourceFile))checkRuntimeResources(root, filePath, readText(filePath), locales);
93
+ for (const filePath of files.filter((filePath)=>isLocaleJson(root, filePath))){
94
+ const relative = relativePath(root, filePath);
95
+ const language = relative.split('/locales/')[1]?.split('/')[0] ?? '';
96
+ checkPluralResources(root, filePath, JSON.parse(readText(filePath)), localeCategories.get(language) ?? [], pluralSuffixPattern);
97
+ }
79
98
  };
80
99
  const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
81
100
  'apps',
82
101
  'verticals'
83
- ] } = {})=>{
102
+ ], locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
84
103
  const oxlintResult = runOxlintRules({
85
104
  cwd,
86
105
  targets: sourceRoots,
@@ -110,7 +129,7 @@ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
110
129
  return oxlintResult.exitCode;
111
130
  }
112
131
  try {
113
- runRuntimeAndLocaleResourceChecks(cwd, sourceRoots);
132
+ runRuntimeAndLocaleResourceChecks(cwd, sourceRoots, locales, pluralCategories);
114
133
  } catch (error) {
115
134
  console.error(error instanceof Error ? error.message : 'UltraModern workspace source checks failed.');
116
135
  return 1;
@@ -3,6 +3,10 @@ import node_fs from "node:fs";
3
3
  import node_path from "node:path";
4
4
  import { printOxlintOutput, runOxlintRules } from "./oxlint.js";
5
5
  const WORKSPACE_SOURCE_SUCCESS = 'UltraModern i18n and boundary guardrails validated';
6
+ const DEFAULT_LOCALES = [
7
+ 'en',
8
+ 'cs'
9
+ ];
6
10
  const ignoredDirectories = new Set([
7
11
  '.modern',
8
12
  '.modernjs',
@@ -10,6 +14,7 @@ const ignoredDirectories = new Set([
10
14
  'dist',
11
15
  'node_modules'
12
16
  ]);
17
+ const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
13
18
  const normalizePath = (filePath)=>filePath.replaceAll('\\', '/');
14
19
  const relativePath = (root, filePath)=>normalizePath(node_path.relative(root, filePath));
15
20
  const walk = (directory, files = [])=>{
@@ -28,13 +33,22 @@ const walk = (directory, files = [])=>{
28
33
  return files;
29
34
  };
30
35
  const isSourceFile = (filePath)=>/\.(?:ts|tsx|js|jsx)$/u.test(filePath);
31
- const isLocaleJson = (root, filePath)=>/\/locales\/(?:en|cs)\/[^/]+\.json$/u.test(`/${relativePath(root, filePath)}`);
36
+ const createLocaleJsonMatcher = (locales)=>{
37
+ const pattern = new RegExp(`/locales/(?:${locales.map(escapeRegExp).join('|')})/[^/]+\\.json$`, 'u');
38
+ return (root, filePath)=>pattern.test(`/${relativePath(root, filePath)}`);
39
+ };
40
+ const resolvePluralCategories = (locale, overrides)=>overrides?.[locale] ?? new Intl.PluralRules(locale).resolvedOptions().pluralCategories;
32
41
  const readText = (filePath)=>node_fs.readFileSync(filePath, 'utf-8');
33
- const checkRuntimeResources = (root, filePath, text)=>{
42
+ const localeImportPattern = (locale)=>new RegExp(`import\\s+(?:\\*\\s+as\\s+)?[A-Za-z_$][\\w$]*\\s+from\\s+['"]\\.\\./locales/${escapeRegExp(locale)}/[^'"]+\\.json['"]`, 'u');
43
+ const checkRuntimeResources = (root, filePath, text, locales)=>{
34
44
  const relative = relativePath(root, filePath);
35
45
  if (!relative.endsWith('/src/modern.runtime.ts')) return;
36
- const importsLocaleResources = /import\s+csResource\s+from\s+['"]\.\.\/locales\/cs\/[^'"]+\.json['"]/u.test(text) && /import\s+enResource\s+from\s+['"]\.\.\/locales\/en\/[^'"]+\.json['"]/u.test(text);
37
- if (!importsLocaleResources || !/initOptions\s*:\s*\{[\s\S]*?\bresources\s*,/u.test(text)) throw new Error(`${relative} must register locale JSON resources in modern.runtime.ts so Worker SSR and hydration use the same first-render translations.`);
46
+ const missingLocales = locales.filter((locale)=>!localeImportPattern(locale).test(text));
47
+ const registersResources = /initOptions\s*:\s*\{[\s\S]*?\bresources\s*[,:}]/u.test(text);
48
+ if (missingLocales.length > 0 || !registersResources) {
49
+ const detail = missingLocales.length > 0 ? `missing locale JSON imports for: ${missingLocales.join(', ')}` : 'initOptions does not register a `resources` entry';
50
+ throw new Error(`${relative} must register locale JSON resources in modern.runtime.ts so Worker SSR and hydration use the same first-render translations (${detail}).`);
51
+ }
38
52
  };
39
53
  const visitLocaleKeys = (value, visitor, pathParts = [])=>{
40
54
  if (!value || 'object' != typeof value || Array.isArray(value)) return;
@@ -47,22 +61,12 @@ const visitLocaleKeys = (value, visitor, pathParts = [])=>{
47
61
  visitLocaleKeys(child, visitor, nextPath);
48
62
  }
49
63
  };
50
- const checkPluralResources = (root, filePath, json)=>{
64
+ const checkPluralResources = (root, filePath, json, requiredSuffixes, pluralSuffixPattern)=>{
51
65
  const relative = relativePath(root, filePath);
52
- const language = relative.split('/locales/')[1]?.split('/')[0];
53
- const requiredSuffixes = 'cs' === language ? [
54
- 'one',
55
- 'few',
56
- 'many',
57
- 'other'
58
- ] : [
59
- 'one',
60
- 'other'
61
- ];
62
66
  const groups = new Map();
63
67
  visitLocaleKeys(json, (key, value, pathParts)=>{
64
68
  if ('string' != typeof value || !value.includes('{{count}}')) return;
65
- const suffixMatch = key.match(/^(.*)_(one|few|many|other)$/u);
69
+ const suffixMatch = key.match(pluralSuffixPattern);
66
70
  if (!suffixMatch) throw new Error(`${relative} key ${pathParts.join('.')} contains {{count}} but is not plural-suffixed.`);
67
71
  const [, base = '', suffix = ''] = suffixMatch;
68
72
  const parentPath = pathParts.slice(0, -1).join('.');
@@ -73,15 +77,30 @@ const checkPluralResources = (root, filePath, json)=>{
73
77
  });
74
78
  for (const [group, suffixes] of groups)for (const suffix of requiredSuffixes)if (!suffixes.has(suffix)) throw new Error(`${relative} plural group ${group} is missing _${suffix}.`);
75
79
  };
76
- const runRuntimeAndLocaleResourceChecks = (root, sourceRoots)=>{
80
+ const runRuntimeAndLocaleResourceChecks = (root, sourceRoots, locales, pluralCategories)=>{
81
+ if (0 === locales.length) return;
82
+ const isLocaleJson = createLocaleJsonMatcher(locales);
83
+ const localeCategories = new Map(locales.map((locale)=>[
84
+ locale,
85
+ resolvePluralCategories(locale, pluralCategories)
86
+ ]));
87
+ const pluralSuffixPattern = new RegExp(`^(.*)_(${[
88
+ ...new Set([
89
+ ...localeCategories.values()
90
+ ].flat())
91
+ ].map(escapeRegExp).join('|')})$`, 'u');
77
92
  const files = sourceRoots.flatMap((sourceRoot)=>walk(node_path.join(root, sourceRoot)));
78
- for (const filePath of files.filter(isSourceFile))checkRuntimeResources(root, filePath, readText(filePath));
79
- for (const filePath of files.filter((filePath)=>isLocaleJson(root, filePath)))checkPluralResources(root, filePath, JSON.parse(readText(filePath)));
93
+ for (const filePath of files.filter(isSourceFile))checkRuntimeResources(root, filePath, readText(filePath), locales);
94
+ for (const filePath of files.filter((filePath)=>isLocaleJson(root, filePath))){
95
+ const relative = relativePath(root, filePath);
96
+ const language = relative.split('/locales/')[1]?.split('/')[0] ?? '';
97
+ checkPluralResources(root, filePath, JSON.parse(readText(filePath)), localeCategories.get(language) ?? [], pluralSuffixPattern);
98
+ }
80
99
  };
81
100
  const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
82
101
  'apps',
83
102
  'verticals'
84
- ] } = {})=>{
103
+ ], locales = DEFAULT_LOCALES, pluralCategories } = {})=>{
85
104
  const oxlintResult = runOxlintRules({
86
105
  cwd,
87
106
  targets: sourceRoots,
@@ -111,7 +130,7 @@ const runWorkspaceSourceCheck = ({ cwd = process.cwd(), sourceRoots = [
111
130
  return oxlintResult.exitCode;
112
131
  }
113
132
  try {
114
- runRuntimeAndLocaleResourceChecks(cwd, sourceRoots);
133
+ runRuntimeAndLocaleResourceChecks(cwd, sourceRoots, locales, pluralCategories);
115
134
  } catch (error) {
116
135
  console.error(error instanceof Error ? error.message : 'UltraModern workspace source checks failed.');
117
136
  return 1;
@@ -1,8 +1,21 @@
1
- type WorkspaceSourceCheckOptions = {
1
+ export type WorkspaceSourceCheckOptions = {
2
2
  readonly cwd?: string;
3
3
  readonly sourceRoots?: readonly string[];
4
+ /**
5
+ * Locale codes whose `locales/<locale>/*.json` resources are plural-checked
6
+ * and must be registered in each app's `src/modern.runtime.ts`.
7
+ * Defaults to `['en', 'cs']` (the UltraModern workspace convention).
8
+ * Pass an empty array to opt out of the runtime/locale resource checks.
9
+ */
10
+ readonly locales?: readonly string[];
11
+ /**
12
+ * Per-locale plural-category overrides. Locales absent from this map
13
+ * resolve their categories through `Intl.PluralRules(locale)` (CLDR
14
+ * cardinal rules), e.g. `['one', 'other']` for `en` and
15
+ * `['one', 'few', 'many', 'other']` for `cs`.
16
+ */
17
+ readonly pluralCategories?: Readonly<Record<string, readonly string[]>>;
4
18
  };
5
19
  export declare const WORKSPACE_SOURCE_SUCCESS = "UltraModern i18n and boundary guardrails validated";
6
- export declare const runWorkspaceSourceCheck: ({ cwd, sourceRoots, }?: WorkspaceSourceCheckOptions) => number;
20
+ export declare const runWorkspaceSourceCheck: ({ cwd, sourceRoots, locales, pluralCategories, }?: WorkspaceSourceCheckOptions) => number;
7
21
  export declare const main: () => void;
8
- export {};
@@ -1,3 +1,3 @@
1
1
  export { runSingleAppI18nCheck } from './cli/i18n-check';
2
- export { runWorkspaceSourceCheck } from './cli/workspace-source-check';
2
+ export { runWorkspaceSourceCheck, type WorkspaceSourceCheckOptions, } from './cli/workspace-source-check';
3
3
  export { default as oxlintPlugin } from './oxlint-plugin';
package/package.json CHANGED
@@ -23,7 +23,7 @@
23
23
  "engines": {
24
24
  "node": ">=20"
25
25
  },
26
- "version": "3.2.0-ultramodern.120",
26
+ "version": "3.2.0-ultramodern.121",
27
27
  "types": "./dist/types/index.d.ts",
28
28
  "main": "./dist/esm-node/index.js",
29
29
  "typesVersions": {