diff --git a/README.md b/README.md
index f07c7e7..a29a011 100644
--- a/README.md
+++ b/README.md
@@ -11,11 +11,13 @@ A library for scanning javscript files to build translation mappings in json aut
## Why use this?
- You no longer have to manage hierarchies of translations
-- Templates are automatically generated for the translators
-- The translations are noted if they are new, unused and what files
-- It allows splitting the translations easily for dynamic imports to allow sliced loading
-- Any string wrapped in `__()` or `__n()`, will be picked up as a
+- Designed for architectures leveraging dynamic imports, allowing splitting of the translations based off of file structure
+- Templates are automatically generated for the translators where they only need to fill in the blanks
+- The translations are annoted if they are new or unused as well as the file names and line numbers of usages
+- Easy auditing for missing or non-updated translation strings with never running your application or enlisting QA
+- Any string wrapped in `__()` or `__n()` or `__p()` or `__np()`, will be picked up as a
translatable making usage extremely easy for developers
+- Works similarly to the venerable [gettext](https://en.wikipedia.org/wiki/Gettext). Any translation strategies that work for that library work for this library.
## What does it do?
@@ -36,7 +38,7 @@ yarn add @zakkudo/translation-static-analyzer
```
## Setup
-1. Wrap strings you want to be translated in `__('text')` or `__n('singlular', 'plural', number)` using a library like `@zakkudo/translator`
+1. Wrap strings you want to be translated in `__('text')` or `__n('singlular', 'plural', number)` or `__p('context', 'text')` or `__np('context', 'singular', 'plural', number)` using a library like `@zakkudo/translator`
2. Initialize the analyzer in your build scripts similar to below.
``` javascript
const TranslationStaticAnalyzer = require('@zakkudo/translation-static-analyzer');
@@ -84,21 +86,35 @@ File Structure
Where `locales/fr.json` will look like this for use by your translators:
``` json5
{
- // NEW
- // src/pages/AboutPage/index.js:14
- "About": "",
- // UNUSED
- "Search Page": "French translation",
- // src/pages/AboutPage/index.js:40
- "There is one user": {"one":"French translation", "other":"French translation"},
- // src/pages/AboutPage/index.js:38
- "Welcome to the about page!": "French translation"
+ "About": {
+ // NEW
+ // src/pages/AboutPage/index.js:14
+ "default": ""
+ },
+ "Search": {
+ // UNUSED
+ "default": "French translation",
+ // UNUSED
+ "menuitem": "French translation"
+ },
+ "There is one user": {
+ // src/pages/AboutPage/index.js:40
+ "default": {"one":"French translation", "other":"French translation"},
+ },
+ "Welcome to the about page!": {
+ // src/pages/AboutPage/index.js:38
+ "default": "French translation"
+ }
}
```
And the optimized `src/.locales/fr.json` will look like this for use by your developers:
``` json
{
+ "Search": {
+ "default": "French translation",
+ "menuitem": "French translation"
+ },
"There is one user": {"one":"French translation", "other":"French translation"},
"Welcome to the about page!": "French translation"
}
diff --git a/__mocks__/filesystem.js b/__mocks__/filesystem.js
index a358976..efb67f3 100644
--- a/__mocks__/filesystem.js
+++ b/__mocks__/filesystem.js
@@ -20,8 +20,7 @@ export default class SearchPage extends Component {
}
static get template() {
- return '{{__('invalid''string')}}
{{__n('%d result', '%d results', 2)}}
';
-
+ return '{{__('invalid''string')}} {{__p('menuitem', 'Search')}} {{__n('%d result', '%d results', 2)}}
{{__np('footer', '%d view', '%d views', 23)}}';
}
};
`.trim();
@@ -34,16 +33,25 @@ export default class Application extends Component {
};
`.trim();
+const EmptyKeysPage = `
+export default class Application extends Component {
+ static get title() {
+ return __('') + __n('') + __np('') + __p('');
+ }
+};
+`.trim();
+
module.exports = {
'src/pages/Search/index.js': SearchPage,
'src/pages/About/index.js': AboutPage,
'src/pages/Empty/index.js': EmptyPage,
+ 'src/pages/EmptyKeys/index.js': EmptyKeysPage,
'src/test.js': EmptyPage,
'src/index.js': Application,
'./locales/existing.json': JSON.stringify({
- "Search": "検索",
- "test unused key": "test value",
- "Application": "アプリケーション",
+ "Search": {"default": "検索"},
+ "test unused key": {"default": "test value"},
+ "Application": {"default": "アプリケーション"},
}),
'src/pages/About/.locales/existing.json': JSON.stringify({}),
'src/pages/Search/.locales/existing.json': JSON.stringify({'Search': ''}),
diff --git a/__mocks__/glob.js b/__mocks__/glob.js
index d27d90f..ea99b73 100644
--- a/__mocks__/glob.js
+++ b/__mocks__/glob.js
@@ -36,6 +36,10 @@ glob.sync.mockImplementation((pattern) => {
'src/pages/Search',
'src/pages/About',
];
+ } else if (pattern === 'test empty keys') {
+ return [
+ 'src/pages/EmptyKeys/index.js',
+ ];
}
return [];
diff --git a/__mocks__/y18n.js b/__mocks__/y18n.js
deleted file mode 100644
index b3b12bf..0000000
--- a/__mocks__/y18n.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const y18n = jest.genMockFromModule('y18n');
-const filesystem = require('./filesystem');
-
-y18n.mockImplementation(() => {
- const instance = {
- cache: {
- template: {
- "Search": "",
- "%s result": {
- "one": "",
- "other": "",
- },
- "About": "",
- "Application": ""
- },
- },
- __(k) {
- const cache = this.cache || {};
- const localization = cache['template'] = cache['template'] || {};
-
- if (!localization.hasOwnProperty(k)) {
- localization[k] = localization[k] || ''
- }
- },
- __n(singular, plural) {
- const cache = this.cache || {};
- const localization = cache['template'] = cache['template'] || {};
-
- if (!localization.hasOwnProperty(singular)) {
- localization[singular] = localization[singular] || {'one': singular, 'other': plural}
- }
- }
- };
-
- instance.__ = instance.__.bind(instance);
- instance.__n = instance.__n.bind(instance);
-
- return instance;
-});
-
-module.exports = y18n;
diff --git a/package.json b/package.json
index 7e79383..b909d54 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "0.0.17",
"description": "A library for generating localization files using static analysis of source files similar to gettext",
"keywords": [
+ "gettext",
"i18n",
"internationalization",
"l10n",
@@ -43,8 +44,7 @@
"fs-extra": "^7.0.0",
"glob": "^7.1.2",
"json5": "^1.0.1",
- "safe-eval": "^0.4.1",
- "y18n": "^4.0.0"
+ "safe-eval": "^0.4.1"
},
"scripts": {
"build": "scripts/build.sh",
diff --git a/src/README.md b/src/README.md
index 9fe1ad7..905a4e7 100644
--- a/src/README.md
+++ b/src/README.md
@@ -11,11 +11,13 @@ A library for scanning javscript files to build translation mappings in json aut
## Why use this?
- You no longer have to manage hierarchies of translations
-- Templates are automatically generated for the translators
-- The translations are noted if they are new, unused and what files
-- It allows splitting the translations easily for dynamic imports to allow sliced loading
-- Any string wrapped in `__()` or `__n()`, will be picked up as a
+- Designed for architectures leveraging dynamic imports, allowing splitting of the translations based off of file structure
+- Templates are automatically generated for the translators where they only need to fill in the blanks
+- The translations are annoted if they are new or unused as well as the file names and line numbers of usages
+- Easy auditing for missing or non-updated translation strings with never running your application or enlisting QA
+- Any string wrapped in `__()` or `__n()` or `__p()` or `__np()`, will be picked up as a
translatable making usage extremely easy for developers
+- Works similarly to the venerable [gettext](https://en.wikipedia.org/wiki/Gettext). Any translation strategies that work for that library work for this library.
## What does it do?
@@ -36,7 +38,7 @@ yarn add @zakkudo/translation-static-analyzer
```
## Setup
-1. Wrap strings you want to be translated in `__('text')` or `__n('singlular', 'plural', number)` using a library like `@zakkudo/translator`
+1. Wrap strings you want to be translated in `__('text')` or `__n('singlular', 'plural', number)` or `__p('context', 'text')` or `__np('context', 'singular', 'plural', number)` using a library like `@zakkudo/translator`
2. Initialize the analyzer in your build scripts similar to below.
``` javascript
const TranslationStaticAnalyzer = require('@zakkudo/translation-static-analyzer');
@@ -84,21 +86,35 @@ File Structure
Where `locales/fr.json` will look like this for use by your translators:
``` json5
{
- // NEW
- // src/pages/AboutPage/index.js:14
- "About": "",
- // UNUSED
- "Search Page": "French translation",
- // src/pages/AboutPage/index.js:40
- "There is one user": {"one":"French translation", "other":"French translation"},
- // src/pages/AboutPage/index.js:38
- "Welcome to the about page!": "French translation"
+ "About": {
+ // NEW
+ // src/pages/AboutPage/index.js:14
+ "default": ""
+ },
+ "Search": {
+ // UNUSED
+ "default": "French translation",
+ // UNUSED
+ "menuitem": "French translation"
+ },
+ "There is one user": {
+ // src/pages/AboutPage/index.js:40
+ "default": {"one":"French translation", "other":"French translation"},
+ },
+ "Welcome to the about page!": {
+ // src/pages/AboutPage/index.js:38
+ "default": "French translation"
+ }
}
```
And the optimized `src/.locales/fr.json` will look like this for use by your developers:
``` json
{
+ "Search": {
+ "default": "French translation",
+ "menuitem": "French translation"
+ },
"There is one user": {"one":"French translation", "other":"French translation"},
"Welcome to the about page!": "French translation"
}
diff --git a/src/hasTranslation.js b/src/hasTranslation.js
index feb487b..a52007b 100644
--- a/src/hasTranslation.js
+++ b/src/hasTranslation.js
@@ -1,3 +1,23 @@
+/**
+ * @private
+ */
+function singularHasTranslation(data) {
+ return Boolean(data.length);
+}
+
+/**
+ * @private
+ */
+function pluralHasTranslation(data) {
+ return Object.values(data || {}).some((v) => {
+ if (Object(v) === v) {
+ return pluralHasTranslation(v);
+ }
+
+ return singularHasTranslation(v);
+ });
+}
+
/**
* Checks of a translation key-value pair has been created.
* @param {*} data - An object that is spidered looking for
@@ -6,23 +26,9 @@
* @private
*/
module.exports = function hasTranslation(data) {
- if (Object(data) === data) {
- const keys = Object.keys(data);
-
- if (!keys.length) {
- return false;
- }
-
- return keys.some((k) => {
- if (typeof data[k] === 'string') {
- return Boolean(data[k].length);
- } else {
- return hasTranslation(data[k]);
- }
- });
- } else if (typeof data === 'string') {
- return Boolean(data.length);
+ if (typeof data === 'string') {
+ return singularHasTranslation(data);
}
- return false;
+ return pluralHasTranslation(data);
}
diff --git a/src/index.js b/src/index.js
index a6d4d9b..a5e1371 100644
--- a/src/index.js
+++ b/src/index.js
@@ -7,32 +7,68 @@ const safeEval = require('safe-eval');
const equal = require('deep-equal');
const fs = require('fs-extra');
const glob = require('glob');
-const os = require('os');
+const querystring = require('querystring');
const path = require('path');
-const y18n = require('y18n');
const console = require('console');
const hasTranslation = require('./hasTranslation');
-const scrubLocalization = require('./scrubLocalization');
const readString = require('./readString');
const isSubPath = require('./isSubPath');
const name = 'translation-static-analyzer';
+
/**
* @private
*/
-function print(message, ...leftover) {
- console.log(`${name}: ${message}`, ...leftover);
+function toKeyWithContext(key, context = 'default') {
+ return `${querystring.escape(key)}:${querystring.escape(context)}`;
+}
+
+/**
+ * @private
+ */
+function fromKeyWithContext(keyWithContext) {
+ const [key, value] = String(keyWithContext).split(':');
+
+ return [
+ querystring.unescape(key),
+ querystring.unescape(value),
+ ];
+}
+
+/**
+ * @private
+ */
+function __(key) {
+ return [toKeyWithContext(key), ""];
+}
+
+/**
+ * @private
+ */
+function __p(context, key) {
+ return [toKeyWithContext(key, context), ""];
}
+/**
+ * @private
+ */
+function __n(singular) {
+ return [toKeyWithContext(singular), {one: "", other: ""}];
+}
/**
* @private
*/
-function cleanup() {
- const localeGen = this.localeGen;
+function __np(context, singular) {
+ return [toKeyWithContext(singular, context), {one: "", other: ""}];
+}
- fs.removeSync(localeGen.directory);
+/**
+ * @private
+ */
+function print(message, ...leftover) {
+ console.log(`${name}: ${message}`, ...leftover);
}
/**
@@ -103,28 +139,68 @@ function calculateFiles(requestFiles) {
* @private
*/
function serializeLocalizationWithMetaData(localizationWithMetadata) {
- const keys = Object.keys(localizationWithMetadata);
- const length = keys.length;
+ const translations = Object.values(localizationWithMetadata).map((t) => {
+ const files = t.files.map((f) => {
+ return path.relative(templateDirectory, f)
+ });
+
+ return Object.assign({}, t, {files});
+ }).sort((a, b) => {
+ return a.key.localeCompare(b.key) || a.context.localeCompare(b.context);
+ });
const templateDirectory = path.resolve(this.templateDirectory, '..');
+ const indent = ' ';
+ const lines = ['{'];
+ let previousKey = translations[0].key;
+
+ /*
+ * EXAMPLE
+ * {
+ * "English": {
+ * // filename:number
+ * "default": "French"
+ * }
+ * }
+ *
+ */
+ return translations.reduce((lines, t, i) => {
+ const {key, context, data, files, note} = t;
+ const newLines = [];
- return keys.reduce((serialized, k, i) => {
- const hasMore = i < length - 1;
- const indent = ' ';
- const note = localizationWithMetadata[k].note;
- const formattedNote = note ? `${indent}// ${note.toUpperCase()}\n` : '';
- const files = localizationWithMetadata[k].files.map((f) => {
- return path.relative(templateDirectory, f)
+ if (i === 0) {
+ newLines.push(`${indent}"${key}": {`);
+ }
+
+ if (previousKey !== key) {
+ newLines.push(`${indent}},`);
+ newLines.push(`${indent}"${key}": {`);
+ } else if (i > 0) {
+ const length = lines.length;
+ const last = length - 1;
+
+ lines[last] = lines[last] + ',';
+ }
+
+ if (note) {
+ newLines.push(`${indent}${indent}// ${note.toUpperCase()}`);
+ }
+
+ files.forEach((f) => {
+ newLines.push(`${indent}${indent}// ${f}`);
});
- const formattedFiles = files.length ? `${indent}// ` + files.join(`\n${indent}// `) + '\n' : '';
- return `${serialized}${formattedNote}${formattedFiles}${indent}"${k}": ${JSON.stringify(localizationWithMetadata[k].data)}${hasMore ? ',' : ''}\n`;
- }, '{\n') + '}';
+ newLines.push(`${indent}${indent}"${context}": ${JSON.stringify(data)}`);
+
+ previousKey = key;
+
+ return lines.concat(newLines);
+ }, lines).concat([`${indent}}`, '}']).join('\n');
}
/**
* @private
*/
-function readJSON5FileWithFallback(filename, fallback = null) {
+function readJSON5FileWithFallback(filename, fallback = {}) {
let data = fallback;
try {
@@ -138,14 +214,68 @@ function readJSON5FileWithFallback(filename, fallback = null) {
return data;
}
+/**
+ * Combines context and key strings from two levels of objects
+ * into one with the key/context concatenated together.
+ * @private
+ */
+function flattenLocalization(localization) {
+ return Object.entries(localization).reduce((accumulator, [k, v]) => {
+ if (Object(v) === v) {
+ const translations = {};
+
+ Object.entries(v).forEach(([context, translation]) => {
+ translations[toKeyWithContext(k, context)] = translation;
+ });
+
+ return Object.assign(accumulator, translations);
+ }
+
+ return accumulator;
+ }, {});
+}
+
+/**
+ * @private
+ */
+function unflattenLocalization(localization) {
+ return Object.entries(localization).reduce((accumulator, [keyWithContext, translation]) => {
+ const [key, context] = fromKeyWithContext(keyWithContext);
+ const contexts = accumulator[key] || {};
+
+ return Object.assign({}, accumulator, {
+ [key]: Object.assign({}, contexts, {[context]: translation})
+ });
+ }, {});
+}
+
+/**
+ * Compresses the translation for use by code, removing any extra context information
+ * when not needed. (If there is only a default context, the context object is removed
+ * and the translation is linked directly to the key for example.)
+ * @private
+ */
+function collapseLocalization(localization) {
+ return Object.entries(localization).reduce((accumulator, [key, contexts]) => {
+ const keys = new Set(Object.keys(contexts));
+
+ if (keys.size === 1 && keys.has('default')) {
+ return Object.assign({}, accumulator, {[key]: contexts.default});
+ }
+
+ return Object.assign({}, accumulator, {[key]: contexts});
+ }, {});
+}
+
/**
* @private
*/
function readLocalization(locale) {
const directory = this.templateDirectory;
const filename = `${directory}/${locale}.json`;
+ const data = readJSON5FileWithFallback.call(this, filename);
- return readJSON5FileWithFallback.call(this, filename);
+ return flattenLocalization(data);
}
/**
@@ -168,7 +298,7 @@ function writeLocalizationWithMetadata(locale, localization) {
* @private
*/
function updateLocalization(localization) {
- const template = scrubLocalization(this.instance.cache.template);
+ const template = this.referenceTemplate;
const filenamesByKey = this.filenamesByKey;
const keys = [
...new Set(Object.keys(localization).concat(Object.keys(template)))
@@ -177,14 +307,18 @@ function updateLocalization(localization) {
return keys.reduce((accumulator, k) => {
const files = [...(filenamesByKey.get(k) || fallbackFiles)].sort();
+
const localizationHasTranslation = hasTranslation(localization[k]);
const templateHasProperty = template.hasOwnProperty(k);
+ const [key, context] = fromKeyWithContext(k);
if (!templateHasProperty && localizationHasTranslation) {
return Object.assign({}, accumulator, {
[k]: {
note: 'unused',
files,
+ key,
+ context,
data: localization[k]
}
});
@@ -193,6 +327,8 @@ function updateLocalization(localization) {
[k]: {
note: 'new',
files,
+ key,
+ context,
data: template[k]
}
});
@@ -200,6 +336,8 @@ function updateLocalization(localization) {
return Object.assign({}, accumulator, {
[k]: {
files,
+ key,
+ context,
data: localization[k]
}
});
@@ -209,6 +347,19 @@ function updateLocalization(localization) {
}, {});
}
+/**
+ * @private
+ */
+function stripMetadata(localizationWithMetadata) {
+ const pairs = Object.entries(localizationWithMetadata);
+
+ return pairs.reduce((accumulator, [keyWithContext, m]) => {
+ return Object.assign(accumulator, {
+ [keyWithContext]: m.data
+ });
+ }, {});
+}
+
/**
* @private
*/
@@ -224,19 +375,21 @@ function generateLocaleFiles() {
locales.forEach((l) => {
const localizationWithMetadata = previousLocalizationWithMetadataByLanguage.get(l);
- const localization = readLocalization.call(this, l) || {};
+ const localization = readLocalization.call(this, l);
const nextLocalizationWithMetadata = updateLocalization.call(this, localization);
- const pairs = Object.entries(nextLocalizationWithMetadata);
- const nextLocalization = pairs.reduce((accumulator, [k, v]) => {
- return Object.assign({}, accumulator, {[k]: v.data});
- }, {});
+ const nextLocalization = stripMetadata(nextLocalizationWithMetadata)
+ const sourceCodeChangeUpdatedLocalization = !equal(
+ localizationWithMetadata,
+ nextLocalizationWithMetadata
+ );
+ const translationChangeUpdatedLocalization = !equal(
+ localization,
+ nextLocalization
+ );
localizationByLanguage.set(l, nextLocalization);
localizationWithMetadataByLanguage.set(l, nextLocalizationWithMetadata);
- const sourceCodeChangeUpdatedLocalization = !equal(localizationWithMetadata, nextLocalizationWithMetadata);
- const translationChangeUpdatedLocalization = !equal(localization, nextLocalization);
-
if (sourceCodeChangeUpdatedLocalization || translationChangeUpdatedLocalization) {
writeLocalizationWithMetadata.call(this, l, nextLocalizationWithMetadata);
changed = true;
@@ -246,58 +399,50 @@ function generateLocaleFiles() {
return changed;
}
-/**
- * @private
- */
-function clear() {
- try {
- fs.unlinkSync(this.template.name);
- } catch (e) {
- // No content
- }
-
- this.instance.cache.template = {};
-}
-
/**
* @private
*/
function rebuildCache() {
const filenamesByKey = this.filenamesByKey = new Map();
+ const keysByFilename = this.keysByFilename = new Map();
+ const referenceTemplate = this.referenceTemplate = {};
const options = this.options;
const sourceByFilename = this.sourceByFilename;
- clear.call(this);
-
this.files.all.forEach((m) => {
const contents = sourceByFilename.get(m);
+ const addedKeysWithContext = new Set();
if (contents && typeof contents === 'string') {
const metadata = readString(contents);
- const keysByFilename = this.keysByFilename;
- const keys = Object.keys(metadata);
- const {__, __n} = this.instance;
-
- Object.values(metadata).forEach((v) => {
- try {
- safeEval(v.fn, {__, __n});
- } catch(e) {
- console.warn(e);
- }
- });
- keys.forEach((k) => {
- const {lineNumber} = metadata[k];
+ Object.values(metadata).forEach((translations) => {
+ translations.forEach((t) => {
+ try {
+ const {lineNumber, fn} = t;
+ const [
+ keyWithContext,
+ placeholderTranslation,
+ ] = safeEval(fn, {__, __n, __p, __np});
- if (!filenamesByKey.has(k)) {
- filenamesByKey.set(k, new Set());
- }
+ referenceTemplate[keyWithContext] = placeholderTranslation;
- filenamesByKey.get(k).add(`${m}:${lineNumber}`);
+ addedKeysWithContext.add(keyWithContext);
+
+ if (!filenamesByKey.has(keyWithContext)) {
+ filenamesByKey.set(keyWithContext, new Set());
+ }
+
+ filenamesByKey.get(keyWithContext).add(`${m}:${lineNumber}`);
+ } catch(e) {
+ console.warn(e);
+ }
+ });
});
- keysByFilename.set(m, new Set(keys));
}
+
+ keysByFilename.set(m, addedKeysWithContext);
});
if (options.debug) {
@@ -331,11 +476,28 @@ function loadSourceFiles() {
function writeIndexTarget(targetDirectory, subLocalization) {
const directory = path.resolve(targetDirectory, '.locales');
const filename = path.resolve(directory, `index.json`);
- const previousSubLocalization = readJSON5FileWithFallback.call(this, filename);
- if (!equal(subLocalization, previousSubLocalization)) {
- fs.writeFileSync(filename, JSON.stringify(subLocalization, null, 4));
- }
+ fs.writeFileSync(filename, JSON.stringify(subLocalization, null, 4));
+}
+
+/**
+ * @private
+ */
+function buildTargetLocalization(localization, filenames) {
+ const subLocalization = {};
+ const keysByFilename = this.keysByFilename;
+
+ filenames.forEach((f) => {
+ const keys = keysByFilename.get(f);
+
+ keys.forEach((k) => {
+ if (localization.hasOwnProperty(k) && hasTranslation(localization[k])) {
+ subLocalization[k] = localization[k]
+ }
+ });
+ });
+
+ return collapseLocalization(unflattenLocalization(subLocalization));
}
/**
@@ -347,8 +509,8 @@ function writeToTargets() {
const filesByTargetDirectory = this.files.target.filesByTargetDirectory;
const targetDirectories = Object.keys(filesByTargetDirectory);
const localizationByLanguage = this.localizationByLanguage;
- const keysByFilename = this.keysByFilename;
const aggregate = {};
+ let localizationChanged = false;
targetDirectories.forEach((t) => {
// This is intentionally a hidden directory. It should generally not be included
@@ -360,33 +522,24 @@ function writeToTargets() {
locales.forEach((l) => {
const filenames = filesByTargetDirectory[t];
const localization = localizationByLanguage.get(l);
- const subLocalization = {};
+ const subLocalization = buildTargetLocalization.call(this, localization, filenames);
const filename = path.resolve(directory, `${l}.json`);
-
- if (options.debug) {
- print('Writing final target to ', filename);
- }
-
- filenames.forEach((f) => {
- const keys = keysByFilename.get(f) || [];
-
- keys.forEach((k) => {
- if (localization.hasOwnProperty(k) && hasTranslation(localization[k])) {
- subLocalization[k] = localization[k]
- }
- });
- });
-
const previousSubLocalization = readJSON5FileWithFallback.call(this, filename);
aggregate[l] = subLocalization;
if (!equal(subLocalization, previousSubLocalization)) {
+ if (options.debug) {
+ print('Writing final target to ', filename);
+ }
+ localizationChanged = true;
fs.writeFileSync(filename, JSON.stringify(subLocalization, null, 4));
}
});
- writeIndexTarget(t, aggregate);
+ if (localizationChanged) {
+ writeIndexTarget(t, aggregate);
+ }
});
}
@@ -408,11 +561,6 @@ class TranslationStaticAnalyzer {
* multiple directories for modularity. If there are no targets, no `.locales` directory will be generated anywhere.
*/
constructor(options) {
- const localeGen = this.localeGen = {
- directory: fs.mkdtempSync(path.resolve(os.tmpdir(), 'locale-gen-'), {}),
- name: 'template',
- };
-
this.options = options || {};
this.sourceByFilename = new Map();
this.keysByFilename = new Map();
@@ -423,20 +571,6 @@ class TranslationStaticAnalyzer {
modified: new Set(),
removed: new Set(),
};
-
- if (this.options.debug) {
- print('Creating locale gen directory', localeGen.directory);
- }
-
- process.on('exit', cleanup.bind(this));
- process.on('SIGINT', cleanup.bind(this));
-
- this.instance = y18n({
- updateFiles: true, //If it doesn't write the file, it also don't update the cache
- directory: localeGen.directory,
- locale: localeGen.name,
- });
-
this.files = calculateFiles.call(this, []);
this.files.modified = new Set();
this.files.removed = new Set();
@@ -496,10 +630,9 @@ class TranslationStaticAnalyzer {
* updating a source file.
*/
write() {
- const cache = this.instance.cache || {};
- const template = cache.template;
+ const referenceTemplate = this.referenceTemplate;
- if (template && generateLocaleFiles.call(this)) {
+ if (referenceTemplate && generateLocaleFiles.call(this)) {
writeToTargets.call(this);
}
diff --git a/src/isLocalizationFunctionStart.js b/src/isLocalizationFunctionStart.js
index 2e2140f..3ef2e73 100644
--- a/src/isLocalizationFunctionStart.js
+++ b/src/isLocalizationFunctionStart.js
@@ -3,6 +3,10 @@ const translationStartPatterns = [
'__`',
`__n(`,
'__n`',
+ '__p(',
+ '__p`',
+ `__np(`,
+ '__np`',
];
const length = translationStartPatterns
diff --git a/src/parseLocalizationFunction.js b/src/parseLocalizationFunction.js
index 9c6e5c8..6324e6d 100644
--- a/src/parseLocalizationFunction.js
+++ b/src/parseLocalizationFunction.js
@@ -37,9 +37,21 @@ function continueUntilStackLengthIs(text, state, length) {
return state;
}
+function readStringArgument(text, {index, stack, lineNumber}, name) {
+ const start = continueToQuoteStart(text, {index, stack, lineNumber});
+ const end = continueUntilStackLengthIs(text, {...start}, start.stack.length - 1);
+ const stringArgument = text.substring(start.index, end.index - 1);
+
+ if (start.index === end.index - 1) {
+ throw new SyntaxError(`${name} string argument is empty`);
+ }
+
+ return [end, stringArgument];
+}
+
/**
* Parses the information from a localization function, include the function string,
- * the key, the line number.
+ * the key, the line number. Parses __, __n, __p, __np.
* @param {String} text - The text blob
* @param {Number} index - The offset on the text
* @param {Array} stack The current code stack
@@ -50,31 +62,42 @@ function continueUntilStackLengthIs(text, state, length) {
*/
module.exports = function parseLocalizationFunction(text, {index, stack, lineNumber}) {
const functionStart = {index, stack, lineNumber};
+ let plural = false;
+ let particular = false;
index += 1;
if (text.charAt(index + 1) === 'n') {
+ plural = true;
index += 1;
}
- if (text.charAt(index + 1) === '(') {
+ if (text.charAt(index + 1) === 'p') {
+ particular = true;
index += 1;
+ }
+ if (text.charAt(index + 1) === '(') {
+ index += 1;
}
- const keyStart = continueToQuoteStart(text, {index, stack, lineNumber});
- const keyEnd = continueUntilStackLengthIs(text, {...keyStart}, keyStart.stack.length - 1);
+ const metadata = {plural, particular};
+ let state = {index, stack, lineNumber};
- if (keyStart.index === keyEnd.index - 1) {
- throw new SyntaxError('empty localization key');
+ if (particular) {
+ let context;
+ [state, context] = readStringArgument(text, state, 'context');
+ metadata.context = context;
}
- const functionEnd = (keyEnd.stack[0] === '(') ?
- continueUntilStackLengthIs(text, {...keyEnd}, keyEnd.stack.length - 1) : keyEnd;
+ let key;
+ [state, key] = readStringArgument(text, state, 'key');
+ metadata.key = key;
+
+ const functionEnd = (state.stack[0] === '(') ?
+ continueUntilStackLengthIs(text, {...state}, state.stack.length - 1) : state;
+ const fn = text.substring(functionStart.index, functionEnd.index);
+ metadata.fn = fn;
- return {
- ...functionEnd,
- key: text.substring(keyStart.index, keyEnd.index - 1),
- fn: text.substring(functionStart.index, functionEnd.index),
- };
+ return Object.assign({}, functionEnd, metadata);
}
diff --git a/src/readCharacter.test.js b/src/readCharacter.test.js
index ac43499..dc9dd0d 100644
--- a/src/readCharacter.test.js
+++ b/src/readCharacter.test.js
@@ -221,6 +221,8 @@ describe('plugins/readCharacter', () => {
localization: {
key: 'a',
fn: '__("a")',
+ plural: false,
+ particular: false,
}
}, {
index: 8,
@@ -229,6 +231,33 @@ describe('plugins/readCharacter', () => {
}]);
});
+ it('parses basic translation function with context', () => {
+ let state = {index: 0, stack: [], lineNumber: 0}
+ const text = '__p("a", "b")c';
+ const actual = [];
+
+ while ((state = readCharacter(text, state)) !== null) {
+ actual.push(state);
+ }
+
+ expect(actual).toEqual([{
+ index: 13,
+ stack: [],
+ lineNumber: 0,
+ localization: {
+ context: 'a',
+ key: 'b',
+ fn: '__p("a", "b")',
+ plural: false,
+ particular: true,
+ }
+ }, {
+ index: 14,
+ stack: [],
+ lineNumber: 0,
+ }]);
+ });
+
it('parses basic plural translation function', () => {
let state = {index: 0, stack: [], lineNumber: 0}
const text = '__n("%d cat", "%d cats", 1)b';
@@ -245,6 +274,8 @@ describe('plugins/readCharacter', () => {
localization: {
key: '%d cat',
fn: '__n("%d cat", "%d cats", 1)',
+ plural: true,
+ particular: false,
}
}, {
index: 28,
@@ -253,6 +284,33 @@ describe('plugins/readCharacter', () => {
}]);
});
+ it('parses basic plural translation function with context', () => {
+ let state = {index: 0, stack: [], lineNumber: 0}
+ const text = '__np("a", "%d cat", "%d cats", 1)b';
+ const actual = [];
+
+ while ((state = readCharacter(text, state)) !== null) {
+ actual.push(state);
+ }
+
+ expect(actual).toEqual([{
+ index: 33,
+ stack: [],
+ lineNumber: 0,
+ localization: {
+ key: '%d cat',
+ context: 'a',
+ fn: '__np("a", "%d cat", "%d cats", 1)',
+ plural: true,
+ particular: true,
+ }
+ }, {
+ index: 34,
+ stack: [],
+ lineNumber: 0,
+ }]);
+ });
+
describe('polymer-style template strings', () => {
it('parses basic translation function in [[]] interpolation string', () => {
let state = {index: 0, stack: [], lineNumber: 0}
@@ -285,7 +343,9 @@ describe('plugins/readCharacter', () => {
lineNumber: 0,
localization: {
"key": "a",
- "fn": "__(\"a\")"
+ "fn": "__(\"a\")",
+ particular: false,
+ plural: false,
}
}, {
index: 12,
@@ -390,7 +450,9 @@ describe('plugins/readCharacter', () => {
lineNumber: 0,
localization: {
"key": "a",
- "fn": "__(\"a\")"
+ "fn": "__(\"a\")",
+ particular: false,
+ plural: false,
}
}, {
index: 12,
@@ -495,7 +557,9 @@ describe('plugins/readCharacter', () => {
lineNumber: 0,
localization: {
"key": "a",
- "fn": "__(\"a\")"
+ "fn": "__(\"a\")",
+ particular: false,
+ plural: false
}
}, {
index: 11,
@@ -579,7 +643,9 @@ describe('plugins/readCharacter', () => {
lineNumber: 0,
localization: {
"key": "a",
- "fn": "__(\"a\")"
+ "fn": "__(\"a\")",
+ plural: false,
+ particular: false,
}
}, {
index: 13,
@@ -664,6 +730,8 @@ describe('plugins/readCharacter', () => {
localization: {
key: 'a',
fn: '__`a`',
+ particular: false,
+ plural: false,
}
}, {
index: 6,
@@ -764,7 +832,7 @@ describe('plugins/readCharacter', () => {
while ((state = readCharacter(text, state)) !== null) {
actual.push(state);
}
- }).toThrow(new SyntaxError('empty localization key'));
+ }).toThrow(new SyntaxError('key string argument is empty'));
expect(actual).toEqual([]);
diff --git a/src/readString.js b/src/readString.js
index 8c014ee..0fc7ecb 100644
--- a/src/readString.js
+++ b/src/readString.js
@@ -50,7 +50,11 @@ module.exports = function readString(text) {
const {key, fn} = state.localization;
const {index, lineNumber} = state;
- localization[key] = {fn, lineNumber, index};
+ if (!localization[key]) {
+ localization[key] = [{fn, lineNumber, index}];
+ } else {
+ localization[key].push({fn, lineNumber, index});
+ }
}
previousIndex = state.index;
diff --git a/src/readString.test.js b/src/readString.test.js
index bf98530..d0451e7 100644
--- a/src/readString.test.js
+++ b/src/readString.test.js
@@ -3,7 +3,9 @@ const readString = require('./readString');
describe('readString', () => {
describe('singular', () => {
it('create key and value when contains translation', () => {
- expect(readString(`a __('b') c`)).toEqual({b: {fn: `__('b')`, lineNumber: 0, index: 9}});
+ expect(readString(`a __('b') c`)).toEqual({b: [
+ {fn: `__('b')`, lineNumber: 0, index: 9}],
+ });
});
it('adds nothing when no translation', () => {
@@ -11,18 +13,108 @@ describe('readString', () => {
});
it('create key and value when contains shorthand translation', () => {
- expect(readString('a __`b` c')).toEqual({b: {fn: '__`b`', lineNumber: 0, index: 7 }});
+ expect(readString('a __`b` c')).toEqual({b: [
+ {fn: '__`b`', lineNumber: 0, index: 7 }],
+ });
});
it('handles unclosed parenthesis gracefully', () => {
expect(readString('a __(`b` c')).toEqual({});
});
+
+ it('handles two duplicate translations on same line gracefully', () => {
+ expect(readString('a __(`b`) __(`b`) c')).toEqual({b: [
+ {fn: '__(`b`)', lineNumber: 0, index: 9 },
+ {fn: '__(`b`)', lineNumber: 0, index: 17 },
+ ]});
+ });
+
+ it('handles two duplicate translations on different line gracefully', () => {
+ expect(readString('a __(`b`)\n__(`b`) c')).toEqual({b: [
+ {fn: '__(`b`)', lineNumber: 0, index: 9 },
+ {fn: '__(`b`)', lineNumber: 1, index: 17 },
+ ]});
+ });
+ });
+
+ describe('singular with context', () => {
+ it('create key and value when contains translation', () => {
+ expect(readString(`a __p('b', 'c') d`)).toEqual({c: [
+ {fn: `__p('b', 'c')`, lineNumber: 0, index: 15},
+ ]});
+ });
+
+ it('adds nothing when no translation', () => {
+ expect(readString(`a c`)).toEqual({});
+ });
+
+ it('handles two duplicate translations on same line gracefully', () => {
+ expect(readString('a __p(`b`, `c`) __p(`b`, `c`) c')).toEqual({c: [
+ {fn: '__p(`b`, `c`)', lineNumber: 0, index: 15 },
+ {fn: '__p(`b`, `c`)', lineNumber: 0, index: 29 },
+ ]});
+ });
+
+ it('handles two duplicate translations on different line gracefully', () => {
+ expect(readString('a __p(`b`, `c`)\n__p(`b`, `c`) c')).toEqual({c: [
+ {fn: '__p(`b`, `c`)', lineNumber: 0, index: 15 },
+ {fn: '__p(`b`, `c`)', lineNumber: 1, index: 29 },
+ ]});
+ });
});
describe('plural', () => {
it('create key and value when contains plural translation', () => {
expect(readString("a __n('%d cat', '%d cats', 1) c")).toEqual({
- '%d cat': {fn: "__n('%d cat', '%d cats', 1)", lineNumber: 0, index: 29}
+ '%d cat': [
+ {fn: "__n('%d cat', '%d cats', 1)", lineNumber: 0, index: 29},
+ ]
+ });
+ });
+
+ it('handles two duplicate translations on same line gracefully', () => {
+ expect(readString("a __n('%d cat', '%d cats', 1) __n('%d cat', '%d cats', 1) c")).toEqual({
+ '%d cat': [
+ {fn: "__n('%d cat', '%d cats', 1)", lineNumber: 0, index: 29 },
+ {fn: "__n('%d cat', '%d cats', 1)", lineNumber: 0, index: 57 },
+ ]
+ });
+ });
+
+ it('handles two duplicate translations on different line gracefully', () => {
+ expect(readString("a __n('%d cat', '%d cats', 1)\n__n('%d cat', '%d cats', 1) c")).toEqual({
+ '%d cat': [
+ {fn: "__n('%d cat', '%d cats', 1)", lineNumber: 0, index: 29 },
+ {fn: "__n('%d cat', '%d cats', 1)", lineNumber: 1, index: 57 },
+ ]
+ });
+ });
+ });
+
+ describe('plural with context', () => {
+ it('create key and value when contains plural translation', () => {
+ expect(readString("a __np('a', '%d cat', '%d cats', 1) c")).toEqual({
+ '%d cat': [
+ {fn: "__np('a', '%d cat', '%d cats', 1)", lineNumber: 0, index: 35},
+ ]
+ });
+ });
+
+ it('handles two duplicate translations on same line gracefully', () => {
+ expect(readString("a __np('b', '%d cat', '%d cats', 1) __np('b', '%d cat', '%d cats', 1) c")).toEqual({
+ '%d cat': [
+ {fn: "__np('b', '%d cat', '%d cats', 1)", lineNumber: 0, index: 35 },
+ {fn: "__np('b', '%d cat', '%d cats', 1)", lineNumber: 0, index: 69 },
+ ]
+ });
+ });
+
+ it('handles two duplicate translations on different line gracefully', () => {
+ expect(readString("a __np('b', '%d cat', '%d cats', 1)\n__np('b', '%d cat', '%d cats', 1) c")).toEqual({
+ '%d cat': [
+ {fn: "__np('b', '%d cat', '%d cats', 1)", lineNumber: 0, index: 35 },
+ {fn: "__np('b', '%d cat', '%d cats', 1)", lineNumber: 1, index: 69 },
+ ]
});
});
});
diff --git a/src/test.js b/src/test.js
index d8fdaed..ebdb207 100644
--- a/src/test.js
+++ b/src/test.js
@@ -1,3 +1,5 @@
+/*eslint max-len: ["error", {"ignoreStrings": true}]*/
+
const TranslationStaticAnalyzer = require('.');
const fs = require('fs-extra');
const console = require('console');
@@ -8,7 +10,6 @@ jest.mock('fs-extra');
jest.mock('console');
const mocks = {};
-
const path = require('path');
describe('TranslationStaticAnalyzer', () => {
@@ -33,6 +34,26 @@ describe('TranslationStaticAnalyzer', () => {
fs.mockReset();
});
+ it('handles empty key gracefully', () => {
+ const analyzer = new TranslationStaticAnalyzer({
+ files: 'test empty keys',
+ locales: ['existing'],
+ target: 'test directory targets',
+ });
+
+ fs.actions.length = 0;
+
+ analyzer.read();
+
+ expect(fs.actions).toEqual([
+ {
+ "action": "read",
+ "filename": "src/pages/EmptyKeys/index.js",
+ "data": "export default class Application extends Component {\n static get title() {\n return __('') + __n('') + __np('') + __p('');\n }\n};"
+ }
+ ]);
+ });
+
it('does nothing when write is called and there is no template', () => {
const analyzer = new TranslationStaticAnalyzer({
files: 'test files',
@@ -40,7 +61,7 @@ describe('TranslationStaticAnalyzer', () => {
target: 'test directory targets',
});
- delete analyzer.instance.cache.template;
+ delete analyzer.referenceTemplate;
fs.actions.length = 0;
analyzer.write();
@@ -48,38 +69,162 @@ describe('TranslationStaticAnalyzer', () => {
expect(fs.actions).toEqual([]);
});
- it('handles write gracefully when cache object is missing', () => {
+ it('filters out traslation strings accidentally placed where contexts should exist', () => {
const analyzer = new TranslationStaticAnalyzer({
files: 'test files',
locales: ['existing'],
target: 'test directory targets',
});
- delete analyzer.instance.cache;
+ analyzer.read();
+
+ fs.writeFileSync('./locales/existing.json', JSON.stringify({
+ 'test key': 'test invalid localization'
+ }));
+
fs.actions.length = 0;
analyzer.write();
- expect(fs.actions).toEqual([]);
+ expect(fs.actions).toEqual([
+ {
+ "action": "read",
+ "filename": "./locales/existing.json",
+ "data": "{\"test key\":\"test invalid localization\"}"
+ },
+ {
+ "action": "write",
+ "filename": "./locales/existing.json",
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // NEW\n // src/index.js:2\n \"default\": \"\"\n },\n \"Search\": {\n // NEW\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n }\n}"
+ },
+ {
+ "action": "read",
+ "filename": "src/pages/.locales/existing.json",
+ "data": null
+ },
+ {
+ "action": "read",
+ "filename": "src/pages/Search/.locales/existing.json",
+ "data": "{\"Search\":\"\"}"
+ },
+ {
+ "action": "write",
+ "filename": "src/pages/Search/.locales/existing.json",
+ "data": "{}"
+ },
+ {
+ "action": "write",
+ "filename": "src/pages/Search/.locales/index.json",
+ "data": "{\n \"existing\": {}\n}"
+ },
+ {
+ "action": "read",
+ "filename": "src/pages/About/.locales/existing.json",
+ "data": "{}"
+ },
+ {
+ "action": "write",
+ "filename": "src/pages/About/.locales/index.json",
+ "data": "{\n \"existing\": {}\n}"
+ },
+ {
+ "action": "read",
+ "filename": "src/application/.locales/existing.json",
+ "data": null
+ },
+ {
+ "action": "write",
+ "filename": "src/application/.locales/index.json",
+ "data": "{\n \"existing\": {}\n}"
+ }
+ ]);
});
- it('calls cleanup on exit', () => {
+ it("doesn't collapse the localization when there is a default and non-default context", () => {
const analyzer = new TranslationStaticAnalyzer({
files: 'test files',
locales: ['existing'],
target: 'test directory targets',
});
- const exitCallback = mocks.processOn.mock.calls[0];
- const sigIntCallback = mocks.processOn.mock.calls[1];
- expect(exitCallback[0]).toEqual('exit');
- expect(sigIntCallback[0]).toEqual('SIGINT');
+ fs.writeFileSync('src/pages/Search/index.js', `export default __('test key') + __p('menuitem', 'test key');`);
+
+ analyzer.read();
- exitCallback[1]();
- sigIntCallback[1]();
+ fs.writeFileSync('./locales/existing.json', JSON.stringify({
+ 'test key': {
+ 'default': 'test default translation context',
+ 'menuitem': 'test menuitem translation context',
+ }
+ }));
- expect(fs.removeSync.mock.calls).toEqual([["/test/tmp/0"], ["/test/tmp/0"]]);
+ fs.actions.length = 0;
+
+ analyzer.write();
+
+ expect(fs.actions).toEqual([
+ {
+ "action": "read",
+ "filename": "./locales/existing.json",
+ "data": "{\"test key\":{\"default\":\"test default translation context\",\"menuitem\":\"test menuitem translation context\"}}"
+ },
+ {
+ "action": "write",
+ "filename": "./locales/existing.json",
+ "data": "{\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // NEW\n // src/index.js:2\n \"default\": \"\"\n },\n \"Search\": {\n // NEW\n // src/pages/About/index.js:6\n \"default\": \"\"\n },\n \"test key\": {\n // src/pages/Search/index.js:0\n \"default\": \"test default translation context\",\n // src/pages/Search/index.js:0\n \"menuitem\": \"test menuitem translation context\"\n }\n}"
+ },
+ {
+ "action": "read",
+ "filename": "src/pages/.locales/existing.json",
+ "data": null
+ },
+ {
+ "action": "write",
+ "filename": "src/pages/.locales/existing.json",
+ "data": "{\n \"test key\": {\n \"default\": \"test default translation context\",\n \"menuitem\": \"test menuitem translation context\"\n }\n}"
+ },
+ {
+ "action": "write",
+ "filename": "src/pages/.locales/index.json",
+ "data": "{\n \"existing\": {\n \"test key\": {\n \"default\": \"test default translation context\",\n \"menuitem\": \"test menuitem translation context\"\n }\n }\n}"
+ },
+ {
+ "action": "read",
+ "filename": "src/pages/Search/.locales/existing.json",
+ "data": "{\"Search\":\"\"}"
+ },
+ {
+ "action": "write",
+ "filename": "src/pages/Search/.locales/existing.json",
+ "data": "{\n \"test key\": {\n \"default\": \"test default translation context\",\n \"menuitem\": \"test menuitem translation context\"\n }\n}"
+ },
+ {
+ "action": "write",
+ "filename": "src/pages/Search/.locales/index.json",
+ "data": "{\n \"existing\": {\n \"test key\": {\n \"default\": \"test default translation context\",\n \"menuitem\": \"test menuitem translation context\"\n }\n }\n}"
+ },
+ {
+ "action": "read",
+ "filename": "src/pages/About/.locales/existing.json",
+ "data": "{}"
+ },
+ {
+ "action": "write",
+ "filename": "src/pages/About/.locales/index.json",
+ "data": "{\n \"existing\": {}\n}"
+ },
+ {
+ "action": "read",
+ "filename": "src/application/.locales/existing.json",
+ "data": null
+ },
+ {
+ "action": "write",
+ "filename": "src/application/.locales/index.json",
+ "data": "{\n \"existing\": {}\n}"
+ }
+ ]);
});
it('works with defaults for language with some prefilled data', () => {
@@ -91,11 +236,11 @@ describe('TranslationStaticAnalyzer', () => {
analyzer.update();
- expect(fs.actions).toEqual([
+ expect(fs.actions).toEqual([
{
"action": "read",
"filename": "src/pages/Search/index.js",
- "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__n('%d result', '%d results', 2)}}
';\n\n }\n};"
+ "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__p('menuitem', 'Search')}} {{__n('%d result', '%d results', 2)}}
{{__np('footer', '%d view', '%d views', 23)}}';\n }\n};"
},
{
"action": "read",
@@ -115,12 +260,12 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "read",
"filename": "./locales/existing.json",
- "data": "{\"Search\":\"検索\",\"test unused key\":\"test value\",\"Application\":\"アプリケーション\"}"
+ "data": "{\"Search\":{\"default\":\"検索\"},\"test unused key\":{\"default\":\"test value\"},\"Application\":{\"default\":\"アプリケーション\"}}"
},
{
"action": "write",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // src/pages/Search/index.js:6\n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // src/index.js:2\n \"Application\": \"アプリケーション\",\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"検索\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
},
{
"action": "read",
@@ -132,11 +277,6 @@ describe('TranslationStaticAnalyzer', () => {
"filename": "src/pages/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/.locales/index.json",
- "data": null
- },
{
"action": "write",
"filename": "src/pages/.locales/index.json",
@@ -152,11 +292,6 @@ describe('TranslationStaticAnalyzer', () => {
"filename": "src/pages/Search/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/Search/.locales/index.json",
- "data": null
- },
{
"action": "write",
"filename": "src/pages/Search/.locales/index.json",
@@ -172,11 +307,6 @@ describe('TranslationStaticAnalyzer', () => {
"filename": "src/pages/About/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/About/.locales/index.json",
- "data": null
- },
{
"action": "write",
"filename": "src/pages/About/.locales/index.json",
@@ -192,11 +322,6 @@ describe('TranslationStaticAnalyzer', () => {
"filename": "src/application/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\"\n}"
},
- {
- "action": "read",
- "filename": "src/application/.locales/index.json",
- "data": null
- },
{
"action": "write",
"filename": "src/application/.locales/index.json",
@@ -207,12 +332,11 @@ describe('TranslationStaticAnalyzer', () => {
fs.actions.length = 0;
analyzer.update();
-
- expect(fs.actions).toEqual([
+ expect(fs.actions).toEqual([
{
"action": "read",
"filename": "src/pages/Search/index.js",
- "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__n('%d result', '%d results', 2)}}
';\n\n }\n};"
+ "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__p('menuitem', 'Search')}} {{__n('%d result', '%d results', 2)}}
{{__np('footer', '%d view', '%d views', 23)}}';\n }\n};"
},
{
"action": "read",
@@ -232,9 +356,9 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "read",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // src/pages/Search/index.js:6\n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // src/index.js:2\n \"Application\": \"アプリケーション\",\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"検索\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
}
- ]);
+ ]);
});
describe('read', () => {
@@ -251,7 +375,7 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "read",
"filename": "src/pages/Search/index.js",
- "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__n('%d result', '%d results', 2)}}
';\n\n }\n};"
+ "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__p('menuitem', 'Search')}} {{__n('%d result', '%d results', 2)}}
{{__np('footer', '%d view', '%d views', 23)}}';\n }\n};"
},
{
"action": "read",
@@ -282,13 +406,14 @@ describe('TranslationStaticAnalyzer', () => {
fs.actions.length = 0;
expect(analyzer.read(['src/pages/Search/index.js'])).toBe(true);
+
expect(fs.actions).toEqual(
[
{
"action": "read",
"filename": "src/pages/Search/index.js",
- "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__n('%d result', '%d results', 2)}}
';\n\n }\n};"
- },
+ "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__p('menuitem', 'Search')}} {{__n('%d result', '%d results', 2)}}
{{__np('footer', '%d view', '%d views', 23)}}';\n }\n};"
+ }
]
);
});
@@ -307,91 +432,6 @@ describe('TranslationStaticAnalyzer', () => {
expect(fs.actions).toEqual(
[
- {
- "action": "read",
- "filename": "./locales/existing.json",
- "data": "{\"Search\":\"検索\",\"test unused key\":\"test value\",\"Application\":\"アプリケーション\"}"
- },
- {
- "action": "write",
- "filename": "./locales/existing.json",
- "data": "{\n // NEW\n \"%s result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n \"About\": \"\",\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
- },
- {
- "action": "read",
- "filename": "src/pages/.locales/existing.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/.locales/existing.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/pages/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/.locales/index.json",
- "data": "{\n \"existing\": {}\n}"
- },
- {
- "action": "read",
- "filename": "src/pages/Search/.locales/existing.json",
- "data": "{\"Search\":\"\"}"
- },
- {
- "action": "write",
- "filename": "src/pages/Search/.locales/existing.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/pages/Search/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/Search/.locales/index.json",
- "data": "{\n \"existing\": {}\n}"
- },
- {
- "action": "read",
- "filename": "src/pages/About/.locales/existing.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/pages/About/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/About/.locales/index.json",
- "data": "{\n \"existing\": {}\n}"
- },
- {
- "action": "read",
- "filename": "src/application/.locales/existing.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/application/.locales/existing.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/application/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/application/.locales/index.json",
- "data": "{\n \"existing\": {}\n}"
- }
]
);
});
@@ -418,42 +458,27 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "read",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // src/pages/Search/index.js:6\n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // src/index.js:2\n \"Application\": \"アプリケーション\",\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"検索\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
},
{
"action": "write",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // src/index.js:2\n \"Application\": \"アプリケーション\",\n // src/pages/About/index.js:6\n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n \"default\": \"検索\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
},
{
"action": "read",
"filename": "src/pages/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/.locales/index.json",
- "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}"
- },
{
"action": "read",
"filename": "src/pages/About/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/About/.locales/index.json",
- "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}"
- },
{
"action": "read",
"filename": "src/application/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\"\n}"
- },
- {
- "action": "read",
- "filename": "src/application/.locales/index.json",
- "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\"\n }\n}"
}
]);
});
@@ -469,19 +494,37 @@ describe('TranslationStaticAnalyzer', () => {
expect(fs.readFileSync('./locales/existing.json')).toEqual(
`{
- // NEW
- // src/pages/Search/index.js:6
- "%d result": {"one":"","other":""},
- // NEW
- // src/pages/About/index.js:2
- "About": "",
- // src/index.js:2
- "Application": "アプリケーション",
- // src/pages/About/index.js:6
- // src/pages/Search/index.js:2
- "Search": "検索",
- // UNUSED
- "test unused key": "test value"
+ "%d result": {
+ // NEW
+ // src/pages/Search/index.js:6
+ "default": {"one":"","other":""}
+ },
+ "%d view": {
+ // NEW
+ // src/pages/Search/index.js:6
+ "footer": {"one":"","other":""}
+ },
+ "About": {
+ // NEW
+ // src/pages/About/index.js:2
+ "default": ""
+ },
+ "Application": {
+ // src/index.js:2
+ "default": "アプリケーション"
+ },
+ "Search": {
+ // src/pages/About/index.js:6
+ // src/pages/Search/index.js:2
+ "default": "検索",
+ // NEW
+ // src/pages/Search/index.js:6
+ "menuitem": ""
+ },
+ "test unused key": {
+ // UNUSED
+ "default": "test value"
+ }
}`
);
@@ -494,30 +537,36 @@ describe('TranslationStaticAnalyzer', () => {
expect(fs.readFileSync('./locales/existing.json')).toEqual(
`{
- // NEW
- // src/pages/About/index.js:2
- "About": "",
- // src/index.js:2
- "Application": "アプリケーション",
- // src/pages/About/index.js:6
- "Search": "検索",
- // UNUSED
- "test unused key": "test value"
+ "About": {
+ // NEW
+ // src/pages/About/index.js:2
+ "default": ""
+ },
+ "Application": {
+ // src/index.js:2
+ "default": "アプリケーション"
+ },
+ "Search": {
+ // src/pages/About/index.js:6
+ "default": "検索"
+ },
+ "test unused key": {
+ // UNUSED
+ "default": "test value"
+ }
}`
);
- /*
-
- expect(fs.actions).toEqual([
+ expect(fs.actions).toEqual([
{
"action": "read",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // \n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // \n \"About\": \"\",\n // \n \"Application\": \"アプリケーション\",\n // \n // \n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"検索\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
},
{
"action": "write",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // \n \"About\": \"\",\n // \n \"Application\": \"アプリケーション\",\n // \n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n \"default\": \"検索\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
},
{
"action": "read",
@@ -533,9 +582,13 @@ describe('TranslationStaticAnalyzer', () => {
"action": "read",
"filename": "src/application/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\"\n}"
+ },
+ {
+ "action": "read",
+ "filename": "./locales/existing.json",
+ "data": "{\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n \"default\": \"検索\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
}
- ]);
- */
+ ]);
});
it('removes unreadable source file', () => {
@@ -566,27 +619,22 @@ describe('TranslationStaticAnalyzer', () => {
fs.readFileSync = originalReadFileSync;
- expect(fs.actions).toEqual([
+ expect(fs.actions).toEqual([
{
"action": "read",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // src/pages/Search/index.js:6\n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // src/index.js:2\n \"Application\": \"アプリケーション\",\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"検索\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
},
{
"action": "write",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // src/index.js:2\n \"Application\": \"アプリケーション\",\n // src/pages/About/index.js:6\n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n \"default\": \"検索\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
},
{
"action": "read",
"filename": "src/pages/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/.locales/index.json",
- "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}"
- },
{
"action": "read",
"filename": "src/pages/Search/.locales/existing.json",
@@ -597,11 +645,6 @@ describe('TranslationStaticAnalyzer', () => {
"filename": "src/pages/Search/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/Search/.locales/index.json",
- "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}"
- },
{
"action": "write",
"filename": "src/pages/Search/.locales/index.json",
@@ -613,7 +656,7 @@ describe('TranslationStaticAnalyzer', () => {
"data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}"
},
{
- "action": "read",
+ "action": "write",
"filename": "src/pages/About/.locales/index.json",
"data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}"
},
@@ -623,7 +666,7 @@ describe('TranslationStaticAnalyzer', () => {
"data": "{\n \"Application\": \"アプリケーション\"\n}"
},
{
- "action": "read",
+ "action": "write",
"filename": "src/application/.locales/index.json",
"data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\"\n }\n}"
}
@@ -655,43 +698,28 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "read",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // src/pages/Search/index.js:6\n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // src/index.js:2\n \"Application\": \"アプリケーション\",\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"検索\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
},
{
"action": "write",
"filename": "./locales/existing.json",
- "data": "{\n // NEW\n // src/pages/Search/index.js:6\n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // NEW\n // src/pages/Added/index.js:0\n \"Added\": \"\",\n // src/index.js:2\n \"Application\": \"アプリケーション\",\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"Search\": \"検索\",\n // UNUSED\n \"test unused key\": \"test value\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Added\": {\n // NEW\n // src/pages/Added/index.js:0\n \"default\": \"\"\n },\n \"Application\": {\n // src/index.js:2\n \"default\": \"アプリケーション\"\n },\n \"Search\": {\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"検索\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}"
},
{
"action": "read",
"filename": "src/pages/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/.locales/index.json",
- "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}"
- },
{
"action": "read",
"filename": "src/pages/About/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/About/.locales/index.json",
- "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}"
- },
{
"action": "read",
"filename": "src/application/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\"\n}"
},
- {
- "action": "read",
- "filename": "src/application/.locales/index.json",
- "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\"\n }\n}"
- },
{
"action": "read",
"filename": "src/pages/Added/.locales/existing.json",
@@ -702,11 +730,6 @@ describe('TranslationStaticAnalyzer', () => {
"filename": "src/pages/Added/.locales/existing.json",
"data": "{\n \"Application\": \"アプリケーション\"\n}"
},
- {
- "action": "read",
- "filename": "src/pages/Added/.locales/index.json",
- "data": null
- },
{
"action": "write",
"filename": "src/pages/Added/.locales/index.json",
@@ -816,7 +839,7 @@ describe('TranslationStaticAnalyzer', () => {
target: 'test directory targets'
});
- fs.readFileSync.mockImplementation((filename) => {
+ fs.readFileSync = jest.fn().mockImplementation((filename) => {
if (filename.endsWith('.json')) {
const e = new Error("MockError: readFileSync issue");
e.code = 'TEST ERROR';
@@ -842,7 +865,7 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "read",
"filename": "src/pages/Search/index.js",
- "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__n('%d result', '%d results', 2)}}
';\n\n }\n};"
+ "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__p('menuitem', 'Search')}} {{__n('%d result', '%d results', 2)}}
{{__np('footer', '%d view', '%d views', 23)}}';\n }\n};"
},
{
"action": "read",
@@ -867,87 +890,27 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "write",
"filename": "./locales/new.json",
- "data": "{\n // NEW\n // src/pages/Search/index.js:6\n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // NEW\n // src/index.js:2\n \"Application\": \"\",\n // NEW\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"Search\": \"\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // NEW\n // src/index.js:2\n \"default\": \"\"\n },\n \"Search\": {\n // NEW\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n }\n}"
},
{
"action": "read",
"filename": "src/pages/.locales/new.json",
"data": null
},
- {
- "action": "write",
- "filename": "src/pages/.locales/new.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/pages/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/.locales/index.json",
- "data": "{\n \"new\": {}\n}"
- },
{
"action": "read",
"filename": "src/pages/Search/.locales/new.json",
"data": null
},
- {
- "action": "write",
- "filename": "src/pages/Search/.locales/new.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/pages/Search/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/Search/.locales/index.json",
- "data": "{\n \"new\": {}\n}"
- },
{
"action": "read",
"filename": "src/pages/About/.locales/new.json",
"data": null
},
- {
- "action": "write",
- "filename": "src/pages/About/.locales/new.json",
- "data": "{}"
- },
{
"action": "read",
- "filename": "src/pages/About/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/About/.locales/index.json",
- "data": "{\n \"new\": {}\n}"
- },
- {
- "action": "read",
- "filename": "src/application/.locales/new.json",
- "data": null
- },
- {
- "action": "write",
"filename": "src/application/.locales/new.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/application/.locales/index.json",
"data": null
- },
- {
- "action": "write",
- "filename": "src/application/.locales/index.json",
- "data": "{\n \"new\": {}\n}"
}
]);
});
@@ -969,7 +932,7 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "read",
"filename": "src/pages/Search/index.js",
- "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__n('%d result', '%d results', 2)}}
';\n\n }\n};"
+ "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__p('menuitem', 'Search')}} {{__n('%d result', '%d results', 2)}}
{{__np('footer', '%d view', '%d views', 23)}}';\n }\n};"
},
{
"action": "read",
@@ -994,87 +957,27 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "write",
"filename": "./locales/new.json",
- "data": "{\n // NEW\n // src/pages/Search/index.js:6\n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // NEW\n // src/index.js:2\n \"Application\": \"\",\n // NEW\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"Search\": \"\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // NEW\n // src/index.js:2\n \"default\": \"\"\n },\n \"Search\": {\n // NEW\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n }\n}"
},
{
"action": "read",
"filename": "src/pages/.locales/new.json",
"data": null
},
- {
- "action": "write",
- "filename": "src/pages/.locales/new.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/pages/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/.locales/index.json",
- "data": "{\n \"new\": {}\n}"
- },
{
"action": "read",
"filename": "src/pages/Search/.locales/new.json",
"data": null
},
- {
- "action": "write",
- "filename": "src/pages/Search/.locales/new.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/pages/Search/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/Search/.locales/index.json",
- "data": "{\n \"new\": {}\n}"
- },
{
"action": "read",
"filename": "src/pages/About/.locales/new.json",
"data": null
},
- {
- "action": "write",
- "filename": "src/pages/About/.locales/new.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/pages/About/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/About/.locales/index.json",
- "data": "{\n \"new\": {}\n}"
- },
{
"action": "read",
"filename": "src/application/.locales/new.json",
"data": null
- },
- {
- "action": "write",
- "filename": "src/application/.locales/new.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/application/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/application/.locales/index.json",
- "data": "{\n \"new\": {}\n}"
}
]);
});
@@ -1116,7 +1019,7 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "read",
"filename": "src/pages/Search/index.js",
- "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__n('%d result', '%d results', 2)}}
';\n\n }\n};"
+ "data": "export default class SearchPage extends Component {\n static get title() {\n return __('Search');\n }\n\n static get template() {\n return '{{__('invalid''string')}} {{__p('menuitem', 'Search')}} {{__n('%d result', '%d results', 2)}}
{{__np('footer', '%d view', '%d views', 23)}}';\n }\n};"
},
{
"action": "read",
@@ -1141,28 +1044,13 @@ describe('TranslationStaticAnalyzer', () => {
{
"action": "write",
"filename": "testtemplatespath/locales/existing.json",
- "data": "{\n // NEW\n // src/pages/Search/index.js:6\n \"%d result\": {\"one\":\"\",\"other\":\"\"},\n // NEW\n // src/pages/About/index.js:2\n \"About\": \"\",\n // NEW\n // src/index.js:2\n \"Application\": \"\",\n // NEW\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"Search\": \"\"\n}"
+ "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"one\":\"\",\"other\":\"\"}\n },\n \"%d view\": {\n // NEW\n // src/pages/Search/index.js:6\n \"footer\": {\"one\":\"\",\"other\":\"\"}\n },\n \"About\": {\n // NEW\n // src/pages/About/index.js:2\n \"default\": \"\"\n },\n \"Application\": {\n // NEW\n // src/index.js:2\n \"default\": \"\"\n },\n \"Search\": {\n // NEW\n // src/pages/About/index.js:6\n // src/pages/Search/index.js:2\n \"default\": \"\",\n // NEW\n // src/pages/Search/index.js:6\n \"menuitem\": \"\"\n }\n}"
},
{
"action": "read",
"filename": "src/pages/.locales/existing.json",
"data": null
},
- {
- "action": "write",
- "filename": "src/pages/.locales/existing.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/pages/.locales/index.json",
- "data": null
- },
- {
- "action": "write",
- "filename": "src/pages/.locales/index.json",
- "data": "{\n \"existing\": {}\n}"
- },
{
"action": "read",
"filename": "src/pages/Search/.locales/existing.json",
@@ -1173,11 +1061,6 @@ describe('TranslationStaticAnalyzer', () => {
"filename": "src/pages/Search/.locales/existing.json",
"data": "{}"
},
- {
- "action": "read",
- "filename": "src/pages/Search/.locales/index.json",
- "data": null
- },
{
"action": "write",
"filename": "src/pages/Search/.locales/index.json",
@@ -1188,11 +1071,6 @@ describe('TranslationStaticAnalyzer', () => {
"filename": "src/pages/About/.locales/existing.json",
"data": "{}"
},
- {
- "action": "read",
- "filename": "src/pages/About/.locales/index.json",
- "data": null
- },
{
"action": "write",
"filename": "src/pages/About/.locales/index.json",
@@ -1203,21 +1081,12 @@ describe('TranslationStaticAnalyzer', () => {
"filename": "src/application/.locales/existing.json",
"data": null
},
- {
- "action": "write",
- "filename": "src/application/.locales/existing.json",
- "data": "{}"
- },
- {
- "action": "read",
- "filename": "src/application/.locales/index.json",
- "data": null
- },
{
"action": "write",
"filename": "src/application/.locales/index.json",
"data": "{\n \"existing\": {}\n}"
}
+
]);
});
});
diff --git a/yarn.lock b/yarn.lock
index f898088..1998a0c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3662,6 +3662,10 @@ pretty-format@^23.5.0:
ansi-regex "^3.0.0"
ansi-styles "^3.2.0"
+printf@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/printf/-/printf-0.5.1.tgz#e0466788260859ed153006dc6867f09ddf240cf3"
+
private@^0.1.6, private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -4729,10 +4733,6 @@ y18n@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
-y18n@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
-
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"