@contentstorage/core 2.1.1 → 2.2.1

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 CHANGED
@@ -1,17 +1,17 @@
1
1
  # @contentstorage/core
2
2
 
3
- > CLI tool for managing translations and generating TypeScript types from ContentStorage
3
+ > CLI tool for managing translations and generating TypeScript types from Contentstorage
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@contentstorage/core.svg)](https://www.npmjs.com/package/@contentstorage/core)
6
6
  [![License](https://img.shields.io/npm/l/@contentstorage/core.svg)](https://github.com/kaidohussar/contentstorage-core/blob/master/LICENSE)
7
7
 
8
8
  ## Overview
9
9
 
10
- ContentStorage Core is a powerful CLI tool for managing translations and content. It pulls content from ContentStorage CDN, generates TypeScript types, and integrates seamlessly with popular i18n libraries.
10
+ Contentstorage Core is a powerful CLI tool for managing translations and content. It pulls content from Contentstorage CDN, generates TypeScript types, and integrates seamlessly with popular i18n libraries.
11
11
 
12
12
  ## Features
13
13
 
14
- - **Translation Management** - Pull content from ContentStorage CDN
14
+ - **Translation Management** - Pull content from Contentstorage CDN
15
15
  - **TypeScript Generation** - Automatic type generation from your content
16
16
  - **Translation Statistics** - Analyze translation completeness across languages
17
17
  - **Multi-Language Support** - Built-in support for 40+ languages
@@ -48,7 +48,7 @@ export default {
48
48
  ### 2. Pull Content & Generate Types
49
49
 
50
50
  ```bash
51
- # Pull content from ContentStorage CDN
51
+ # Pull content from Contentstorage CDN
52
52
  npx contentstorage pull
53
53
 
54
54
  # Generate TypeScript type definitions
@@ -69,7 +69,7 @@ npm install @contentstorage/plugin-react-intl
69
69
  npx contentstorage-react-intl export
70
70
  ```
71
71
 
72
- **With ContentStorage SDK (for advanced features like variations and images):**
72
+ **With Contentstorage SDK (for advanced features like variations and images):**
73
73
  ```bash
74
74
  npm install @contentstorage/sdk
75
75
  ```
@@ -78,7 +78,7 @@ npm install @contentstorage/sdk
78
78
 
79
79
  ### `contentstorage pull`
80
80
 
81
- Pull content from ContentStorage CDN
81
+ Pull content from Contentstorage CDN
82
82
 
83
83
  ```bash
84
84
  npx contentstorage pull [options]
@@ -193,13 +193,13 @@ EN, FR, DE, ES, IT, PT, NL, PL, RU, TR, SV, NO, DA, FI, CS, SK, HU, RO, BG, HR,
193
193
 
194
194
  ### Option 1: Use with i18n Libraries (Recommended for most projects)
195
195
 
196
- For standard i18n needs, use ContentStorage CLI with popular i18n libraries:
196
+ For standard i18n needs, use Contentstorage CLI with popular i18n libraries:
197
197
 
198
198
  - **[@contentstorage/plugin-i18next](https://www.npmjs.com/package/@contentstorage/plugin-i18next)** - i18next integration
199
199
  - **[@contentstorage/plugin-react-intl](https://www.npmjs.com/package/@contentstorage/plugin-react-intl)** - react-intl (FormatJS) integration
200
200
  - **[@contentstorage/plugin-vue-i18n](https://www.npmjs.com/package/@contentstorage/plugin-vue-i18n)** - Vue i18n integration
201
201
 
202
- ### Option 2: Use with ContentStorage SDK (Advanced features)
202
+ ### Option 2: Use with Contentstorage SDK (Advanced features)
203
203
 
204
204
  If you need advanced features like variations (A/B testing) and image management:
205
205
 
@@ -306,7 +306,7 @@ npm run content:sync # Pull and generate in one command
306
306
 
307
307
  ## SDK Extract
308
308
 
309
- The `/sdk-extract` folder contains the ContentStorage SDK code ready to be moved to a separate repository. This SDK provides runtime features like:
309
+ The `/sdk-extract` folder contains the Contentstorage SDK code ready to be moved to a separate repository. This SDK provides runtime features like:
310
310
  - getText/getImage/getVariation functions
311
311
  - Content variations (A/B testing)
312
312
  - Image management
@@ -335,4 +335,4 @@ Kaido Hussar - [kaidohus@gmail.com](mailto:kaidohus@gmail.com)
335
335
 
336
336
  ## Support
337
337
 
338
- For issues and questions, please [open an issue](https://github.com/kaidohussar/contentstorage-core/issues) on GitHub.
338
+ For issues and questions, please [open an issue](https://github.com/kaidohussar/contentstorage-core/issues) on GitHub.
@@ -13,6 +13,7 @@ const COMMANDS = {
13
13
  ' --content-dir <dir> Directory to save content files',
14
14
  ' --lang <code> Language code (e.g., EN, FR)',
15
15
  ' --pending-changes Fetch pending/draft content',
16
+ ' --flatten Output flattened key-value pairs',
16
17
  ],
17
18
  },
18
19
  'generate-types': {
@@ -38,7 +39,7 @@ const COMMANDS = {
38
39
  },
39
40
  };
40
41
  function showHelp() {
41
- console.log(chalk.bold('\nContentStorage CLI'));
42
+ console.log(chalk.bold('\nContentstorage CLI'));
42
43
  console.log(chalk.dim('Manage content and generate TypeScript types\n'));
43
44
  console.log(chalk.bold('Usage:'));
44
45
  console.log(' contentstorage <command> [options]\n');
@@ -5,6 +5,7 @@ import path from 'path';
5
5
  import chalk from 'chalk';
6
6
  import { loadConfig } from '../core/config-loader.js';
7
7
  import { CONTENTSTORAGE_CONFIG } from '../utils/constants.js';
8
+ import { flattenJson } from '../utils/flatten-json.js';
8
9
  export async function pullContent() {
9
10
  console.log(chalk.blue('Starting content pull...'));
10
11
  const args = process.argv.slice(2);
@@ -17,6 +18,9 @@ export async function pullContent() {
17
18
  if (key === 'pending-changes') {
18
19
  cliConfig.pendingChanges = true;
19
20
  }
21
+ else if (key === 'flatten') {
22
+ cliConfig.flatten = true;
23
+ }
20
24
  else if (value && !value.startsWith('--')) {
21
25
  if (key === 'lang') {
22
26
  cliConfig.languageCodes = [value.toUpperCase()];
@@ -102,7 +106,8 @@ export async function pullContent() {
102
106
  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
107
  }
104
108
  console.log(chalk.green(`Received JSON for ${languageCode}. Saving to ${outputPath}`));
105
- await fs.writeFile(outputPath, JSON.stringify(jsonData, null, 2));
109
+ const outputData = config.flatten ? flattenJson(jsonData) : jsonData;
110
+ await fs.writeFile(outputPath, JSON.stringify(outputData, null, 2));
106
111
  console.log(chalk.green(`Successfully saved ${outputPath}`));
107
112
  }
108
113
  catch (error) {
package/dist/types.d.ts CHANGED
@@ -4,6 +4,7 @@ export type AppConfig = {
4
4
  contentDir: string;
5
5
  typesOutputFile: string;
6
6
  pendingChanges?: boolean;
7
+ flatten?: boolean;
7
8
  };
8
9
  export type LanguageCode = 'SQ' | 'BE' | 'BS' | 'BG' | 'HR' | 'CS' | 'DA' | 'NL' | 'EN' | 'ET' | 'FI' | 'FR' | 'DE' | 'EL' | 'HU' | 'GA' | 'IT' | 'LV' | 'LT' | 'MK' | 'NO' | 'PL' | 'PT' | 'RO' | 'RU' | 'SR' | 'SK' | 'SL' | 'ES' | 'SV' | 'TR' | 'UK';
9
10
  /**
@@ -1 +1 @@
1
- export declare function flattenJson(data: any, prefix?: string, result?: Record<string, any>): Record<string, any>;
1
+ export declare function flattenJson(data: any, prefix?: string, result?: Record<string, string>): Record<string, string>;
@@ -1,56 +1,21 @@
1
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
2
+ if (typeof data === 'object' && data !== null) {
23
3
  if (Array.isArray(data)) {
24
- if (data.length === 0 && prefix) {
25
- // Handle empty arrays if prefix exists
26
- result[prefix] = [];
27
- }
28
4
  data.forEach((item, index) => {
29
- // Recursively call, the check for 'stopFlatteningIfKeyExists' will apply to 'item'
30
5
  flattenJson(item, prefix ? `${prefix}.${index}` : `${index}`, result);
31
6
  });
32
7
  }
33
8
  else {
34
- let isEmptyObject = true;
35
9
  for (const key in data) {
36
10
  if (Object.prototype.hasOwnProperty.call(data, key)) {
37
- isEmptyObject = false;
38
11
  const newPrefix = prefix ? `${prefix}.${key}` : key;
39
- // Recursively call, the check for 'stopFlatteningIfKeyExists' will apply to 'data[key]'
40
12
  flattenJson(data[key], newPrefix, result);
41
13
  }
42
14
  }
43
- if (isEmptyObject && prefix) {
44
- // Handle empty objects (that were not 'special') if prefix exists
45
- result[prefix] = {};
46
- }
47
15
  }
48
16
  }
49
17
  else if (prefix) {
50
- result[prefix] = data;
18
+ result[prefix] = String(data);
51
19
  }
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
20
  return result;
56
21
  }
package/package.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "name": "@contentstorage/core",
3
3
  "author": "Kaido Hussar <kaido@contentstorage.app>",
4
4
  "homepage": "https://contentstorage.app",
5
- "version": "2.1.1",
5
+ "version": "2.2.1",
6
6
  "type": "module",
7
- "description": "ContentStorage CLI for managing translations and generating TypeScript types",
7
+ "description": "Contentstorage CLI for managing translations and generating TypeScript types",
8
8
  "license": "MIT",
9
9
  "bin": {
10
10
  "contentstorage": "dist/commands/cli.js"
@@ -1,4 +0,0 @@
1
- export declare const CONTENTSTORAGE_CONFIG: {
2
- BASE_URL: string;
3
- API_URL: string;
4
- };
@@ -1,4 +0,0 @@
1
- export const CONTENTSTORAGE_CONFIG = {
2
- BASE_URL: 'https://cdn.contentstorage.app',
3
- API_URL: 'https://api.contentstorage.app',
4
- };
@@ -1,6 +0,0 @@
1
- import { AppConfig } from '../types.js';
2
- /**
3
- * Helper function to define your application configuration.
4
- * Provides autocompletion and type-checking for contentstorage.config.js files.
5
- */
6
- export declare function defineConfig(config: AppConfig): AppConfig;
@@ -1,19 +0,0 @@
1
- /**
2
- * Helper function to define your application configuration.
3
- * Provides autocompletion and type-checking for contentstorage.config.js files.
4
- */
5
- export function defineConfig(config) {
6
- // You can add basic runtime validation here if desired,
7
- // e.g., check if contentUrl is a valid URL format,
8
- // or if languageCodes is not empty.
9
- if (!config.languageCodes || config.languageCodes.length === 0) {
10
- console.warn('Warning: languageCodes array is empty or missing in the configuration.');
11
- }
12
- if (!config.contentDir) {
13
- // This would typically be a hard error, but defineConfig is more for type safety at edit time.
14
- // Runtime validation (see point 3) is better for hard errors.
15
- console.warn('Warning: contentDir is missing in the configuration.');
16
- }
17
- // ... other checks
18
- return config;
19
- }
@@ -1 +0,0 @@
1
- export declare function flattenJson(data: any, prefix?: string, result?: Record<string, any>): Record<string, any>;
@@ -1,56 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export declare function populateTextWithVariables(text: string, variables: Record<string, any>, textKey: string): string;
@@ -1,10 +0,0 @@
1
- export function populateTextWithVariables(text, variables, textKey) {
2
- return text.replace(/\{(\w+)}/g, (placeholder, variableName) => {
3
- if (Object.prototype.hasOwnProperty.call(variables, variableName)) {
4
- const value = variables[variableName];
5
- return String(value);
6
- }
7
- console.warn(`[getText] Variable "${variableName}" for text id "${textKey}" not found in provided variables. Placeholder "${placeholder}" will be replaced with an empty string.`);
8
- return placeholder;
9
- });
10
- }
package/dist/index.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { AppConfig, LanguageCode, ContentStructure } from './types.js';
2
- import { setContentLanguage, getText, getImage, getVariation, initContentStorage } from './lib/contentManagement.js';
3
- import { fetchContent } from './lib/functions/fetchContent.js';
4
- export { AppConfig, LanguageCode, ContentStructure };
5
- export { initContentStorage, fetchContent, setContentLanguage, getText, getImage, getVariation, liveEditorReady, };
6
- declare function liveEditorReady(retries?: number, delay?: number): Promise<boolean>;
package/dist/index.js DELETED
@@ -1,62 +0,0 @@
1
- import { CONTENTSTORAGE_CONFIG } from './contentstorage-config.js';
2
- import { setContentLanguage, getText, getImage, getVariation, initContentStorage, } from './lib/contentManagement.js';
3
- import { fetchContent } from './lib/functions/fetchContent.js';
4
- export { initContentStorage, fetchContent, setContentLanguage, getText, getImage, getVariation, liveEditorReady, };
5
- let liveEditorReadyPromise = null;
6
- async function isLiveEditorMode() {
7
- try {
8
- const inIframe = window.self !== window.top;
9
- const urlParams = new URLSearchParams(window.location.search);
10
- const iframeMarkerFromParent = urlParams.get('contentstorage_live_editor');
11
- return !!(inIframe && iframeMarkerFromParent);
12
- }
13
- catch (e) {
14
- console.warn('Error accessing window.top:', e);
15
- return false;
16
- }
17
- }
18
- function liveEditorReady(retries = 2, delay = 3000) {
19
- if (liveEditorReadyPromise) {
20
- return liveEditorReadyPromise;
21
- }
22
- liveEditorReadyPromise = new Promise(async (resolve) => {
23
- const isLiveMode = await isLiveEditorMode();
24
- if (!isLiveMode) {
25
- resolve(false);
26
- return;
27
- }
28
- const cdnScriptUrl = `${CONTENTSTORAGE_CONFIG.BASE_URL}/live-editor.js?contentstorage-live-editor=true`;
29
- const loadScript = (attempt = 1) => {
30
- console.log(`Attempting to load Contentstorage live editor script (attempt ${attempt}/${retries})`);
31
- const scriptElement = document.createElement('script');
32
- scriptElement.type = 'text/javascript';
33
- scriptElement.src = cdnScriptUrl;
34
- scriptElement.onload = () => {
35
- console.log(`Script loaded successfully from: ${cdnScriptUrl}`);
36
- resolve(true);
37
- };
38
- scriptElement.onerror = (error) => {
39
- // Clean up the failed script element to avoid clutter
40
- scriptElement.remove();
41
- console.error(`Failed to load script (attempt ${attempt}/${retries})`, error);
42
- if (attempt < retries) {
43
- setTimeout(() => loadScript(attempt + 1), delay);
44
- }
45
- else {
46
- console.error(`All ${retries} attempts to load the script failed.`);
47
- resolve(false);
48
- }
49
- };
50
- document.head.appendChild(scriptElement);
51
- };
52
- loadScript();
53
- });
54
- return liveEditorReadyPromise;
55
- }
56
- if (typeof window !== 'undefined') {
57
- liveEditorReady().then((result) => {
58
- if (result === true) {
59
- console.log('Contentstorage live editor script loaded!');
60
- }
61
- });
62
- }
@@ -1,2 +0,0 @@
1
- import { AppConfig } from '../types.js';
2
- export declare function loadConfig(): Promise<AppConfig>;
@@ -1,42 +0,0 @@
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,32 +0,0 @@
1
- import { AppConfig, ContentStructure, GetImageReturn, GetTextReturn, GetVariationReturn, LanguageCode } from '../types.js';
2
- export declare let activeContent: object | null;
3
- export declare let appConfig: Pick<AppConfig, 'contentKey' | 'languageCodes' | 'pendingChanges'> | null;
4
- /**
5
- * Loads and sets the content for a specific language.
6
- * It will internally ensure the application configuration (for contentDir) is loaded.
7
- * @param contentJson
8
- * Language code which is used for live editor to manage pending changes
9
- * @param languageCode
10
- */
11
- export declare function setContentLanguage({ languageCode, contentJson, }: {
12
- languageCode: LanguageCode;
13
- contentJson: object;
14
- }): void;
15
- export declare function initContentStorage(config: Pick<AppConfig, 'contentKey' | 'languageCodes'>): void;
16
- /**
17
- * Retrieves the text string from the loaded JSON content for the given path.
18
- * Autocompletion for pathString is enabled via module augmentation of CustomContentStructure.
19
- * `setContentLanguage()` must be called successfully before using this.
20
- *
21
- * @param contentId A dot-notation path string (e.g., 'HomePage.Login'). Autocompletion is provided.
22
- * @param variables Variables help to render dynamic content inside text strings
23
- * If not provided, and path is not found/value not string, undefined is returned.
24
- * @returns The text string from the JSON, or the fallbackValue, or undefined.
25
- */
26
- export declare function getText<Path extends keyof ContentStructure>(contentId: Path, variables?: ContentStructure[Path] extends {
27
- variables: infer Vars;
28
- } ? keyof Vars : Record<string, any>): GetTextReturn;
29
- export declare function getImage(contentId: keyof ContentStructure): GetImageReturn | undefined;
30
- export declare function getVariation<Path extends keyof ContentStructure>(contentId: Path, variationKey?: ContentStructure[Path] extends {
31
- data: infer D;
32
- } ? keyof D : string, variables?: Record<string, any>): GetVariationReturn;