@contentstorage/core 1.2.1 → 2.1.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/README.md +128 -139
- package/dist/commands/cli.d.ts +2 -0
- package/dist/commands/cli.js +110 -0
- package/dist/commands/generate-types.d.ts +2 -0
- package/dist/commands/generate-types.js +177 -0
- package/dist/commands/pull.d.ts +2 -0
- package/dist/commands/pull.js +140 -0
- package/dist/commands/stats.d.ts +4 -0
- package/dist/commands/stats.js +268 -0
- package/dist/core/config-loader.d.ts +2 -0
- package/dist/core/config-loader.js +42 -0
- package/dist/type-generation/get-names.js +14 -11
- package/dist/type-generation/util.js +7 -7
- package/dist/types.d.ts +0 -24
- package/dist/utils/constants.d.ts +4 -0
- package/dist/utils/constants.js +4 -0
- package/dist/utils/flatten-json.d.ts +1 -0
- package/dist/utils/flatten-json.js +56 -0
- package/package.json +5 -12
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { loadConfig } from '../core/config-loader.js';
|
|
7
|
+
import { CONTENTSTORAGE_CONFIG } from '../utils/constants.js';
|
|
8
|
+
export async function pullContent() {
|
|
9
|
+
console.log(chalk.blue('Starting content pull...'));
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const cliConfig = {};
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
if (arg.startsWith('--')) {
|
|
15
|
+
const key = arg.substring(2);
|
|
16
|
+
const value = args[i + 1];
|
|
17
|
+
if (key === 'pending-changes') {
|
|
18
|
+
cliConfig.pendingChanges = true;
|
|
19
|
+
}
|
|
20
|
+
else if (value && !value.startsWith('--')) {
|
|
21
|
+
if (key === 'lang') {
|
|
22
|
+
cliConfig.languageCodes = [value.toUpperCase()];
|
|
23
|
+
}
|
|
24
|
+
else if (key === 'content-key') {
|
|
25
|
+
cliConfig.contentKey = value;
|
|
26
|
+
}
|
|
27
|
+
else if (key === 'content-dir') {
|
|
28
|
+
cliConfig.contentDir = value;
|
|
29
|
+
}
|
|
30
|
+
// Skip the value in the next iteration
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let fileConfig = {};
|
|
36
|
+
try {
|
|
37
|
+
fileConfig = await loadConfig();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
console.log(chalk.yellow('Could not load a configuration file. Proceeding with CLI arguments.'));
|
|
41
|
+
}
|
|
42
|
+
const config = { ...fileConfig, ...cliConfig };
|
|
43
|
+
// Validate required fields
|
|
44
|
+
if (!config.contentKey) {
|
|
45
|
+
console.error(chalk.red('Error: Configuration is missing the required "contentKey" property.'));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
if (!config.contentDir) {
|
|
49
|
+
console.error(chalk.red('Error: Configuration is missing the required "contentDir" property.'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
console.log(chalk.blue(`Content key: ${config.contentKey}`));
|
|
53
|
+
console.log(chalk.blue(`Saving content to: ${config.contentDir}`));
|
|
54
|
+
try {
|
|
55
|
+
// Validate languageCodes array
|
|
56
|
+
if (!Array.isArray(config.languageCodes)) {
|
|
57
|
+
console.log(chalk.red(`Expected array from config.languageCodes, but received type ${typeof config.languageCodes}. Cannot pull files.`));
|
|
58
|
+
return; // Exit if languageCodes is not an array
|
|
59
|
+
}
|
|
60
|
+
if (config.languageCodes.length === 0) {
|
|
61
|
+
console.log(chalk.yellow('config.languageCodes array is empty. No files to pull.'));
|
|
62
|
+
return; // Exit if languageCodes array is empty
|
|
63
|
+
}
|
|
64
|
+
// Ensure the output directory exists (create it once before the loop)
|
|
65
|
+
await fs.mkdir(config.contentDir, { recursive: true });
|
|
66
|
+
// Process each language code
|
|
67
|
+
for (const languageCode of config.languageCodes) {
|
|
68
|
+
let fileUrl;
|
|
69
|
+
const requestConfig = {};
|
|
70
|
+
if (config.pendingChanges) {
|
|
71
|
+
fileUrl = `${CONTENTSTORAGE_CONFIG.API_URL}/pending-changes/get-json?languageCode=${languageCode}`;
|
|
72
|
+
requestConfig.headers = {
|
|
73
|
+
'X-Content-Key': config.contentKey,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
fileUrl = `${CONTENTSTORAGE_CONFIG.BASE_URL}/${config.contentKey}/content/${languageCode}.json`;
|
|
78
|
+
}
|
|
79
|
+
const filename = `${languageCode}.json`;
|
|
80
|
+
const outputPath = path.join(config.contentDir, filename);
|
|
81
|
+
console.log(chalk.blue(`\nProcessing language: ${languageCode}`));
|
|
82
|
+
console.log(chalk.blue(`Using following contentKey to fetch json: ${config.contentKey}`));
|
|
83
|
+
try {
|
|
84
|
+
// Fetch data for the current language
|
|
85
|
+
const response = await axios.get(fileUrl, requestConfig);
|
|
86
|
+
let jsonData = response.data;
|
|
87
|
+
// Handle API response structure - only for pending changes API
|
|
88
|
+
if (config.pendingChanges &&
|
|
89
|
+
jsonData &&
|
|
90
|
+
typeof jsonData === 'object' &&
|
|
91
|
+
'data' in jsonData) {
|
|
92
|
+
jsonData = jsonData.data;
|
|
93
|
+
}
|
|
94
|
+
// Basic check for data existence, although axios usually throws for non-2xx responses
|
|
95
|
+
if (jsonData === undefined || jsonData === null) {
|
|
96
|
+
throw new Error(`No data received from ${fileUrl} for language ${languageCode}.`);
|
|
97
|
+
}
|
|
98
|
+
// Validate that jsonData is a single, non-null JSON object (not an array)
|
|
99
|
+
// This check mirrors the original code's expectation for the content of a JSON file.
|
|
100
|
+
if (typeof jsonData !== 'object' ||
|
|
101
|
+
Array.isArray(jsonData) /* jsonData === null is already covered */) {
|
|
102
|
+
throw new Error(`Expected a single JSON object from ${fileUrl} for language ${languageCode}, but received type ${Array.isArray(jsonData) ? 'array' : typeof jsonData}. Cannot save the file.`);
|
|
103
|
+
}
|
|
104
|
+
console.log(chalk.green(`Received JSON for ${languageCode}. Saving to ${outputPath}`));
|
|
105
|
+
await fs.writeFile(outputPath, JSON.stringify(jsonData, null, 2));
|
|
106
|
+
console.log(chalk.green(`Successfully saved ${outputPath}`));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
// Catch errors related to fetching or saving a single language file
|
|
110
|
+
console.error(chalk.red(`\nError processing language ${languageCode} from ${fileUrl}:`));
|
|
111
|
+
if (axios.isAxiosError(error)) {
|
|
112
|
+
console.error(chalk.red(` Status: ${error.response?.status}`));
|
|
113
|
+
console.error(chalk.red(`Response Data: ${error.response?.data ? JSON.stringify(error.response.data) : 'N/A'}`));
|
|
114
|
+
console.error(chalk.red(` Message: ${error.message}`)); // Axios error message
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// For non-Axios errors (e.g., manually thrown errors, fs errors)
|
|
118
|
+
console.error(chalk.red(` Error: ${error.message}`));
|
|
119
|
+
}
|
|
120
|
+
// Re-throw the error to be caught by the outer try-catch block,
|
|
121
|
+
// which will then call process.exit(1), maintaining original exit behavior on error.
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk.green('\nAll content successfully pulled and saved.'));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Outer catch for setup errors (like loadConfig) or re-thrown errors from the loop
|
|
129
|
+
// The specific error details for a file operation would have been logged by the inner catch.
|
|
130
|
+
// This block provides a general failure message and ensures the process exits with an error code.
|
|
131
|
+
console.error(chalk.red('\n-----------------------------------------------------'));
|
|
132
|
+
console.error(chalk.red('Content pull failed due to an error. See details above.'));
|
|
133
|
+
// error.message from the re-thrown error will be implicitly part of the error object logged by some environments,
|
|
134
|
+
// or if you add console.error(error) here.
|
|
135
|
+
// The original code logged error.message at this level:
|
|
136
|
+
// if (error.message) console.error(chalk.red(`Underlying error: ${error.message}`));
|
|
137
|
+
console.error(chalk.red('-----------------------------------------------------'));
|
|
138
|
+
process.exit(1); // Exit with error code
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { loadConfig } from '../core/config-loader.js';
|
|
6
|
+
import { flattenJson } from '../utils/flatten-json.js';
|
|
7
|
+
import { CONTENTSTORAGE_CONFIG } from '../utils/constants.js';
|
|
8
|
+
/**
|
|
9
|
+
* Check if a value is considered untranslated
|
|
10
|
+
*/
|
|
11
|
+
function isUntranslated(value) {
|
|
12
|
+
return value === '' || value === null || value === undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Load content for a specific language, trying local files first, then API
|
|
16
|
+
*/
|
|
17
|
+
async function loadLanguageContent(languageCode, config) {
|
|
18
|
+
// Try local file first
|
|
19
|
+
try {
|
|
20
|
+
const filePath = path.join(config.contentDir, `${languageCode}.json`);
|
|
21
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
22
|
+
return JSON.parse(content);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Fallback to API
|
|
26
|
+
console.log(chalk.dim(`Local file not found for ${languageCode}, fetching from API...`));
|
|
27
|
+
let fileUrl;
|
|
28
|
+
const requestConfig = {
|
|
29
|
+
timeout: 30000,
|
|
30
|
+
};
|
|
31
|
+
if (config.pendingChanges) {
|
|
32
|
+
fileUrl = `${CONTENTSTORAGE_CONFIG.API_URL}/pending-changes/get-json?languageCode=${languageCode}`;
|
|
33
|
+
requestConfig.headers = {
|
|
34
|
+
'X-Content-Key': config.contentKey,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
fileUrl = `${CONTENTSTORAGE_CONFIG.BASE_URL}/${config.contentKey}/content/${languageCode}.json`;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const response = await axios.get(fileUrl, requestConfig);
|
|
42
|
+
let jsonData = response.data;
|
|
43
|
+
// Unwrap pending changes response
|
|
44
|
+
if (config.pendingChanges &&
|
|
45
|
+
jsonData &&
|
|
46
|
+
typeof jsonData === 'object' &&
|
|
47
|
+
'data' in jsonData) {
|
|
48
|
+
jsonData = jsonData.data;
|
|
49
|
+
}
|
|
50
|
+
return jsonData;
|
|
51
|
+
}
|
|
52
|
+
catch (apiError) {
|
|
53
|
+
console.error(chalk.red(`Failed to load content for ${languageCode}:`));
|
|
54
|
+
if (apiError.response) {
|
|
55
|
+
console.error(chalk.red(` Status: ${apiError.response.status}`));
|
|
56
|
+
}
|
|
57
|
+
throw apiError;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Analyze a single language against a reference language
|
|
63
|
+
*/
|
|
64
|
+
function analyzeLanguage(languageCode, content, referenceContent) {
|
|
65
|
+
const flatContent = flattenJson(content);
|
|
66
|
+
const flatReference = flattenJson(referenceContent);
|
|
67
|
+
const untranslatedItems = [];
|
|
68
|
+
// Get all keys from reference
|
|
69
|
+
const allKeys = Object.keys(flatReference);
|
|
70
|
+
const totalItems = allKeys.length;
|
|
71
|
+
// Check each key
|
|
72
|
+
for (const key of allKeys) {
|
|
73
|
+
// Check if key is missing in target language
|
|
74
|
+
if (!(key in flatContent)) {
|
|
75
|
+
untranslatedItems.push({ key, reason: 'missing' });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Check if value is empty
|
|
79
|
+
if (isUntranslated(flatContent[key])) {
|
|
80
|
+
untranslatedItems.push({ key, reason: 'empty' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const untranslatedCount = untranslatedItems.length;
|
|
84
|
+
const translatedCount = totalItems - untranslatedCount;
|
|
85
|
+
const completionPercentage = totalItems > 0 ? (translatedCount / totalItems) * 100 : 100;
|
|
86
|
+
return {
|
|
87
|
+
languageCode,
|
|
88
|
+
total: totalItems,
|
|
89
|
+
translated: translatedCount,
|
|
90
|
+
untranslated: untranslatedCount,
|
|
91
|
+
completionPercentage,
|
|
92
|
+
untranslatedItems,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Display statistics in a formatted table
|
|
97
|
+
*/
|
|
98
|
+
function displayStats(result) {
|
|
99
|
+
console.log(chalk.bold('\n📊 Translation Statistics'));
|
|
100
|
+
console.log(chalk.dim('═'.repeat(70)));
|
|
101
|
+
// Reference language info
|
|
102
|
+
console.log(chalk.cyan(`\nReference Language: ${chalk.bold(result.referenceLanguage)} (baseline for comparison)`));
|
|
103
|
+
console.log(chalk.cyan(`Total unique content items: ${chalk.bold(result.totalItems)}`));
|
|
104
|
+
// Language statistics table header
|
|
105
|
+
console.log(chalk.bold('\n📋 Language Statistics:'));
|
|
106
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
107
|
+
// Table header
|
|
108
|
+
const headerFormat = (str, width) => str.padEnd(width, ' ');
|
|
109
|
+
console.log(chalk.bold(headerFormat('Language', 12) +
|
|
110
|
+
headerFormat('Total', 10) +
|
|
111
|
+
headerFormat('Translated', 13) +
|
|
112
|
+
headerFormat('Untranslated', 15) +
|
|
113
|
+
'Complete'));
|
|
114
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
115
|
+
// Language rows
|
|
116
|
+
for (const stats of result.languageStats) {
|
|
117
|
+
const percentageColor = stats.completionPercentage === 100
|
|
118
|
+
? chalk.green
|
|
119
|
+
: stats.completionPercentage >= 80
|
|
120
|
+
? chalk.yellow
|
|
121
|
+
: chalk.red;
|
|
122
|
+
const percentage = percentageColor(stats.completionPercentage.toFixed(1) + '%');
|
|
123
|
+
console.log(chalk.white(headerFormat(stats.languageCode, 12)) +
|
|
124
|
+
chalk.white(headerFormat(stats.total.toString(), 10)) +
|
|
125
|
+
chalk.white(headerFormat(stats.translated.toString(), 13)) +
|
|
126
|
+
(stats.untranslated > 0
|
|
127
|
+
? chalk.red(headerFormat(stats.untranslated.toString(), 15))
|
|
128
|
+
: chalk.green(headerFormat(stats.untranslated.toString(), 15))) +
|
|
129
|
+
percentage);
|
|
130
|
+
}
|
|
131
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
132
|
+
// Untranslated items details
|
|
133
|
+
const languagesWithIssues = result.languageStats.filter((s) => s.untranslated > 0);
|
|
134
|
+
if (languagesWithIssues.length > 0) {
|
|
135
|
+
console.log(chalk.bold('\n⚠️ Untranslated Items by Language:'));
|
|
136
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
137
|
+
for (const stats of languagesWithIssues) {
|
|
138
|
+
console.log(chalk.yellow(`\n${stats.languageCode} (${stats.untranslated} ${stats.untranslated === 1 ? 'item' : 'items'}):`));
|
|
139
|
+
// Group by reason
|
|
140
|
+
const emptyItems = stats.untranslatedItems
|
|
141
|
+
.filter((item) => item.reason === 'empty')
|
|
142
|
+
.map((item) => item.key);
|
|
143
|
+
const missingItems = stats.untranslatedItems
|
|
144
|
+
.filter((item) => item.reason === 'missing')
|
|
145
|
+
.map((item) => item.key);
|
|
146
|
+
if (emptyItems.length > 0) {
|
|
147
|
+
console.log(chalk.dim(' Empty values:'));
|
|
148
|
+
emptyItems.forEach((key) => {
|
|
149
|
+
console.log(chalk.red(` • ${key}`));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (missingItems.length > 0) {
|
|
153
|
+
console.log(chalk.dim(' Missing keys:'));
|
|
154
|
+
missingItems.forEach((key) => {
|
|
155
|
+
console.log(chalk.red(` • ${key}`));
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.log(chalk.green('\n✅ All languages are fully translated!'));
|
|
162
|
+
}
|
|
163
|
+
// Overall completion
|
|
164
|
+
console.log(chalk.bold('\n📈 Overall Summary:'));
|
|
165
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
166
|
+
const overallColor = result.overallCompletion === 100
|
|
167
|
+
? chalk.green
|
|
168
|
+
: result.overallCompletion >= 80
|
|
169
|
+
? chalk.yellow
|
|
170
|
+
: chalk.red;
|
|
171
|
+
console.log(overallColor(`Overall Completion: ${chalk.bold(result.overallCompletion.toFixed(1) + '%')}`));
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Main stats command function
|
|
176
|
+
*/
|
|
177
|
+
export async function showStats() {
|
|
178
|
+
try {
|
|
179
|
+
console.log(chalk.blue('Loading configuration...'));
|
|
180
|
+
// Parse CLI arguments
|
|
181
|
+
const args = process.argv.slice(2);
|
|
182
|
+
const cliConfig = {};
|
|
183
|
+
for (let i = 0; i < args.length; i++) {
|
|
184
|
+
const arg = args[i];
|
|
185
|
+
if (arg.startsWith('--')) {
|
|
186
|
+
const key = arg.substring(2);
|
|
187
|
+
if (key === 'pending-changes') {
|
|
188
|
+
cliConfig.pendingChanges = true;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const value = args[i + 1];
|
|
192
|
+
if (value && !value.startsWith('--')) {
|
|
193
|
+
if (key === 'content-key') {
|
|
194
|
+
cliConfig.contentKey = value;
|
|
195
|
+
}
|
|
196
|
+
else if (key === 'content-dir') {
|
|
197
|
+
cliConfig.contentDir = value;
|
|
198
|
+
}
|
|
199
|
+
i++; // Skip the value in next iteration
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Load config from file
|
|
205
|
+
let fileConfig = {};
|
|
206
|
+
try {
|
|
207
|
+
fileConfig = await loadConfig();
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
console.log(chalk.yellow('⚠️ Could not load a configuration file, using CLI arguments only'));
|
|
211
|
+
}
|
|
212
|
+
// Merge configurations (CLI args override file config)
|
|
213
|
+
const config = { ...fileConfig, ...cliConfig };
|
|
214
|
+
// Validate required fields
|
|
215
|
+
if (!config.contentKey) {
|
|
216
|
+
console.error(chalk.red('\n❌ Error: Content key is required. Provide it via config file or --content-key argument.'));
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
if (!config.languageCodes || config.languageCodes.length === 0) {
|
|
220
|
+
console.error(chalk.red('\n❌ Error: At least one language code is required in configuration.'));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
// Set defaults
|
|
224
|
+
if (!config.contentDir) {
|
|
225
|
+
config.contentDir = 'src/content/json';
|
|
226
|
+
}
|
|
227
|
+
const fullConfig = config;
|
|
228
|
+
console.log(chalk.blue(`Analyzing ${fullConfig.languageCodes.length} language(s)...`));
|
|
229
|
+
// Load content for all languages
|
|
230
|
+
const languageContents = {};
|
|
231
|
+
for (const languageCode of fullConfig.languageCodes) {
|
|
232
|
+
try {
|
|
233
|
+
languageContents[languageCode] = await loadLanguageContent(languageCode, fullConfig);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
console.error(chalk.red(`\n❌ Failed to load content for ${languageCode}`));
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
console.log(chalk.green('✓ All content loaded successfully\n'));
|
|
241
|
+
// Use first language as reference
|
|
242
|
+
const referenceLanguage = fullConfig.languageCodes[0];
|
|
243
|
+
const referenceContent = languageContents[referenceLanguage];
|
|
244
|
+
// Analyze each language
|
|
245
|
+
const languageStats = [];
|
|
246
|
+
for (const languageCode of fullConfig.languageCodes) {
|
|
247
|
+
const stats = analyzeLanguage(languageCode, languageContents[languageCode], referenceContent);
|
|
248
|
+
languageStats.push(stats);
|
|
249
|
+
}
|
|
250
|
+
// Calculate overall completion
|
|
251
|
+
const totalTranslated = languageStats.reduce((sum, s) => sum + s.translated, 0);
|
|
252
|
+
const totalPossible = languageStats.reduce((sum, s) => sum + s.total, 0);
|
|
253
|
+
const overallCompletion = totalPossible > 0 ? (totalTranslated / totalPossible) * 100 : 100;
|
|
254
|
+
const result = {
|
|
255
|
+
referenceLanguage,
|
|
256
|
+
totalItems: Object.keys(flattenJson(referenceContent)).length,
|
|
257
|
+
languageStats,
|
|
258
|
+
overallCompletion,
|
|
259
|
+
};
|
|
260
|
+
// Display results
|
|
261
|
+
displayStats(result);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
console.error(chalk.red('\n❌ An error occurred:'));
|
|
265
|
+
console.error(chalk.red(error.message || error));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
languageCodes: [],
|
|
6
|
+
contentDir: path.join('src', 'content', 'json'),
|
|
7
|
+
typesOutputFile: path.join('src', 'content', 'content-types.ts'),
|
|
8
|
+
};
|
|
9
|
+
export async function loadConfig() {
|
|
10
|
+
const configPath = path.resolve(process.cwd(), 'contentstorage.config.js'); // Look in user's current working dir
|
|
11
|
+
let userConfig = {};
|
|
12
|
+
if (fs.existsSync(configPath)) {
|
|
13
|
+
try {
|
|
14
|
+
// Use require for JS config file
|
|
15
|
+
const loadedModule = await import(configPath);
|
|
16
|
+
userConfig = loadedModule.default || loadedModule;
|
|
17
|
+
console.log(chalk.blue('Loaded config', JSON.stringify(userConfig)));
|
|
18
|
+
console.log(chalk.blue(`Loaded configuration from ${configPath}`));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error(chalk.red(`Error loading configuration from ${configPath}:`, error));
|
|
22
|
+
// Decide if you want to proceed with defaults or exit
|
|
23
|
+
// For now, we'll proceed with defaults but warn
|
|
24
|
+
console.warn(chalk.yellow('Proceeding with default configuration.'));
|
|
25
|
+
userConfig = {}; // Reset in case of partial load failure
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(chalk.blue('No content.config.js found. Continuing.'));
|
|
30
|
+
}
|
|
31
|
+
const mergedConfig = {
|
|
32
|
+
...DEFAULT_CONFIG,
|
|
33
|
+
...userConfig,
|
|
34
|
+
};
|
|
35
|
+
const finalConfig = {
|
|
36
|
+
languageCodes: mergedConfig.languageCodes || [],
|
|
37
|
+
contentKey: mergedConfig.contentKey || '',
|
|
38
|
+
contentDir: path.resolve(process.cwd(), mergedConfig.contentDir),
|
|
39
|
+
typesOutputFile: path.resolve(process.cwd(), mergedConfig.typesOutputFile),
|
|
40
|
+
};
|
|
41
|
+
return finalConfig;
|
|
42
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-expect-error
|
|
2
|
-
import * as pluralize from
|
|
3
|
-
import { TypeGroup } from './model.js';
|
|
4
|
-
import { findTypeById, getTypeDescriptionGroup, isHash, parseKeyMetaData } from './util.js';
|
|
2
|
+
import * as pluralize from 'pluralize';
|
|
3
|
+
import { TypeGroup, } from './model.js';
|
|
4
|
+
import { findTypeById, getTypeDescriptionGroup, isHash, parseKeyMetaData, } from './util.js';
|
|
5
5
|
function getName({ rootTypeId, types }, keyName, names, isInsideArray
|
|
6
6
|
// @ts-ignore
|
|
7
7
|
) {
|
|
@@ -34,7 +34,7 @@ function getName({ rootTypeId, types }, keyName, names, isInsideArray
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
export function getNames(typeStructure, rootName =
|
|
37
|
+
export function getNames(typeStructure, rootName = 'RootObject') {
|
|
38
38
|
return getName(typeStructure, rootName, [], false).names.reverse();
|
|
39
39
|
}
|
|
40
40
|
function getNameById(id, keyName, isInsideArray, types, nameMap) {
|
|
@@ -47,7 +47,9 @@ function getNameById(id, keyName, isInsideArray, types, nameMap) {
|
|
|
47
47
|
let name;
|
|
48
48
|
switch (group) {
|
|
49
49
|
case TypeGroup.Array:
|
|
50
|
-
name = typeDesc.isUnion
|
|
50
|
+
name = typeDesc.isUnion
|
|
51
|
+
? getArrayName(typeDesc, types, nameMap)
|
|
52
|
+
: formatArrayName(typeDesc, types, nameMap);
|
|
51
53
|
break;
|
|
52
54
|
case TypeGroup.Object:
|
|
53
55
|
/**
|
|
@@ -72,9 +74,9 @@ function getNameById(id, keyName, isInsideArray, types, nameMap) {
|
|
|
72
74
|
function pascalCase(name) {
|
|
73
75
|
return name
|
|
74
76
|
.split(/\s+/g)
|
|
75
|
-
.filter((_) => _ !==
|
|
77
|
+
.filter((_) => _ !== '')
|
|
76
78
|
.map(capitalize)
|
|
77
|
-
.reduce((a, b) => a + b,
|
|
79
|
+
.reduce((a, b) => a + b, '');
|
|
78
80
|
}
|
|
79
81
|
function capitalize(name) {
|
|
80
82
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
@@ -84,7 +86,7 @@ function normalizeInvalidTypeName(name) {
|
|
|
84
86
|
return name;
|
|
85
87
|
}
|
|
86
88
|
else {
|
|
87
|
-
const noSymbolsName = name.replace(/[^a-zA-Z0-9]/g,
|
|
89
|
+
const noSymbolsName = name.replace(/[^a-zA-Z0-9]/g, '');
|
|
88
90
|
const startsWithWordCharacter = /^[a-zA-Z]/.test(noSymbolsName);
|
|
89
91
|
return startsWithWordCharacter ? noSymbolsName : `_${noSymbolsName}`;
|
|
90
92
|
}
|
|
@@ -100,9 +102,10 @@ function uniqueByIncrement(name, names) {
|
|
|
100
102
|
function getArrayName(typeDesc, types, nameMap) {
|
|
101
103
|
// @ts-ignore
|
|
102
104
|
if (typeDesc.arrayOfTypes.length === 0) {
|
|
103
|
-
return
|
|
105
|
+
return 'any';
|
|
104
106
|
}
|
|
105
|
-
else {
|
|
107
|
+
else {
|
|
108
|
+
// @ts-ignore
|
|
106
109
|
if (typeDesc.arrayOfTypes.length === 1) {
|
|
107
110
|
// @ts-ignore
|
|
108
111
|
const [idOrPrimitive] = typeDesc.arrayOfTypes;
|
|
@@ -125,7 +128,7 @@ function unionToString(typeDesc, types, nameMap) {
|
|
|
125
128
|
return typeDesc.arrayOfTypes.reduce((acc, type, i) => {
|
|
126
129
|
const readableTypeName = convertToReadableType(type, types, nameMap);
|
|
127
130
|
return i === 0 ? readableTypeName : `${acc} | ${readableTypeName}`;
|
|
128
|
-
},
|
|
131
|
+
}, '');
|
|
129
132
|
}
|
|
130
133
|
function formatArrayName(typeDesc, types, nameMap) {
|
|
131
134
|
// @ts-ignore
|
|
@@ -6,30 +6,30 @@ export function onlyUnique(value, index, self) {
|
|
|
6
6
|
return self.indexOf(value) === index;
|
|
7
7
|
}
|
|
8
8
|
export function isArray(x) {
|
|
9
|
-
return Object.prototype.toString.call(x) ===
|
|
9
|
+
return Object.prototype.toString.call(x) === '[object Array]';
|
|
10
10
|
}
|
|
11
11
|
export function isNonArrayUnion(typeName) {
|
|
12
12
|
const arrayUnionRegex = /^\(.*\)\[\]$/;
|
|
13
|
-
return typeName.includes(
|
|
13
|
+
return typeName.includes(' | ') && !arrayUnionRegex.test(typeName);
|
|
14
14
|
}
|
|
15
15
|
export function isObject(x) {
|
|
16
|
-
return Object.prototype.toString.call(x) ===
|
|
16
|
+
return Object.prototype.toString.call(x) === '[object Object]' && x !== null;
|
|
17
17
|
}
|
|
18
18
|
export function isDate(x) {
|
|
19
19
|
return x instanceof Date;
|
|
20
20
|
}
|
|
21
21
|
export function parseKeyMetaData(key) {
|
|
22
|
-
const isOptional = key.endsWith(
|
|
22
|
+
const isOptional = key.endsWith('--?');
|
|
23
23
|
if (isOptional) {
|
|
24
24
|
return {
|
|
25
25
|
isOptional,
|
|
26
|
-
keyValue: key.slice(0, -3)
|
|
26
|
+
keyValue: key.slice(0, -3),
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
else {
|
|
30
30
|
return {
|
|
31
31
|
isOptional,
|
|
32
|
-
keyValue: key
|
|
32
|
+
keyValue: key,
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -45,5 +45,5 @@ export function getTypeDescriptionGroup(desc) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
export function findTypeById(id, types) {
|
|
48
|
-
return types.find(_ => _.id === id);
|
|
48
|
+
return types.find((_) => _.id === id);
|
|
49
49
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -15,27 +15,3 @@ export type LanguageCode = 'SQ' | 'BE' | 'BS' | 'BG' | 'HR' | 'CS' | 'DA' | 'NL'
|
|
|
15
15
|
*/
|
|
16
16
|
export interface ContentStructure {
|
|
17
17
|
}
|
|
18
|
-
export type GetTextReturn = {
|
|
19
|
-
contentId: string;
|
|
20
|
-
text: string;
|
|
21
|
-
};
|
|
22
|
-
export type GetImageReturn = {
|
|
23
|
-
contentId: string;
|
|
24
|
-
data: ImageObject;
|
|
25
|
-
};
|
|
26
|
-
export type GetVariationReturn = {
|
|
27
|
-
contentId: string;
|
|
28
|
-
text: string;
|
|
29
|
-
};
|
|
30
|
-
export interface ImageObject {
|
|
31
|
-
contentstorage_type: 'image';
|
|
32
|
-
url: string;
|
|
33
|
-
altText: string;
|
|
34
|
-
}
|
|
35
|
-
export interface VariationData {
|
|
36
|
-
[key: string]: string;
|
|
37
|
-
}
|
|
38
|
-
export interface VariationObject {
|
|
39
|
-
contentstorage_type: 'variation';
|
|
40
|
-
data: VariationData;
|
|
41
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function flattenJson(data: any, prefix?: string, result?: Record<string, any>): Record<string, any>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function flattenJson(data, prefix = '', result = {}) {
|
|
2
|
+
const stopFlatteningIfKeyExists = 'contentstorage_type';
|
|
3
|
+
// Check if the current data is an object that should not be flattened further
|
|
4
|
+
if (typeof data === 'object' &&
|
|
5
|
+
data !== null &&
|
|
6
|
+
!Array.isArray(data) && // Must be an object, not an array
|
|
7
|
+
Object.prototype.hasOwnProperty.call(data, stopFlatteningIfKeyExists)) {
|
|
8
|
+
if (prefix) {
|
|
9
|
+
// If there's a prefix, this object is nested. Assign it directly.
|
|
10
|
+
result[prefix] = data;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
// This is the root object itself having the 'stopFlatteningIfKeyExists' key.
|
|
14
|
+
// Consistent with how root primitives are handled (result remains empty),
|
|
15
|
+
// we don't add it to the result if there's no prefix. The function's
|
|
16
|
+
// purpose is to flatten *into* key-value pairs. If the root itself
|
|
17
|
+
// is one of these "don't flatten" types, 'result' will remain empty,
|
|
18
|
+
// which is consistent with the original function's behavior for root primitives.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (typeof data === 'object' && data !== null) {
|
|
22
|
+
// It's an object or array that should be processed further
|
|
23
|
+
if (Array.isArray(data)) {
|
|
24
|
+
if (data.length === 0 && prefix) {
|
|
25
|
+
// Handle empty arrays if prefix exists
|
|
26
|
+
result[prefix] = [];
|
|
27
|
+
}
|
|
28
|
+
data.forEach((item, index) => {
|
|
29
|
+
// Recursively call, the check for 'stopFlatteningIfKeyExists' will apply to 'item'
|
|
30
|
+
flattenJson(item, prefix ? `${prefix}.${index}` : `${index}`, result);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
let isEmptyObject = true;
|
|
35
|
+
for (const key in data) {
|
|
36
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
37
|
+
isEmptyObject = false;
|
|
38
|
+
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
39
|
+
// Recursively call, the check for 'stopFlatteningIfKeyExists' will apply to 'data[key]'
|
|
40
|
+
flattenJson(data[key], newPrefix, result);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (isEmptyObject && prefix) {
|
|
44
|
+
// Handle empty objects (that were not 'special') if prefix exists
|
|
45
|
+
result[prefix] = {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (prefix) {
|
|
50
|
+
result[prefix] = data;
|
|
51
|
+
}
|
|
52
|
+
// If the initial data is a primitive and prefix is empty, result remains empty.
|
|
53
|
+
// If the initial data is a 'special' object (contains 'stopFlatteningIfKeyExists')
|
|
54
|
+
// and prefix is empty, result also remains empty based on the logic above.
|
|
55
|
+
return result;
|
|
56
|
+
}
|