From 85ad9cdd62cdf2825b092bb2cbf48e5ba8518446 Mon Sep 17 00:00:00 2001 From: Zachary Dovel Date: Sat, 8 Sep 2018 18:46:02 -0700 Subject: [PATCH] Update documentation an build --- .travis.yml | 2 + README.md | 280 ++++++++++++++++++++++------------- package.json | 12 +- scripts/build.sh | 23 ++- scripts/clean.sh | 18 +-- scripts/cover.sh | 12 +- scripts/deploy.sh | 16 +- scripts/document.sh | 19 ++- scripts/lint.sh | 13 +- scripts/postProcessReadme.js | 54 +++++++ scripts/test.sh | 14 +- src/.babelrc | 3 - src/.babelrc.js | 20 +++ src/README.md | 189 +++++++++++++++++++++++ src/index.js | 151 +++---------------- yarn.lock | 23 ++- 16 files changed, 580 insertions(+), 269 deletions(-) create mode 100755 scripts/postProcessReadme.js delete mode 100644 src/.babelrc create mode 100644 src/.babelrc.js create mode 100644 src/README.md diff --git a/.travis.yml b/.travis.yml index bdb5d08..2281cfd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: node_js cache: yarn node_js: + - "6" +# - "7" NOT SUPPORTED BY fs-extra - "8" - "9" - "10" diff --git a/README.md b/README.md index ca1a220..f3f0ce7 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,14 @@ - +# @zakkudo/translation-static-analyzer -## TranslationStaticAnalyzer A library for scanning javscript files to build translation mappings in json automatically. -

- - Build Status - - Coverage Status - - Known Vulnerabilities -

- -Why use this? +[![Build Status](https://travis-ci.org/zakkudo/translation-static-analyzer.svg?branch=master)](https://travis-ci.org/zakkudo/translation-static-analyzer) +[![Coverage Status](https://coveralls.io/repos/github/zakkudo/translation-static-analyzer/badge.svg?branch=master)](https://coveralls.io/github/zakkudo/translation-static-analyzer?branch=master) +[![Known Vulnerabilities](https://snyk.io/test/github/zakkudo/translation-static-analyzer/badge.svg)](https://snyk.io/test/github/zakkudo/translation-static-analyzer) +[![Node](https://img.shields.io/node/v/@zakkudo/translation-static-analyzer.svg)](https://nodejs.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +## Why use this? - You no longer have to manage hierarchies of translations - Templates are automatically generated for the translators @@ -26,40 +17,61 @@ Why use this? - Any string wrapped in `__()` or `__n()`, will be picked up as a translatable making usage extremely easy for developers -What does it do? +## What does it do? -- I generates a locales directory filled with templates where the program was run, used by humans to translate -- It generates .locale directories optimized for loading in each of the directories passed to targets -- You load those translations from .locales as you need them +- It searches your source code for translatable strings and aggregates them +- It writes human-centric translation templates in json5 showing usages, new strings and no longer used strings +- It generates developer-centric optimized json templates, stripping away any unused strings and metadata -Install with: +## Install ```console -yarn add @zakkudo/translation-static-analyzer +# Install using npm +npm install @zakkudo/translation-static-analyzer ``` -Also consider `@zakkudo/translate-webpack-plugin` which is a wrapper for this library -for webpack and `@zakkudo/translator` for a library that can read the localization with -no fuss and apply the translations. See the [Polymer 3 Starter Project](https://github.com/zakkudo/polymer-3-starter-project) -for an example of using this library. +``` console +# Install using yarn +yarn add @zakkudo/translation-static-analyzer +``` -**Example** *(Usage for just translating everything in a project)* -```js +## Setup +1. Wrap strings you want to be translated in `__('text')` or `__n('singlular', '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'); const analyzer = new TransalationStaticAnalyzer({ - files: 'src/**/*.js', // Analyzes all javscript files in the src directory - debug: true, // Enables verbose output - locales: ['fr', 'en'], // generate a locales/fr.json as well as a locales/en.json - target: 'src' // Consolidate all of the localizations into src + // Analyzes all javscript files in the src directory, which is a good initial value + files: 'src/**/*.js', + // Use verbose output to see what files are parsed, what keys are extracted, and where they are being written to + debug: true, + // You do not need to add your default language (which for most people will be English) + locales: ['fr'], + // Consolidate all of the optimized localizations into `src/.locale`, good as an initial configuration + target: 'src' }); -analyzer.update(); +// Use `read` and `write` to brute force updates to the translations, avoiding optimizations that reduce disk usage. + +// Reads the source files that match `src/**/*.js` and parses out any translation keys, merging it into the database +analyzer.read(); + +// Updates the `locales` translation templates for the translators and then writes the optimized `src/.locales` templates for the developers +analyzer.write(); +``` +3. Add `.locales` to your `.gitignore` so it isn't commited. It is a dynamic source file that has no value being added to a repository. Its existance in the `src` directory is simply to facilitate importing it. +4. Add `find src -name '.locales' | xargs rm -r` to your clean scripts for an easy way to remove the auto generated `src/.locales` from your source code +5. Import locales into your source code from the `src/.locales` folder so you can merge it into the lookup of `@zakkudo/translator`. It is plain old json with the untranslated and unexisting values optimized out. +6. Have your localization team use the files from `locales` (without a period.) It's annoted with information about new, unused, and existing usages of the translations to help them audit what needs updating. + +You'll end up with a file structure similar to below. +``` File Structure -├── locales <- Your translators translate this +├── locales <- For your translators │ ├── en.json │ └── fr.json └── src - ├── .locales <- Auto generated, should probably be added to .gitignore + ├── .locales <- For your developers │ ├── en.json │ └── fr.json └── pages @@ -68,129 +80,187 @@ File Structure └── Search └── index.js ``` -**Example** *(Usage for splitting transaltions between dynamically imported pages of a web app)* -```js + +Where `locales/fr.json` will look like this for use by your translators: +``` json5 +{ + // NEW + // src/pages/AboutPage/index.js:14 + "About": "", + // UNUSED + "Search Page": "French translation", + // src/pages/AboutPage/index.js:40 + "There is one user": {"one":"French translation", "other":"French translation"}, + // src/pages/AboutPage/index.js:38 + "Welcome to the about page!": "French translation" +} +``` + +And the optimized `src/.locales/fr.json` will look like this for use by your developers: +``` json +{ + "There is one user": {"one":"French translation", "other":"French translation"}, + "Welcome to the about page!": "French translation" +} +``` + +Your developers will use the translation similarly to below: +``` javascript +import Translator from '@zakkudo/translator'; +import fr from 'src/.locales/fr.json'; +const translator = new Translator(); +const {__, __n} = translator; +const language = navigator.language.split('-')[0]; + +translator.setLocalization('fr', fr); +translator.setLocale(language); + +document.title = __('About'); +document.body.innerHTML = __n('There is one user', 'There are %d users', 2); +``` + +## Examples + +### Configure the analyzer to build a single `.locales` directory +``` javascript const TranslationStaticAnalyzer = require('@zakkudo/translation-static-analyzer'); const analyzer = new TransalationStaticAnalyzer({ - files: 'src/**/*.js', // Analyzes all javscript files in the src directory - debug: true, // Enables verbose output - locales: ['fr', 'en'], // generate a locales/fr.json as well as a locales/en.json - target: 'src/pages/*' // Each page in the folder will get it's own subset of translations + files: 'src/**/*.js', + locales: ['es', 'fr'], + target: 'src' }); -analyzer.update(); +``` +``` File Structure -├── locales <- Your translators translate this -│ ├── en.json +├── locales <- For your translators. Contains translations for everything +│ ├── es.json │ └── fr.json └── src + ├── Application.js + ├── .locales <- For your developers. Contains translations for everything + │ ├── es.json + │ └── fr.json └── pages ├── About - │ ├── .locales <- Auto generated, should probably be added to .gitignore - │ │ ├── en.json + │ └── index.js + └── Search + └── index.js +``` + +### Configure the analyzer for a split `.locales` directory +``` javascript +const TranslationStaticAnalyzer = require('@zakkudo/translation-static-analyzer'); +const analyzer = new TransalationStaticAnalyzer({ + files: 'src/**/*.js', + locales: ['es', 'fr'], + target: 'src/pages/*' +}); +``` + +``` +File Structure +├── locales <- For your translators. Contains translations for everything +│ ├── es.json +│ └── fr.json +└── src + ├── Application.js + └── pages + ├── About + │ ├── .locales <- For your developers. Contains translations for `Application.js` and `About/index.js` + │ │ ├── es.json │ │ └── fr.json │ └── index.js └── Search - ├── .locales <- Auto generated, should probably be added to .gitignore - │ ├── en.json + ├── .locales <- For your developers. Contains translations for `Application.js` and `Search/index.js` + │ ├── es.json │ └── fr.json └── index.js ``` -**Example** *(Generated translation templates)* -```js -{ - // NEW - // src/Application/pages/AboutPage/index.js:14 - "About": "", - // UNUSED - "This isn't used anymore": "So the text here doesn't really do anything", - // src/Application/pages/AboutPage/index.js:38 - "Welcome to the about page!": "ようこそ" -} -``` -**Example** *(Use the translations with @zakkudo/translator)* -```js -import Translator from '@zakkudo/translator'; -import localization = from './src/.locales/ja.json'; //Generated by the analyzer -const translator = new Translator(); -translator.mergeLocalization('ja', localization); //Load the localization -translator.setLocale('ja'); //Tell the translator to use it +## Also see -const translated = translator.__('I love fish'); //Translate! -const translated = translator.__n('There is a duck in the pond.', 'There are %d ducks in the pond', 3); //Translate! -``` +- `@zakkudo/translate-webpack-plugin` which is a wrapper for this library +for use with [webpack](https://webpack.js.org) +- `@zakkudo/translator` is a library that can read the localization with +no fuss and apply the translations. +- [Polymer 3 Starter Project](https://github.com/zakkudo/polymer-3-starter-project) +is an example project using this library. + +## API -* [TranslationStaticAnalyzer](#module_TranslationStaticAnalyzer) - * [~TranslationStaticAnalyzer](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer) - * [new TranslationStaticAnalyzer(options)](#new_module_TranslationStaticAnalyzer..TranslationStaticAnalyzer_new) - * [.templateDirectory](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer+templateDirectory) ⇒ String - * [.read([requestFiles])](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer+read) ⇒ Boolean - * [.write()](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer+write) - * [.update([requestFiles])](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer+update) + - + -### TranslationStaticAnalyzer~TranslationStaticAnalyzer -Class description. +### @zakkudo/translation-static-analyzer~TranslationStaticAnalyzer ⏏ +Class for analyzing javascript source files, extracting the translations, and converting them into +localization templates. -**Kind**: inner class of [TranslationStaticAnalyzer](#module_TranslationStaticAnalyzer) +**Kind**: Exported class -* [~TranslationStaticAnalyzer](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer) - * [new TranslationStaticAnalyzer(options)](#new_module_TranslationStaticAnalyzer..TranslationStaticAnalyzer_new) - * [.templateDirectory](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer+templateDirectory) ⇒ String - * [.read([requestFiles])](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer+read) ⇒ Boolean - * [.write()](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer+write) - * [.update([requestFiles])](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer+update) +* [~TranslationStaticAnalyzer](#module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer) + * [new TranslationStaticAnalyzer(options)](#new_module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer_new) + * [.templateDirectory](#module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer+templateDirectory) ⇒ String + * [.read([requestFiles])](#module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer+read) ⇒ Boolean + * [.write()](#module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer+write) + * [.update([requestFiles])](#module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer+update) - + #### new TranslationStaticAnalyzer(options) | Param | Type | Default | Description | | --- | --- | --- | --- | | options | Object | | The modifiers for how the analyzer is run | -| options.files | String | | A glob of the files to pull translations from | +| options.files | String | | A [glob pattern](https://www.npmjs.com/package/glob) of the files to pull translations from | | [options.debug] | Boolean | false | Show debugging information in the console | | [options.locales] | Array.<String> | [] | The locales to generate (eg fr, ja_JP, en) | -| [options.templates] | String | | The location to store the translator translatable templates for each language | -| [options.target] | String | | Where to write the final translations, which can be split between multiple directories for modularity. | +| [options.templates] | String | | The location to store the translator translatable templates for each language. Defaults to making a `locales` directory in the current working directory | +| [options.target] | String | | Where to write the final translations, which can be split between multiple directories for modularity. If there are no targets, no `.locales` directory will be generated anywhere. | - + #### translationStaticAnalyzer.templateDirectory ⇒ String -**Kind**: instance property of [TranslationStaticAnalyzer](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer) +**Kind**: instance property of [TranslationStaticAnalyzer](#module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer) **Returns**: String - The path to the directory which holds the translation templates that are dynamically updated by code changes and should be used by translators to add the localizations. - + #### translationStaticAnalyzer.read([requestFiles]) ⇒ Boolean -Read changes from the source files and update the language templates. +Read changes from the source files and update the database stored in the current +analyzer instance. No changes will be written to the templates and all reads are +accumulative for the next write. Use the `requestFiles` option if you want to hook +this method up to a file watcher which can supply a list of files that have changed. -**Kind**: instance method of [TranslationStaticAnalyzer](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer) +**Kind**: instance method of [TranslationStaticAnalyzer](#module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer) **Returns**: Boolean - True if some some of the modified files matches the file option passed on initialization | Param | Type | Default | Description | | --- | --- | --- | --- | -| [requestFiles] | Array.<String> | [] | The files or none to update everything in the options.files glob pattern. | +| [requestFiles] | Array.<String> | [] | A subset of files from the `options.files` glob to read or non to reread all files. Any files that are supplied to this method that are not part of the `options.files` glob are simply ignored. | - + #### translationStaticAnalyzer.write() -Write to the targets. Use to force an update of the targets if a -language file template in the templateDirectory is updated without +Write the current database to the templates and targets. This method is +useful to force an update of the targets if a +language file template in `templateDirectory` is updated without updating a source file. -**Kind**: instance method of [TranslationStaticAnalyzer](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer) - +**Kind**: instance method of [TranslationStaticAnalyzer](#module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer) + #### translationStaticAnalyzer.update([requestFiles]) -Updates the translations to match the source files. +Updates the translations to match the source files, using logic to try to reduce disk writes +if no source files changed. This method was designed to be hooked up to a file watcher for the source +code. *There will be no changes if this method is called after there is a manual change to the translation +templates. It only cares about source files.* -**Kind**: instance method of [TranslationStaticAnalyzer](#module_TranslationStaticAnalyzer..TranslationStaticAnalyzer) +**Kind**: instance method of [TranslationStaticAnalyzer](#module_@zakkudo/translation-static-analyzer..TranslationStaticAnalyzer) | Param | Type | Default | Description | | --- | --- | --- | --- | diff --git a/package.json b/package.json index 769cf48..bc08a89 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,19 @@ "files": [ "*" ], + "engines": { + "node": ">=6.0.0 <7.0.0 || >=8.0.0" + }, "repository": "github:zakkudo/translation-static-analyzer", "license": "BSD-3-Clause", "devDependencies": { "@babel/cli": "^7.0.0-beta.56", "@babel/core": "^7.0.0-beta.56", + "@babel/plugin-transform-runtime": "^7.0.0", "@babel/preset-env": "^7.0.0-beta.56", "babel-core": "^7.0.0-0", "babel-jest": "^23.4.2", + "babel-plugin-transform-undefined-to-void": "^6.9.4", "eslint": "^4.19.1", "eslint-plugin-jasmine": "^2.10.1", "eslint-plugin-jest": "^21.21.0", @@ -30,10 +35,10 @@ "jest": "^23.4.2", "jest-cli": "^23.4.2", "jsdoc": "^3.5.5", - "jsdoc-to-markdown": "^4.0.1", - "regenerator-runtime": "^0.12.1" + "jsdoc-to-markdown": "^4.0.1" }, "dependencies": { + "@babel/runtime-corejs2": "^7.0.0", "deep-equal": "^1.0.1", "fs-extra": "^7.0.0", "glob": "^7.1.2", @@ -49,8 +54,5 @@ "lint": "scripts/lint.sh", "deploy": "scripts/deploy.sh", "test": "scripts/test.sh" - }, - "engines": { - "node": ">=8.0.0" } } diff --git a/scripts/build.sh b/scripts/build.sh index aae5062..d767af8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -2,4 +2,25 @@ set -e -./node_modules/.bin/babel src --out-dir build --source-maps inline --ignore "src/*.test.js" --ignore "src/test.js" +export NODE_ENV="build" + +CURRENT_DIR=$(pwd) +PROJECT_DIR=$(git rev-parse --show-toplevel) + +cd $PROJECT_DIR + +./scripts/clean.sh +./scripts/document.sh + +mkdir build + +cp package.json build/package.json +cp README.md build/README.md + +./node_modules/.bin/babel src \ + --out-dir build \ + --source-maps inline \ + --ignore "src/test.js" \ + --ignore "src/*.test.js" \ + --verbose \ + "$@" diff --git a/scripts/clean.sh b/scripts/clean.sh index 74a1c92..88169a1 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -2,15 +2,13 @@ set -e +CURRENT_DIR=$(pwd) PROJECT_DIR=$(git rev-parse --show-toplevel) -BIN_DIR=$(npm bin) -JSDOC="$BIN_DIR/jsdoc" -rm -rf $PROJECT_DIR/build -rm -rf $PROJECT_DIR/coverage -rm -rf $PROJECT_DIR/documentation -rm -rf $PROJECT_DIR/demo -rm -f $PROJECT_DIR/.karma-test-results.json -rm -f $PROJECT_DIR/.jest-test-results.json -rm -f $PROJECT_DIR/jsdoc.*.conf.tmp -rm -f $PROJECT_DIR/yarn-error.log +cd $PROJECT_DIR + +rm -rf build +rm -rf coverage +rm -rf documentation +rm -f jsdoc.*.conf.tmp +rm -f yarn-error.log diff --git a/scripts/cover.sh b/scripts/cover.sh index faadd02..36805ef 100755 --- a/scripts/cover.sh +++ b/scripts/cover.sh @@ -1,3 +1,11 @@ -#!/bin/sh +#!/bin/bash -./node_modules/.bin/jest --coverage --config jest.config.js +set -e + +CURRENT_DIR=$(pwd) +PROJECT_DIR=$(git rev-parse --show-toplevel) + +cd $PROJECT_DIR + +./scripts/clean.sh +./scripts/test.sh --coverage --coveragePathIgnorePatterns '.*TestHelper.js' "$@" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 5a5ee94..f765a35 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -2,11 +2,17 @@ set -e -yarn build -yarn document -yarn cover +CURRENT_DIR=$(pwd) +PROJECT_DIR=$(git rev-parse --show-toplevel) -cp README.md build/README.md -cp package.json build/package.json +cd $PROJECT_DIR + +./scripts/cover.sh +./scripts/document.sh +./scripts/build.sh + +yarn version + +./scripts/build.sh yarn publish --access public --cwd build --no-git-tag-version diff --git a/scripts/document.sh b/scripts/document.sh index 2bf9927..b1b2bec 100755 --- a/scripts/document.sh +++ b/scripts/document.sh @@ -2,10 +2,23 @@ set -e +export NODE_ENV="document" + +CURRENT_DIR=$(pwd) PROJECT_DIR=$(git rev-parse --show-toplevel) BIN_DIR=$(npm bin) JSDOC="$BIN_DIR/jsdoc" +OPTIONS="--module-index-format none --global-index-format none --example-lang js --heading-depth 3" + +cd $PROJECT_DIR + +$JSDOC -c jsdoc.config.json "$@" +cat src/README.md > README.md + +echo "" >> README.md +echo "## API" >> README.md +echo "" >> README.md + +./node_modules/.bin/jsdoc2md src/index.js $OPTIONS >> README.md -$JSDOC -c $PROJECT_DIR/jsdoc.config.json "$@" -./node_modules/.bin/jsdoc2md src/*.js > README.md -sed -i '' 's/\\\//\//g' README.md +./scripts/postProcessReadme.js README.md diff --git a/scripts/lint.sh b/scripts/lint.sh index bfc6de9..e8a6b43 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,3 +1,12 @@ -#!/bin/sh +#!/bin/bash -./node_modules/.bin/eslint src +set -e + +export NODE_ENV="lint" + +CURRENT_DIR=$(pwd) +PROJECT_DIR=$(git rev-parse --show-toplevel) + +cd $PROJECT_DIR + +./node_modules/.bin/eslint src "$@" diff --git a/scripts/postProcessReadme.js b/scripts/postProcessReadme.js new file mode 100755 index 0000000..95015e3 --- /dev/null +++ b/scripts/postProcessReadme.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +const fs = require('fs'); + +const arguments = process.argv.slice(2);; +const filename = arguments[0]; +const contents = String(fs.readFileSync(filename)); +const lines = contents.split('\n'); + +let isIndex = false; +let isApiSection = false; + +const newContents = lines.filter((l) => { + if (l.startsWith('* [@')) { + isIndex = true; + } else if (l === '') { + isIndex = false; + } + + return !isIndex; +}).filter((l) => { + return !l.startsWith('### @'); +}).map((l, index, lines) => { + const innerClassOfPrefix = '**Kind**: inner class of'; + const innerMethodOfPrefix = '**Kind**: inner method of'; + + if (l.startsWith('#### @')) { + return l + ' ⏏'; + } + + if (l.startsWith(innerClassOfPrefix)) { + return '\n**Kind**: Exported class\n'; + } + + if (l.startsWith(innerMethodOfPrefix)) { + return '\n**Kind**: Exported function\n'; + } + + return l; +}).map((l, index, lines) => { + if (l === '## API') { + isApiSection = true; + return l; + } + + if (isApiSection && l.startsWith('##')) { + return l.slice(1); + } + + return l; +}).join('\n').replace(/\n\n+/gm, '\n\n').replace('@', '@').replace('/', '/'); + +fs.writeFileSync(filename, newContents); + diff --git a/scripts/test.sh b/scripts/test.sh index 040cf71..a4e1107 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,3 +1,13 @@ -#!/bin/sh +#!/bin/bash -./node_modules/.bin/jest --runInBand +set -e + +export NODE_ENV="test" + +CURRENT_DIR=$(pwd) +PROJECT_DIR=$(git rev-parse --show-toplevel) + +cd $PROJECT_DIR + +./scripts/clean.sh +./node_modules/.bin/jest --runInBand "$@" diff --git a/src/.babelrc b/src/.babelrc deleted file mode 100644 index 309070e..0000000 --- a/src/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/env"] -} diff --git a/src/.babelrc.js b/src/.babelrc.js new file mode 100644 index 0000000..f3de39d --- /dev/null +++ b/src/.babelrc.js @@ -0,0 +1,20 @@ +module.exports = { + "presets": [ + [ + "@babel/env", { + "debug": process.env.NODE_ENV === 'build', + "targets": {"browsers": [ + "last 1 version", + "> 1%", + "not dead" + ], "node": "6"} + } + ] + ], + "plugins": [ + ["@babel/transform-runtime", {"corejs": 2}], + "transform-undefined-to-void" + ], + minified: true, + comments: false +} diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..6599020 --- /dev/null +++ b/src/README.md @@ -0,0 +1,189 @@ +# @zakkudo/translation-static-analyzer + +A library for scanning javscript files to build translation mappings in json automatically. + +[![Build Status](https://travis-ci.org/zakkudo/translation-static-analyzer.svg?branch=master)](https://travis-ci.org/zakkudo/translation-static-analyzer) +[![Coverage Status](https://coveralls.io/repos/github/zakkudo/translation-static-analyzer/badge.svg?branch=master)](https://coveralls.io/github/zakkudo/translation-static-analyzer?branch=master) +[![Known Vulnerabilities](https://snyk.io/test/github/zakkudo/translation-static-analyzer/badge.svg)](https://snyk.io/test/github/zakkudo/translation-static-analyzer) +[![Node](https://img.shields.io/node/v/@zakkudo/translation-static-analyzer.svg)](https://nodejs.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +## Why use this? + +- You no longer have to manage hierarchies of translations +- Templates are automatically generated for the translators +- The translations are noted if they are new, unused and what files +- It allows splitting the translations easily for dynamic imports to allow sliced loading +- Any string wrapped in `__()` or `__n()`, will be picked up as a + translatable making usage extremely easy for developers + +## What does it do? + +- It searches your source code for translatable strings and aggregates them +- It writes human-centric translation templates in json5 showing usages, new strings and no longer used strings +- It generates developer-centric optimized json templates, stripping away any unused strings and metadata + +## Install + +```console +# Install using npm +npm install @zakkudo/translation-static-analyzer +``` + +``` console +# Install using yarn +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` +2. Initialize the analyzer in your build scripts similar to below. +``` javascript +const TranslationStaticAnalyzer = require('@zakkudo/translation-static-analyzer'); +const analyzer = new TransalationStaticAnalyzer({ + // Analyzes all javscript files in the src directory, which is a good initial value + files: 'src/**/*.js', + // Use verbose output to see what files are parsed, what keys are extracted, and where they are being written to + debug: true, + // You do not need to add your default language (which for most people will be English) + locales: ['fr'], + // Consolidate all of the optimized localizations into `src/.locale`, good as an initial configuration + target: 'src' +}); + +// Use `read` and `write` to brute force updates to the translations, avoiding optimizations that reduce disk usage. + +// Reads the source files that match `src/**/*.js` and parses out any translation keys, merging it into the database +analyzer.read(); + +// Updates the `locales` translation templates for the translators and then writes the optimized `src/.locales` templates for the developers +analyzer.write(); +``` +3. Add `.locales` to your `.gitignore` so it isn't commited. It is a dynamic source file that has no value being added to a repository. Its existance in the `src` directory is simply to facilitate importing it. +4. Add `find src -name '.locales' | xargs rm -r` to your clean scripts for an easy way to remove the auto generated `src/.locales` from your source code +5. Import locales into your source code from the `src/.locales` folder so you can merge it into the lookup of `@zakkudo/translator`. It is plain old json with the untranslated and unexisting values optimized out. +6. Have your localization team use the files from `locales` (without a period.) It's annoted with information about new, unused, and existing usages of the translations to help them audit what needs updating. + +You'll end up with a file structure similar to below. +``` +File Structure +├── locales <- For your translators +│ ├── en.json +│ └── fr.json +└── src + ├── .locales <- For your developers + │ ├── en.json + │ └── fr.json + └── pages + ├── About + │ └── index.js + └── Search + └── index.js +``` + +Where `locales/fr.json` will look like this for use by your translators: +``` json5 +{ + // NEW + // src/pages/AboutPage/index.js:14 + "About": "", + // UNUSED + "Search Page": "French translation", + // src/pages/AboutPage/index.js:40 + "There is one user": {"one":"French translation", "other":"French translation"}, + // src/pages/AboutPage/index.js:38 + "Welcome to the about page!": "French translation" +} +``` + +And the optimized `src/.locales/fr.json` will look like this for use by your developers: +``` json +{ + "There is one user": {"one":"French translation", "other":"French translation"}, + "Welcome to the about page!": "French translation" +} +``` + +Your developers will use the translation similarly to below: +``` javascript +import Translator from '@zakkudo/translator'; +import fr from 'src/.locales/fr.json'; +const translator = new Translator(); +const {__, __n} = translator; +const language = navigator.language.split('-')[0]; + +translator.setLocalization('fr', fr); +translator.setLocale(language); + +document.title = __('About'); +document.body.innerHTML = __n('There is one user', 'There are %d users', 2); +``` + +## Examples + +### Configure the analyzer to build a single `.locales` directory +``` javascript +const TranslationStaticAnalyzer = require('@zakkudo/translation-static-analyzer'); +const analyzer = new TransalationStaticAnalyzer({ + files: 'src/**/*.js', + locales: ['es', 'fr'], + target: 'src' +}); +``` + +``` +File Structure +├── locales <- For your translators. Contains translations for everything +│ ├── es.json +│ └── fr.json +└── src + ├── Application.js + ├── .locales <- For your developers. Contains translations for everything + │ ├── es.json + │ └── fr.json + └── pages + ├── About + │ └── index.js + └── Search + └── index.js +``` + + +### Configure the analyzer for a split `.locales` directory +``` javascript +const TranslationStaticAnalyzer = require('@zakkudo/translation-static-analyzer'); +const analyzer = new TransalationStaticAnalyzer({ + files: 'src/**/*.js', + locales: ['es', 'fr'], + target: 'src/pages/*' +}); +``` + +``` +File Structure +├── locales <- For your translators. Contains translations for everything +│ ├── es.json +│ └── fr.json +└── src + ├── Application.js + └── pages + ├── About + │ ├── .locales <- For your developers. Contains translations for `Application.js` and `About/index.js` + │ │ ├── es.json + │ │ └── fr.json + │ └── index.js + └── Search + ├── .locales <- For your developers. Contains translations for `Application.js` and `Search/index.js` + │ ├── es.json + │ └── fr.json + └── index.js +``` + +## Also see + +- `@zakkudo/translate-webpack-plugin` which is a wrapper for this library +for use with [webpack](https://webpack.js.org) +- `@zakkudo/translator` is a library that can read the localization with +no fuss and apply the translations. +- [Polymer 3 Starter Project](https://github.com/zakkudo/polymer-3-starter-project) +is an example project using this library. diff --git a/src/index.js b/src/index.js index 654d83b..895528b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,122 +1,5 @@ /** - * A library for scanning javscript files to build translation mappings in json automatically. - * - *

- * - * Build Status - * - * Coverage Status - * - * Known Vulnerabilities - *

- * - * Why use this? - * - * - You no longer have to manage hierarchies of translations - * - Templates are automatically generated for the translators - * - The translations are noted if they are new, unused and what files - * - It allows splitting the translations easily for dynamic imports to allow sliced loading - * - Any string wrapped in `__()` or `__n()`, will be picked up as a - * translatable making usage extremely easy for developers - * - * What does it do? - * - * - I generates a locales directory filled with templates where the program was run, used by humans to translate - * - It generates .locale directories optimized for loading in each of the directories passed to targets - * - You load those translations from .locales as you need them - * - * Install with: - * - * ```console - * yarn add @zakkudo/translation-static-analyzer - * ``` - * - * Also consider `@zakkudo/translate-webpack-plugin` which is a wrapper for this library - * for webpack and `@zakkudo/translator` for a library that can read the localization with - * no fuss and apply the translations. See the [Polymer 3 Starter Project]{@link https://github.com/zakkudo/polymer-3-starter-project} - * for an example of using this library. - * - * @example Usage for just translating everything in a project - * const TranslationStaticAnalyzer = require('@zakkudo/translation-static-analyzer'); - * const analyzer = new TransalationStaticAnalyzer({ - * files: 'src/**\/*.js', // Analyzes all javscript files in the src directory - * debug: true, // Enables verbose output - * locales: ['fr', 'en'], // generate a locales/fr.json as well as a locales/en.json - * target: 'src' // Consolidate all of the localizations into src - * }); - * analyzer.update(); - * - * File Structure - * ├── locales <- Your translators translate this - * │ ├── en.json - * │ └── fr.json - * └── src - * ├── .locales <- Auto generated, should probably be added to .gitignore - * │ ├── en.json - * │ └── fr.json - * └── pages - * ├── About - * │ └── index.js - * └── Search - * └── index.js - * - * - * @example Usage for splitting transaltions between dynamically imported pages of a web app - * const TranslationStaticAnalyzer = require('@zakkudo/translation-static-analyzer'); - * const analyzer = new TransalationStaticAnalyzer({ - * files: 'src/**\/*.js', // Analyzes all javscript files in the src directory - * debug: true, // Enables verbose output - * locales: ['fr', 'en'], // generate a locales/fr.json as well as a locales/en.json - * target: 'src/pages/*' // Each page in the folder will get it's own subset of translations - * }); - * analyzer.update(); - * - * File Structure - * ├── locales <- Your translators translate this - * │ ├── en.json - * │ └── fr.json - * └── src - * └── pages - * ├── About - * │ ├── .locales <- Auto generated, should probably be added to .gitignore - * │ │ ├── en.json - * │ │ └── fr.json - * │ └── index.js - * └── Search - * ├── .locales <- Auto generated, should probably be added to .gitignore - * │ ├── en.json - * │ └── fr.json - * └── index.js - * - * @example Generated translation templates - * { - * // NEW - * // src/Application/pages/AboutPage/index.js:14 - * "About": "", - * // UNUSED - * "This isn't used anymore": "So the text here doesn't really do anything", - * // src/Application/pages/AboutPage/index.js:38 - * "Welcome to the about page!": "ようこそ" - * } - * - * - * @example Use the translations with @zakkudo/translator - * import Translator from '@zakkudo/translator'; - * import localization = from './src/.locales/ja.json'; //Generated by the analyzer - * - * const translator = new Translator(); - * translator.mergeLocalization('ja', localization); //Load the localization - * translator.setLocale('ja'); //Tell the translator to use it - * - * const translated = translator.__('I love fish'); //Translate! - * const translated = translator.__n('There is a duck in the pond.', 'There are %d ducks in the pond', 3); //Translate! - * - * @module TranslationStaticAnalyzer + * @module @zakkudo/translation-static-analyzer */ const JSON5 = require('json5'); @@ -490,18 +373,21 @@ function writeToTargets() { } /** - * Class description. + * Class for analyzing javascript source files, extracting the translations, and converting them into + * localization templates. */ class TranslationStaticAnalyzer { /** * @param {Object} options - The modifiers for how the analyzer is run - * @param {String} options.files - A glob of the files to pull translations from + * @param {String} options.files - A + * [glob pattern]{@link https://www.npmjs.com/package/glob} of the files to pull translations from * @param {Boolean} [options.debug = false] - Show debugging information in the console * @param {Array} [options.locales = []] - The locales to generate (eg fr, ja_JP, en) * @param {String} [options.templates] - The location to store - * the translator translatable templates for each language + * the translator translatable templates for each language. Defaults to + * making a `locales` directory in the current working directory * @param {String} [options.target] - Where to write the final translations, which can be split between - * multiple directories for modularity. + * multiple directories for modularity. If there are no targets, no `.locales` directory will be generated anywhere. */ constructor(options) { const localeGen = this.localeGen = { @@ -539,9 +425,13 @@ class TranslationStaticAnalyzer { } /** - * Read changes from the source files and update the language templates. - * @param {Array} [requestFiles = []] - The files or none to - * update everything in the options.files glob pattern. + * Read changes from the source files and update the database stored in the current + * analyzer instance. No changes will be written to the templates and all reads are + * accumulative for the next write. Use the `requestFiles` option if you want to hook + * this method up to a file watcher which can supply a list of files that have changed. + * @param {Array} [requestFiles = []] - A subset of files from the + * `options.files` glob to read or non to reread all files. Any files that are supplied to this + * method that are not part of the `options.files` glob are simply ignored. * @return {Boolean} True if some some of the modified files matches the * file option passed on initialization */ @@ -582,8 +472,9 @@ class TranslationStaticAnalyzer { } /** - * Write to the targets. Use to force an update of the targets if a - * language file template in the templateDirectory is updated without + * Write the current database to the templates and targets. This method is + * useful to force an update of the targets if a + * language file template in `templateDirectory` is updated without * updating a source file. */ write() { @@ -596,7 +487,10 @@ class TranslationStaticAnalyzer { } /** - * Updates the translations to match the source files. + * Updates the translations to match the source files, using logic to try to reduce disk writes + * if no source files changed. This method was designed to be hooked up to a file watcher for the source + * code. *There will be no changes if this method is called after there is a manual change to the translation + * templates. It only cares about source files.* * @param {Array} [requestFiles = []] - The files or none to * update everything in the options.files glob pattern. */ @@ -629,5 +523,4 @@ class TranslationStaticAnalyzer { } } -// Export must be separate for jsdoc ~3.5 for the class to document correctly module.exports = TranslationStaticAnalyzer; diff --git a/yarn.lock b/yarn.lock index b24c9a3..f898088 100644 --- a/yarn.lock +++ b/yarn.lock @@ -466,6 +466,14 @@ dependencies: regenerator-transform "^0.13.3" +"@babel/plugin-transform-runtime@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.0.0.tgz#0f1443c07bac16dba8efa939e0c61d6922740062" + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + resolve "^1.8.1" + "@babel/plugin-transform-shorthand-properties@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0.tgz#85f8af592dcc07647541a0350e8c95c7bf419d15" @@ -552,6 +560,13 @@ js-levenshtein "^1.1.3" semver "^5.3.0" +"@babel/runtime-corejs2@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.0.0.tgz#786711ee099c2c2af7875638866c1259eff30a8c" + dependencies: + core-js "^2.5.7" + regenerator-runtime "^0.12.0" + "@babel/template@7.0.0-beta.51": version "7.0.0-beta.51" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.51.tgz#9602a40aebcf357ae9677e2532ef5fc810f5fbff" @@ -926,6 +941,10 @@ babel-plugin-syntax-object-rest-spread@^6.13.0: version "6.13.0" resolved "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" +babel-plugin-transform-undefined-to-void@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz#be241ca81404030678b748717322b89d0c8fe280" + babel-preset-jest@^23.2.0: version "23.2.0" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz#8ec7a03a138f001a1a8fb1e8113652bf1a55da46" @@ -1358,7 +1377,7 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" -core-js@^2.4.0, core-js@^2.5.0: +core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7: version "2.5.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" @@ -3775,7 +3794,7 @@ regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" -regenerator-runtime@^0.12.1: +regenerator-runtime@^0.12.0: version "0.12.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"