diff --git a/src/parser.js b/src/parser.js index a1a5e51..1652570 100644 --- a/src/parser.js +++ b/src/parser.js @@ -11,6 +11,7 @@ import { parse } from 'esprima-next'; import fs from 'fs'; import i18next from 'i18next'; import _ from 'lodash'; +import path from 'path'; import parse5 from 'parse5'; import sortObject from 'sortobject'; import jsxwalk from './acorn-jsx-walk'; @@ -61,6 +62,8 @@ const defaults = { defaultValue: '', // default value used if not passed to `parser.set` + dynamicNamespaceCreation: false, // automatically create namespace files during parsing if they don't already exist + // resource resource: { // The path where resources get loaded from. Relative to current working directory. @@ -323,6 +326,34 @@ class Parser { .replace(regex.ns, ns); } + createNamespaceFile(lng, ns) { + const resPath = this.formatResourceLoadPath(lng, ns); + + try { + // Create directory structure if needed + const dirPath = path.dirname(resPath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + // Create empty JSON file if it doesn't exist + if (!fs.existsSync(resPath)) { + fs.writeFileSync(resPath, '{}', 'utf-8'); + this.log(chalk.green(`Created namespace file: ${chalk.yellow(resPath)}`)); + } + + // Initialize resStore and resScan for this namespace + this.resStore[lng][ns] = {}; + this.resScan[lng][ns] = {}; + + return true; + } catch (err) { + this.error(`Unable to create namespace file while parsing: ${chalk.yellow(JSON.stringify(resPath))}: lng=${lng}, ns=${ns}`); + this.error(err); + return false; + } + } + fixStringAfterRegExpAsArray(strToFix) { let fixedString = _.trim(strToFix); const firstChar = fixedString[0]; @@ -953,8 +984,19 @@ class Parser { let resScan = this.resScan[lng] && this.resScan[lng][ns]; if (!_.isPlainObject(resLoad)) { // Skip undefined namespace - this.error(`${chalk.yellow(JSON.stringify(ns))} does not exist in the namespaces (${chalk.yellow(JSON.stringify(this.options.ns))}): key=${chalk.yellow(JSON.stringify(key))}, options=${chalk.yellow(JSON.stringify(options))}`); - return; + // Early error escape if dynamic namespace creation was not enabled + if (!this.options.dynamicNamespaceCreation) { + this.error(`${chalk.yellow(JSON.stringify(ns))} does not exist in the namespaces (${chalk.yellow(JSON.stringify(this.options.ns))}): key=${chalk.yellow(JSON.stringify(key))}, options=${chalk.yellow(JSON.stringify(options))}`); + return; + } + if (!this.createNamespaceFile(lng, ns)) { + // Failed to create namespace, skip this language+ns combination + return; + } else { + // Namespace was created successfully, update references and continue + resLoad = this.resStore[lng][ns]; + resScan = this.resScan[lng][ns]; + } } Object.keys(keys).forEach((index) => { diff --git a/test/parser.test.js b/test/parser.test.js index 8e7295c..a194481 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -1265,3 +1265,157 @@ test('Should support fallback keys', () => { } }); }); + +describe('Dynamic Namespace Creation', () => { + const tempDir = path.resolve(__dirname, 'temp'); + + beforeEach(() => { + // Clean up temp directory before each test + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + fs.mkdirSync(tempDir, { recursive: true }); + }); + + afterEach(() => { + // Clean up temp directory after each test + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test('should dynamically create namespace files when enabled', () => { + const parser = new Parser({ + lngs: ['en', 'fr'], + ns: [], // Empty namespace array to test dynamic creation + dynamicNamespaceCreation: true, + resource: { + loadPath: path.join(tempDir, '{{lng}}/{{ns}}.json'), + savePath: path.join(tempDir, '{{lng}}/{{ns}}.json') + } + }); + + // Try to set a key with a new namespace + parser.set('newNamespace:test.key', { defaultValue: 'Test value' }); + + // Check that namespace files were created + const enPath = path.join(tempDir, 'en/newNamespace.json'); + const frPath = path.join(tempDir, 'fr/newNamespace.json'); + + expect(fs.existsSync(enPath)).toBe(true); + expect(fs.existsSync(frPath)).toBe(true); + + // Check file contents are empty JSON objects + expect(JSON.parse(fs.readFileSync(enPath, 'utf-8'))).toEqual({}); + expect(JSON.parse(fs.readFileSync(frPath, 'utf-8'))).toEqual({}); + + // Check that the key was properly set in resStore + const result = parser.get(); + expect(result.en.newNamespace.test.key).toBe('Test value'); + expect(result.fr.newNamespace.test.key).toBe('Test value'); + }); + + test('should not create namespace files when dynamicNamespaceCreation is disabled', () => { + const parser = new Parser({ + lngs: ['en'], + ns: [], // Empty namespace array + dynamicNamespaceCreation: false, + resource: { + loadPath: path.join(tempDir, '{{lng}}/{{ns}}.json'), + savePath: path.join(tempDir, '{{lng}}/{{ns}}.json') + } + }); + + // Capture error output + const originalError = parser.error; + const errors = []; + parser.error = (...args) => errors.push(args.join(' ')); + + // Try to set a key with a new namespace + parser.set('newNamespace:test.key', { defaultValue: 'Test value' }); + + // Check that namespace file was NOT created + const enPath = path.join(tempDir, 'en/newNamespace.json'); + expect(fs.existsSync(enPath)).toBe(false); + + // Check that error was logged + expect(errors.length).toBeGreaterThan(0); + expect(errors[0]).toContain('does not exist in the namespaces'); + + // Restore original error function + parser.error = originalError; + }); + + test('should create directory structure recursively', () => { + const deepPath = path.join(tempDir, 'deep/nested/structure'); + const parser = new Parser({ + lngs: ['en'], + ns: [], + dynamicNamespaceCreation: true, + resource: { + loadPath: path.join(deepPath, '{{lng}}/{{ns}}.json'), + savePath: path.join(deepPath, '{{lng}}/{{ns}}.json') + } + }); + + parser.set('test:key', { defaultValue: 'Test value' }); + + const filePath = path.join(deepPath, 'en/test.json'); + expect(fs.existsSync(filePath)).toBe(true); + expect(fs.existsSync(path.join(deepPath, 'en'))).toBe(true); + }); + + test('should handle existing files gracefully', () => { + // Pre-create the namespace file with existing content + const enDir = path.join(tempDir, 'en'); + fs.mkdirSync(enDir, { recursive: true }); + const enPath = path.join(enDir, 'existing.json'); + fs.writeFileSync(enPath, JSON.stringify({ existingKey: 'existing value' }), 'utf-8'); + + const parser = new Parser({ + lngs: ['en'], + ns: ['existing'], + dynamicNamespaceCreation: true, + resource: { + loadPath: path.join(tempDir, '{{lng}}/{{ns}}.json'), + savePath: path.join(tempDir, '{{lng}}/{{ns}}.json') + } + }); + + // The existing file should be loaded normally + expect(parser.get().en.existing.existingKey).toBe('existing value'); + + // Now test dynamic creation of a new namespace + parser.set('newNs:newKey', { defaultValue: 'new value' }); + + const newPath = path.join(tempDir, 'en/newNs.json'); + expect(fs.existsSync(newPath)).toBe(true); + expect(parser.get().en.newNs.newKey).toBe('new value'); + }); + + test('should handle file creation errors gracefully', () => { + const parser = new Parser({ + lngs: ['en'], + ns: [], + dynamicNamespaceCreation: true, + resource: { + loadPath: '/invalid/readonly/path/{{lng}}/{{ns}}.json', + savePath: '/invalid/readonly/path/{{lng}}/{{ns}}.json' + } + }); + + // Capture error output + const originalError = parser.error; + const errors = []; + parser.error = (...args) => errors.push(args.join(' ')); + + parser.set('test:key', { defaultValue: 'Test value' }); + + // Check that errors were logged + expect(errors.length).toBeGreaterThan(0); + expect(errors.some(error => error.includes('Unable to create namespace file'))).toBe(true); + + // Restore original error function + parser.error = originalError; + }); +});