diff --git a/README.md b/README.md index f07c7e7..717775b 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ A library for scanning javscript files to build translation mappings in json aut - 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 +- 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) ## What does it do? @@ -36,7 +37,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'); 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..34c8264 100644 --- a/src/README.md +++ b/src/README.md @@ -14,8 +14,9 @@ A library for scanning javscript files to build translation mappings in json aut - 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 +- 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) ## What does it do? @@ -36,7 +37,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'); 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"