Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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) => {
Expand Down
154 changes: 154 additions & 0 deletions test/parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});