├── .commitlintrc.js ├── .editorconfig ├── .eslintignore ├── .gitattributes ├── .github ├── scripts │ └── decrypt_secret.sh ├── secrets │ └── my_secret.json.gpg └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.js ├── .npmignore ├── .npmrc ├── .prettierrc.js ├── .remarkignore ├── .remarkrc.js ├── .xo-config.js ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── test.js /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | }; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.*.js 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/scripts/decrypt_secret.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Decrypt the file 4 | mkdir $HOME/secrets 5 | # --batch to prevent interactive command 6 | # --yes to assume "yes" for questions 7 | gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" \ 8 | --output $HOME/secrets/my_secret.json .github/secrets/my_secret.json.gpg 9 | -------------------------------------------------------------------------------- /.github/secrets/my_secret.json.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ladjs/mandarin/973db08c779f21807e9707b9e7f853faa607b8ff/.github/secrets/my_secret.json.gpg -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | node_version: 13 | - 14 14 | - 16 15 | - 18 16 | name: Node ${{ matrix.node_version }} on ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Decrypt large secret 20 | run: .github/scripts/decrypt_secret.sh 21 | env: 22 | LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} 23 | - name: Set environment variable 24 | run: | 25 | echo "GOOGLE_APPLICATION_CREDENTIALS=$HOME/secrets/my_secret.json" >> $GITHUB_ENV 26 | - name: Setup node 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: ${{ matrix.node_version }} 30 | - name: Install dependencies 31 | run: npm install 32 | - name: Run tests 33 | run: npm run test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | .idea 4 | node_modules 5 | coverage 6 | .nyc_output 7 | locales/ 8 | package-lock.json 9 | yarn.lock 10 | 11 | Thumbs.db 12 | tmp/ 13 | temp/ 14 | *.lcov 15 | .env -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.md": filenames => filenames.map(filename => `remark ${filename} -qfo`), 3 | 'package.json': 'fixpack', 4 | '*.js': 'xo --fix' 5 | }; 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | client_secrets.json.enc 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | bracketSpacing: true, 4 | trailingComma: 'none' 5 | }; 6 | -------------------------------------------------------------------------------- /.remarkignore: -------------------------------------------------------------------------------- 1 | test/snapshots/**/*.md 2 | -------------------------------------------------------------------------------- /.remarkrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['preset-github'] 3 | }; 4 | -------------------------------------------------------------------------------- /.xo-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prettier: true, 3 | space: true, 4 | extends: ['xo-lass'] 5 | }; 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nick Baugh (http://niftylettuce.com/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mandarin 2 | 3 | [![build status](https://github.com/ladjs/mandarin/actions/workflows/ci.yml/badge.svg)](https://github.com/ladjs/mandarin/actions/workflows/ci.yml) 4 | [![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 5 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 6 | [![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org) 7 | [![license](https://img.shields.io/github/license/ladjs/mandarin.svg)](LICENSE) 8 | 9 | > Automatic i18n markdown translation and i18n phrase translation using Google Translate 10 | 11 | 12 | ## Table of Contents 13 | 14 | * [Install](#install) 15 | * [Requirements](#requirements) 16 | * [Redis](#redis) 17 | * [Google Application Credentials](#google-application-credentials) 18 | * [Usage](#usage) 19 | * [Contributors](#contributors) 20 | * [License](#license) 21 | 22 | 23 | ## Install 24 | 25 | [npm][]: 26 | 27 | ```sh 28 | npm install mandarin 29 | ``` 30 | 31 | 32 | ## Requirements 33 | 34 | ### Redis 35 | 36 | You will need to have [Redis][] installed in order for caching to work properly. 37 | 38 | If you do not plan to use Redis, then set `redis: false` as an option. 39 | 40 | ### Google Application Credentials 41 | 42 | You will also need Google Application Credentials, and you will need to set them as environment variables (e.g. `GOOGLE_APPLICATION_CREDENTIALS=/home/user/Downloads/service-account-file.json`). 43 | 44 | For more information on Google Application credentials, see . 45 | 46 | 47 | ## Usage 48 | 49 | 1. Implement Mandarin and pass it an instance of [i18n][] 50 | 51 | ```js 52 | const Mandarin = require('mandarin'); 53 | const I18N = require('@ladjs/i18n'); 54 | 55 | const i18n = new I18N(); 56 | 57 | // you can also pass a custom `logger` option (it defaults to `console`) 58 | const mandarin = new Mandarin({ 59 | 60 | // REQUIRED: 61 | i18n 62 | 63 | // OPTIONAL: 64 | // logger: console, 65 | 66 | // OPTIONAL (see index.js for defaults): 67 | // redis: ... 68 | 69 | // OPTIONAL (see index.js for defaults): 70 | // redisMonitor: ... 71 | 72 | // OPTIONAL: 73 | // see all commented options from this following link: 74 | // 75 | // 76 | // clientConfig: {}, 77 | 78 | // OPTIONAL (see index.js for defaults): 79 | // Files to convert from `index.md` to `index-es.md` 80 | // Or `README.md` to `README-ZH.md` for example 81 | // 82 | // 83 | // markdown: ... (note we expose `Mandarin.DEFAULT_PATTERNS` for you) 84 | }); 85 | 86 | // 87 | // Translate Phrases 88 | // 89 | // with async/await 90 | (async () => { 91 | try { 92 | await mandarin.translate(); 93 | } catch (err) { 94 | console.log(err); 95 | } 96 | })(); 97 | 98 | // with promises and then/catch 99 | mandarin 100 | .translate() 101 | .then(() => { 102 | console.log('done'); 103 | }) 104 | .catch(console.error); 105 | 106 | // with callbacks 107 | mandarin.translate(err => { 108 | if (err) throw err; 109 | console.log('done'); 110 | }); 111 | 112 | // 113 | // Translate Markdown Files 114 | // 115 | // with async/await 116 | (async () => { 117 | try { 118 | await mandarin.markdown(); 119 | } catch (err) { 120 | console.log(err); 121 | } 122 | })(); 123 | 124 | // with promises and then/catch 125 | mandarin 126 | .markdown() 127 | .then(() => { 128 | console.log('done'); 129 | }) 130 | .catch(console.error); 131 | 132 | // with callbacks 133 | mandarin.markdown(err => { 134 | if (err) throw err; 135 | console.log('done'); 136 | }); 137 | ``` 138 | 139 | 2. This assumes that you have locale files already and a default locale file (e.g. `./locales/en.json` with phrases that need translated to other languages you support). Based off the defaults from [i18n][], you would automatically get your `en.json` file translated to the locales `es` (Spanish) and `zh` (Chinese). 140 | 141 | 3. Follow the "Before you begin" steps here (basically you download a JSON file after creating a Google Cloud Project with Cloud Translation API enabled). 142 | 143 | 4. Specify the path to the JSON file and run your script that uses `mandarin`: 144 | 145 | ```sh 146 | GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/[FILE_NAME].json" node app.js 147 | ``` 148 | 149 | 150 | ## Contributors 151 | 152 | | Name | Website | 153 | | -------------- | -------------------------- | 154 | | **Nick Baugh** | | 155 | 156 | 157 | ## License 158 | 159 | [MIT](LICENSE) © [Nick Baugh](http://niftylettuce.com/) 160 | 161 | 162 | ## 163 | 164 | [npm]: https://www.npmjs.com/ 165 | 166 | [i18n]: https://github.com/ladjs/i18n 167 | 168 | [redis]: https://redis.io/ 169 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const process = require('process'); 4 | const { isIP } = require('net'); 5 | 6 | // const formatSpecifiers = require('format-specifiers'); 7 | const Redis = require('@ladjs/redis'); 8 | const _ = require('lodash'); 9 | const autoLinkHeadings = require('remark-autolink-headings'); 10 | const debug = require('debug')('mandarin'); 11 | const emoji = require('remark-emoji'); 12 | const globby = require('globby'); 13 | const isFQDN = require('is-fqdn'); 14 | const isSANB = require('is-string-and-not-blank'); 15 | const languages = require('@cospired/i18n-iso-languages'); 16 | const modifyFilename = require('modify-filename'); 17 | const pMapSeries = require('p-map-series'); 18 | const pify = require('pify'); 19 | const rehypeRaw = require('rehype-raw'); 20 | const rehypeRewrite = require('rehype-rewrite'); 21 | const rehypeStringify = require('rehype-stringify'); 22 | const remarkParse = require('remark-parse'); 23 | const remarkPresetGitHub = require('remark-preset-github'); 24 | const remarkRehype = require('remark-rehype'); 25 | const revHash = require('rev-hash'); 26 | const sharedConfig = require('@ladjs/shared-config'); 27 | const slug = require('remark-slug'); 28 | const unified = require('unified'); 29 | const universalify = require('universalify'); 30 | const vfile = require('to-vfile'); 31 | const { v2 } = require('@google-cloud/translate'); 32 | const { isEmail, isURL } = require('validator'); 33 | 34 | const isoCodes = Object.keys(languages.getAlpha2Codes()); 35 | const writeFile = pify(fs.writeFile); 36 | const conf = _.pick(sharedConfig('MANDARIN'), [ 37 | 'logger', 38 | 'redis', 39 | 'redisMonitor' 40 | ]); 41 | 42 | const DEFAULT_PATTERNS = [ 43 | '**/*.md', 44 | '!*.md', 45 | ...isoCodes.map((code) => `!*-${code}.md`), 46 | ...isoCodes.map((code) => `!*-${code.toUpperCase()}.md`), 47 | ...isoCodes.map((code) => `!**/*-${code}.md`), 48 | ...isoCodes.map((code) => `!**/*-${code.toUpperCase()}.md`), 49 | '!test', 50 | '!coverage', 51 | '!node_modules' 52 | ]; 53 | 54 | function parsePreAndPostWhitespace(str) { 55 | const value = str.trim(); 56 | const index = str.indexOf(value); 57 | return [str.slice(0, index), value, str.slice(index + value.length)]; 58 | } 59 | 60 | class Mandarin { 61 | constructor(config = {}) { 62 | this.config = _.merge( 63 | { 64 | ..._.merge(conf, { 65 | redis: { 66 | keyPrefix: `mandarin_${( 67 | process.env.NODE_ENV || 'development' 68 | ).toLowerCase()}` 69 | } 70 | }), 71 | i18n: false, 72 | // 73 | // NOTE: you can pass `GOOGLE_APPLICATION_CREDENTIALS` as an environment variable 74 | // or you can pass individual environment variables 75 | // 76 | // OPTIONAL: 77 | // see all commented options from this following link: 78 | // https://googleapis.dev/nodejs/translate/5.0.1/v2_index.js.html 79 | // 80 | clientConfig: {}, 81 | // 82 | // Files to convert from `index.md` to `index-es.md` 83 | // Or `README.md` to `README-ZH.md` for example 84 | // https://github.com/sindresorhus/globby 85 | // 86 | markdown: { 87 | patterns: DEFAULT_PATTERNS, 88 | options: { 89 | gitignore: true 90 | } 91 | } 92 | }, 93 | config 94 | ); 95 | 96 | debug(this.config); 97 | 98 | if (!this.config.i18n) throw new Error('i18n instance option required'); 99 | 100 | // initialize redis 101 | this.redisClient = 102 | this.config.redis === false 103 | ? false 104 | : _.isPlainObject(this.config.redis) 105 | ? new Redis( 106 | this.config.redis, 107 | this.config.logger, 108 | this.config.redisMonitor 109 | ) 110 | : this.config.redis; 111 | 112 | // setup google translate with api key 113 | this.client = new v2.Translate(this.config.clientConfig); 114 | 115 | this.translate = universalify.fromPromise(this.translate).bind(this); 116 | this.markdown = universalify.fromPromise(this.markdown).bind(this); 117 | this.parseMarkdownFile = universalify 118 | .fromPromise(this.parseMarkdownFile) 119 | .bind(this); 120 | this.getLocalizedMarkdownFileName = universalify 121 | .fromPromise(this.getLocalizedMarkdownFileName) 122 | .bind(this); 123 | } 124 | 125 | getLocalizedMarkdownFileName(filePath, locale) { 126 | debug('getLocalizedMarkdownFileName', filePath, locale); 127 | return modifyFilename(filePath, (filename, extension) => { 128 | const isUpperCase = filename.toUpperCase() === filename; 129 | return `${filename}-${ 130 | isUpperCase ? locale.toUpperCase() : locale.toLowerCase() 131 | }${extension}`; 132 | }); 133 | } 134 | 135 | async parseMarkdownFile(filePath) { 136 | debug('parseMarkdownFile', filePath); 137 | const markdown = await vfile.read(filePath); 138 | // don't translate the main file.md file, only for other locales 139 | const locales = this.config.i18n.config.locales.filter( 140 | (locale) => locale !== this.config.i18n.config.defaultLocale 141 | ); 142 | const files = await Promise.all( 143 | locales.map((locale) => { 144 | return new Promise((resolve, reject) => { 145 | unified() 146 | // 147 | .use(remarkPresetGitHub) 148 | .use(remarkParse) 149 | .use(slug) 150 | .use(autoLinkHeadings, { 151 | behavior: 'prepend', 152 | content: { 153 | type: 'element', 154 | tagName: 'i', 155 | properties: { 156 | className: ['fa', 'fa-link', 'mr-2', 'text-dark'] 157 | }, 158 | children: [] 159 | } 160 | }) 161 | .use(emoji) 162 | .use(remarkRehype, { allowDangerousHtml: true }) 163 | .use(rehypeRaw) 164 | .data('settings', { fragment: true, emitParseErrors: true }) 165 | .use(rehypeRewrite, (node, index, parent) => { 166 | if ( 167 | locale !== 'en' && 168 | node.type === 'text' && 169 | parent.tagName !== 'code' && 170 | isSANB(node.value) && 171 | node.value !== node.value.toUpperCase() 172 | ) { 173 | // if the `parent.tagName` is `code` 174 | // or if the `node.value` is empty string (and not just \n either) 175 | // or if the `node.value` when converted to uppercase is the same (e.g. abbreviation) 176 | // then do not translate the value using i18n 177 | // otherwise translate the value and set the new node value 178 | // 179 | // NOTE: we must strip the preceeding and succeeding whitespace and line breaks 180 | // and then add them back after the string is successfully translated 181 | // 182 | const [pre, phrase, post] = parsePreAndPostWhitespace( 183 | node.value 184 | ); 185 | node.value = 186 | pre + 187 | this.config.i18n.api.t({ 188 | phrase, 189 | locale 190 | }) + 191 | post; 192 | } 193 | }) 194 | .use(rehypeStringify) 195 | .process(markdown, (err, file) => { 196 | if (err) return reject(err); 197 | resolve({ locale, content: String(file) }); 198 | }); 199 | }); 200 | }) 201 | ); 202 | await Promise.all( 203 | files.map(async (file) => { 204 | const localizedFilePath = this.getLocalizedMarkdownFileName( 205 | filePath, 206 | file.locale 207 | ); 208 | debug('writing file', localizedFilePath); 209 | await writeFile(localizedFilePath, file.content); 210 | }) 211 | ); 212 | } 213 | 214 | async markdown() { 215 | // if title is all uppercase then `-EN` otherwise `-en` 216 | const filePaths = await globby( 217 | this.config.markdown.patterns, 218 | this.config.markdown.options 219 | ); 220 | debug('markdown', filePaths); 221 | await Promise.all( 222 | filePaths.map((filePath) => this.parseMarkdownFile(filePath)) 223 | ); 224 | } 225 | 226 | async translate() { 227 | const { i18n, logger } = this.config; 228 | 229 | const defaultFields = _.zipObject( 230 | _.values(i18n.config.phrases), 231 | _.values(i18n.config.phrases) 232 | ); 233 | 234 | const defaultLocaleFilePath = path.join( 235 | i18n.config.directory, 236 | `${i18n.config.defaultLocale}.json` 237 | ); 238 | 239 | let defaultLocaleFile; 240 | try { 241 | defaultLocaleFile = require(defaultLocaleFilePath); 242 | } catch (err) { 243 | logger.error(err); 244 | defaultLocaleFile = {}; 245 | } 246 | 247 | return pMapSeries(i18n.config.locales, async (locale) => { 248 | debug('locale', locale); 249 | const filePath = path.join(i18n.config.directory, `${locale}.json`); 250 | 251 | // look up the file, and if it does not exist, then 252 | // create it with an empty object 253 | let file; 254 | try { 255 | file = require(filePath); 256 | } catch (err) { 257 | logger.error(err); 258 | file = {}; 259 | } 260 | 261 | // add any missing fields if they don't exist 262 | file = _.defaultsDeep(file, defaultFields); 263 | 264 | // if the locale is not the default 265 | // then check if translations need done 266 | if (locale === i18n.config.defaultLocale) return file; 267 | 268 | const translationsRequired = _.intersection( 269 | _.uniq([ 270 | ..._.values(i18n.config.phrases), 271 | ..._.values(defaultLocaleFile) 272 | ]), 273 | _.values(file) 274 | ); 275 | 276 | if (translationsRequired.length === 0) return file; 277 | 278 | debug('translationsRequired', translationsRequired); 279 | 280 | // attempt to translate all of these in the given language 281 | await pMapSeries(translationsRequired, async (phrase) => { 282 | // 283 | // TODO: note that this will corrupt `` 284 | // so I have turned it off for now until we have a better parser 285 | // 286 | /* 287 | // prevent %s %d and %j from getting translated 288 | // 289 | // 290 | for (const element of formatSpecifiers) { 291 | safePhrase = safePhrase.replace( 292 | new RegExp(element, 'g'), 293 | `${element}` 294 | ); 295 | } 296 | */ 297 | 298 | debug('phrase', phrase); 299 | 300 | // TODO: also prevent {{...}} from getting translated 301 | // by wrapping such with ``? 302 | 303 | // lookup translation result from cache 304 | const key = `${locale}:${revHash(phrase)}`; 305 | let translation; 306 | 307 | // do not translate if it is an email, FQDN, URL, or IP 308 | if (isEmail(phrase) || isFQDN(phrase) || isURL(phrase) || isIP(phrase)) 309 | translation = phrase; 310 | else if (this.redisClient) 311 | translation = await this.redisClient.get(key); 312 | debug('translation', translation); 313 | 314 | // get the translation results from Google 315 | if (!_.isString(translation)) { 316 | debug('getting translation', key); 317 | try { 318 | [translation] = await this.client.translate(phrase, locale); 319 | } catch (err) { 320 | debug('error', err, 'key', key, 'phrase', phrase, 'locale', locale); 321 | } 322 | 323 | if (_.isString(translation)) { 324 | debug('got translation', translation); 325 | if (this.redisClient) await this.redisClient.set(key, translation); 326 | } 327 | } 328 | 329 | // replace `|` pipe character because translation will 330 | // interpret as ranged interval 331 | // 332 | // TODO: maybe use `he` package to re-encode entities? 333 | if (_.isString(translation)) { 334 | file[phrase] = translation.replace(/\|/g, '|'); 335 | 336 | // write the file again 337 | debug('writing filePath', filePath, 'with translation', translation); 338 | await writeFile(filePath, JSON.stringify(file, null, 2)); 339 | } 340 | }); 341 | 342 | return file; 343 | }); 344 | } 345 | } 346 | 347 | Mandarin.parsePreAndPostWhitespace = parsePreAndPostWhitespace; 348 | 349 | Mandarin.DEFAULT_PATTERNS = DEFAULT_PATTERNS; 350 | 351 | module.exports = Mandarin; 352 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mandarin", 3 | "description": "Automatic i18n markdown translation and i18n phrase translation using Google Translate", 4 | "version": "5.0.6", 5 | "author": "Nick Baugh (http://niftylettuce.com/)", 6 | "bugs": { 7 | "url": "https://github.com/ladjs/mandarin/issues", 8 | "email": "niftylettuce@gmail.com" 9 | }, 10 | "commitlint": { 11 | "extends": [ 12 | "@commitlint/config-conventional" 13 | ] 14 | }, 15 | "contributors": [ 16 | "Nick Baugh (http://niftylettuce.com/)" 17 | ], 18 | "dependencies": { 19 | "@cospired/i18n-iso-languages": "^4.0.0", 20 | "@google-cloud/translate": "^7.0.1", 21 | "@ladjs/redis": "^1.0.7", 22 | "@ladjs/shared-config": "^8.0.0", 23 | "debug": "^4.3.4", 24 | "globby": "11", 25 | "is-fqdn": "^2.0.1", 26 | "is-string-and-not-blank": "^0.0.2", 27 | "lodash": "^4.17.21", 28 | "modify-filename": "1", 29 | "p-map-series": "2", 30 | "pify": "5", 31 | "rehype-raw": "5", 32 | "rehype-rewrite": "1", 33 | "rehype-stringify": "8", 34 | "remark-autolink-headings": "6", 35 | "remark-emoji": "2", 36 | "remark-parse": "9", 37 | "remark-preset-github": "^4.0.4", 38 | "remark-rehype": "8", 39 | "remark-slug": "6", 40 | "rev-hash": "3", 41 | "to-vfile": "6", 42 | "unified": "9", 43 | "universalify": "^2.0.0", 44 | "validator": "^13.9.0" 45 | }, 46 | "devDependencies": { 47 | "@commitlint/cli": "^17.1.2", 48 | "@commitlint/config-conventional": "^17.1.0", 49 | "@ladjs/i18n": "^8.0.1", 50 | "ava": "^4.3.3", 51 | "cross-env": "^7.0.3", 52 | "del": "6.1.1", 53 | "delay": "^5.0.0", 54 | "eslint": "^8.23.0", 55 | "eslint-config-xo-lass": "^2.0.1", 56 | "fixpack": "^4.0.0", 57 | "husky": "^8.0.1", 58 | "ioredis": "^5.2.3", 59 | "ioredis-mock": "^8.2.2", 60 | "lint-staged": "^13.0.3", 61 | "nyc": "^15.1.0", 62 | "remark-cli": "^11.0.0", 63 | "xo": "^0.52.2" 64 | }, 65 | "engines": { 66 | "node": ">=14" 67 | }, 68 | "files": [ 69 | "index.js" 70 | ], 71 | "homepage": "https://github.com/ladjs/mandarin", 72 | "keywords": [ 73 | "agenda", 74 | "auto", 75 | "automatic", 76 | "callback", 77 | "connect", 78 | "convert", 79 | "express", 80 | "generate", 81 | "generator", 82 | "google", 83 | "i10n", 84 | "i18n", 85 | "job", 86 | "koa", 87 | "lad", 88 | "lass", 89 | "locale", 90 | "locales", 91 | "localization", 92 | "localize", 93 | "mongoose", 94 | "phrase", 95 | "phrases", 96 | "sentence", 97 | "sentences", 98 | "string", 99 | "strings", 100 | "text", 101 | "translate", 102 | "translater", 103 | "translation", 104 | "word", 105 | "words" 106 | ], 107 | "license": "MIT", 108 | "main": "index.js", 109 | "repository": { 110 | "type": "git", 111 | "url": "https://github.com/ladjs/mandarin" 112 | }, 113 | "scripts": { 114 | "lint": "xo --fix && remark . -qfo && fixpack", 115 | "nyc": "cross-env NODE_ENV=test nyc ava", 116 | "prepare": "husky install", 117 | "pretest": "npm run lint", 118 | "test": "npm run test-coverage", 119 | "test-coverage": "cross-env NODE_ENV=test nyc ava" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const I18N = require('@ladjs/i18n'); 4 | const Redis = require('ioredis-mock'); 5 | const del = require('del'); 6 | const delay = require('delay'); 7 | const test = require('ava'); 8 | 9 | const Mandarin = require('..'); 10 | 11 | test('translates a basic phrase to multiple languages', async (t) => { 12 | await del(path.join(__dirname, '..', 'locales')); 13 | const i18n = new I18N({ autoReload: true }); 14 | const hello = i18n.api.t({ phrase: 'Hello', locale: 'en' }); 15 | t.is(hello, 'Hello'); 16 | const mandarin = new Mandarin({ i18n, redis: new Redis() }); 17 | await mandarin.translate(); 18 | // allow auto-reload to occur 19 | await delay(1000); 20 | t.is(i18n.api.t({ phrase: 'Hello', locale: 'es' }), 'Hola'); 21 | }); 22 | --------------------------------------------------------------------------------