diff --git a/__mocks__/filesystem.js b/__mocks__/filesystem.js index a358976..2e177a9 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(); @@ -41,9 +40,9 @@ module.exports = { '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__/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..39f9775 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,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/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..c8754cd 100644 --- a/src/index.js +++ b/src/index.js @@ -7,32 +7,84 @@ 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] = keyWithContext.split(':'); + + return [ + querystring.unescape(key), + querystring.unescape(value), + ]; +} + +/** + * @private + */ +function __(key) { + if (key) { + return [toKeyWithContext(key), ""]; + } + + return null; +} + +/** + * @private + */ +function __p(context, key) { + if (context && key) { + return [toKeyWithContext(key, context), ""]; + } + + return null; } +/** + * @private + */ +function __n(singular) { + if (singular) { + return [toKeyWithContext(singular), {one: "", other: ""}]; + } + + return null; +} /** * @private */ -function cleanup() { - const localeGen = this.localeGen; +function __np(context, singular) { + if (context && singular) { + return [toKeyWithContext(singular, context), {one: "", other: ""}]; + } + + return null; +} - fs.removeSync(localeGen.directory); +/** + * @private + */ +function print(message, ...leftover) { + console.log(`${name}: ${message}`, ...leftover); } /** @@ -103,28 +155,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 +230,63 @@ function readJSON5FileWithFallback(filename, fallback = null) { return data; } +/** + * @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}) + }); + }, {}); +} + +/** + * @private + */ +function collapseLocalization(localization) { + return Object.entries(localization).reduce((accumulator, [key, contexts]) => { + const keys = Object.keys(contexts); + + if (keys.length === 1 && contexts.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 +309,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 +318,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 +338,8 @@ function updateLocalization(localization) { [k]: { note: 'new', files, + key, + context, data: template[k] } }); @@ -200,6 +347,8 @@ function updateLocalization(localization) { return Object.assign({}, accumulator, { [k]: { files, + key, + context, data: localization[k] } }); @@ -209,6 +358,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 */ @@ -226,17 +388,19 @@ function generateLocaleFiles() { const localizationWithMetadata = previousLocalizationWithMetadataByLanguage.get(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 +410,51 @@ 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); - } - }); + const {lineNumber, fn} = v; + const pair = safeEval(fn, {__, __n, __p, __np}); - keys.forEach((k) => { - const {lineNumber} = metadata[k]; + if (pair) { + const [keyWithContext, placeholderTranslation] = pair; - if (!filenamesByKey.has(k)) { - filenamesByKey.set(k, new Set()); - } + referenceTemplate[keyWithContext] = placeholderTranslation; + + addedKeysWithContext.add(keyWithContext); - filenamesByKey.get(k).add(`${m}:${lineNumber}`); + if (!filenamesByKey.has(keyWithContext)) { + filenamesByKey.set(keyWithContext, new Set()); + } + + filenamesByKey.get(keyWithContext).add(`${m}:${lineNumber}`); + } else { + throw SyntaxError(`invalid translation function ${fn}`); + } + } catch(e) { + console.warn(e); + } }); - keysByFilename.set(m, new Set(keys)); } + + keysByFilename.set(m, addedKeysWithContext); }); if (options.debug) { @@ -338,6 +495,26 @@ function writeIndexTarget(targetDirectory, subLocalization) { } } +/** + * @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)); +} + /** * @private */ @@ -347,8 +524,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 +537,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 +576,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 +586,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 +645,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.test.js b/src/readString.test.js index bf98530..4b5cf26 100644 --- a/src/readString.test.js +++ b/src/readString.test.js @@ -19,6 +19,16 @@ describe('readString', () => { }); }); + 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({}); + }); + }); + describe('plural', () => { it('create key and value when contains plural translation', () => { expect(readString("a __n('%d cat', '%d cats', 1) c")).toEqual({ @@ -26,4 +36,12 @@ describe('readString', () => { }); }); }); + + 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} + }); + }); + }); }); diff --git a/src/test.js b/src/test.js index d8fdaed..e6f9301 100644 --- a/src/test.js +++ b/src/test.js @@ -1,20 +1,21 @@ +/*eslint max-len: ["error", {"ignoreStrings": true}]*/ + const TranslationStaticAnalyzer = require('.'); const fs = require('fs-extra'); -const console = require('console'); +//const console = require('console'); jest.mock('path'); jest.mock('glob'); jest.mock('fs-extra'); -jest.mock('console'); +//jest.mock('console'); const mocks = {}; - const path = require('path'); describe('TranslationStaticAnalyzer', () => { beforeEach(() => { mocks.processOn = jest.spyOn(process, 'on'); - mocks.consoleLog = jest.spyOn(console, 'log'); + //mocks.consoleLog = jest.spyOn(console, 'log'); path.relative.mockImplementation((from, to) => { return to; }); @@ -40,7 +41,7 @@ describe('TranslationStaticAnalyzer', () => { target: 'test directory targets', }); - delete analyzer.instance.cache.template; + delete analyzer.referenceTemplate; fs.actions.length = 0; analyzer.write(); @@ -48,40 +49,6 @@ describe('TranslationStaticAnalyzer', () => { expect(fs.actions).toEqual([]); }); - it('handles write gracefully when cache object is missing', () => { - const analyzer = new TranslationStaticAnalyzer({ - files: 'test files', - locales: ['existing'], - target: 'test directory targets', - }); - - delete analyzer.instance.cache; - fs.actions.length = 0; - - analyzer.write(); - - expect(fs.actions).toEqual([]); - }); - - it('calls cleanup on exit', () => { - 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'); - - exitCallback[1](); - sigIntCallback[1](); - - expect(fs.removeSync.mock.calls).toEqual([["/test/tmp/0"], ["/test/tmp/0"]]); - }); - it('works with defaults for language with some prefilled data', () => { const analyzer = new TranslationStaticAnalyzer({ files: 'test files', @@ -92,123 +59,122 @@ describe('TranslationStaticAnalyzer', () => { analyzer.update(); 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};" - }, - { - "action": "read", - "filename": "src/pages/About/index.js", - "data": "export default class AboutPage extends Component {\n static get title() {\n return __('About');\n }\n\n static get template() {\n return
{{__('Search')}} Welcome to the about page!
';\n\n }\n};" - }, - { - "action": "read", - "filename": "src/index.js", - "data": "export default class Application extends Component {\n static get title() {\n return __('Application');\n }\n};" - }, - { - "action": "read", - "filename": "src/test.js", - "data": null - }, - { - "action": "read", - "filename": "./locales/existing.json", - "data": "{\"Search\":\"検索\",\"test unused key\":\"test value\",\"Application\":\"アプリケーション\"}" - }, - { - "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}" - }, - { - "action": "read", - "filename": "src/pages/.locales/existing.json", - "data": null - }, - { - "action": "write", - "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", - "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}" - }, - { - "action": "read", - "filename": "src/pages/Search/.locales/existing.json", - "data": "{\"Search\":\"\"}" - }, - { - "action": "write", - "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", - "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}" - }, - { - "action": "read", - "filename": "src/pages/About/.locales/existing.json", - "data": "{}" - }, - { - "action": "write", - "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", - "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}" - }, - { - "action": "read", - "filename": "src/application/.locales/existing.json", - "data": null - }, - { - "action": "write", - "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", - "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\"\n }\n}" - } + { + "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};" + }, + { + "action": "read", + "filename": "src/pages/About/index.js", + "data": "export default class AboutPage extends Component {\n static get title() {\n return __('About');\n }\n\n static get template() {\n return
{{__('Search')}} Welcome to the about page!
';\n\n }\n};" + }, + { + "action": "read", + "filename": "src/index.js", + "data": "export default class Application extends Component {\n static get title() {\n return __('Application');\n }\n};" + }, + { + "action": "read", + "filename": "src/test.js", + "data": null + }, + { + "action": "read", + "filename": "./locales/existing.json", + "data": "{\"Search\":{\"default\":\"検索\"},\"test unused key\":{\"default\":\"test value\"},\"Application\":{\"default\":\"アプリケーション\"}}" + }, + { + "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 \"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 },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}" + }, + { + "action": "read", + "filename": "src/pages/.locales/existing.json", + "data": null + }, + { + "action": "write", + "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", + "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}" + }, + { + "action": "read", + "filename": "src/pages/Search/.locales/existing.json", + "data": "{\"Search\":\"\"}" + }, + { + "action": "write", + "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", + "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}" + }, + { + "action": "read", + "filename": "src/pages/About/.locales/existing.json", + "data": "{}" + }, + { + "action": "write", + "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", + "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n }\n}" + }, + { + "action": "read", + "filename": "src/application/.locales/existing.json", + "data": null + }, + { + "action": "write", + "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", + "data": "{\n \"existing\": {\n \"Application\": \"アプリケーション\"\n }\n}" + } ]); fs.actions.length = 0; analyzer.update(); - - expect(fs.actions).toEqual([ + expect(fs.actions).toEqual([ { "action": "read", "filename": "src/pages/Search/index.js", @@ -232,9 +198,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 \"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 },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}" } - ]); + ]); }); describe('read', () => { @@ -307,91 +273,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}" - } ] ); }); @@ -415,46 +296,31 @@ describe('TranslationStaticAnalyzer', () => { analyzer.update(['src/pages/Search/index.js']); 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}" - }, - { - "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}" - }, - { - "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": "./locales/existing.json", + "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"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 },\n \"test unused key\": {\n // UNUSED\n \"default\": \"test value\"\n }\n}" + }, + { + "action": "write", + "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}" + }, + { + "action": "read", + "filename": "src/pages/.locales/existing.json", + "data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}" + }, + { + "action": "read", + "filename": "src/pages/About/.locales/existing.json", + "data": "{\n \"Application\": \"アプリケーション\",\n \"Search\": \"検索\"\n}" + }, + { + "action": "read", + "filename": "src/application/.locales/existing.json", + "data": "{\n \"Application\": \"アプリケーション\"\n}" + } ]); }); @@ -469,19 +335,36 @@ 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 + "default": "検索", + // NEW + // src/pages/Search/index.js:6 + "menuitem": "" + }, + "test unused key": { + // UNUSED + "default": "test value" + } }` ); @@ -494,30 +377,37 @@ 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" + } }` ); - /* - + console.log(JSON.stringify(fs.actions, null, 4)); 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 \"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 +423,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', () => { @@ -570,23 +464,18 @@ 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 \"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 },\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", @@ -655,43 +544,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 \"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 },\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 \"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 },\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", @@ -816,7 +690,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'; @@ -839,116 +713,56 @@ describe('TranslationStaticAnalyzer', () => { analyzer.update(); 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};" - }, - { - "action": "read", - "filename": "src/pages/About/index.js", - "data": "export default class AboutPage extends Component {\n static get title() {\n return __('About');\n }\n\n static get template() {\n return
{{__('Search')}} Welcome to the about page!
';\n\n }\n};" - }, - { - "action": "read", - "filename": "src/index.js", - "data": "export default class Application extends Component {\n static get title() {\n return __('Application');\n }\n};" - }, - { - "action": "read", - "filename": "src/test.js", - "data": null - }, - { - "action": "read", - "filename": "./locales/new.json", - "data": null - }, - { - "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}" - }, - { - "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}" - } + { + "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};" + }, + { + "action": "read", + "filename": "src/pages/About/index.js", + "data": "export default class AboutPage extends Component {\n static get title() {\n return __('About');\n }\n\n static get template() {\n return
{{__('Search')}} Welcome to the about page!
';\n\n }\n};" + }, + { + "action": "read", + "filename": "src/index.js", + "data": "export default class Application extends Component {\n static get title() {\n return __('Application');\n }\n};" + }, + { + "action": "read", + "filename": "src/test.js", + "data": null + }, + { + "action": "read", + "filename": "./locales/new.json", + "data": null + }, + { + "action": "write", + "filename": "./locales/new.json", + "data": "{\n \"%d result\": {\n // NEW\n // src/pages/Search/index.js:6\n \"default\": {\"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 }\n}" + }, + { + "action": "read", + "filename": "src/pages/.locales/new.json", + "data": null + }, + { + "action": "read", + "filename": "src/pages/Search/.locales/new.json", + "data": null + }, + { + "action": "read", + "filename": "src/pages/About/.locales/new.json", + "data": null + }, + { + "action": "read", + "filename": "src/application/.locales/new.json", + "data": null + } ]); }); @@ -969,7 +783,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 +808,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 \"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 +870,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 +895,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 \"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", @@ -1203,11 +942,6 @@ 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", 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"