├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── i18n └── pt_BR.json ├── index.js ├── package.json ├── scripts └── test └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | env: 6 | - EXPORT_COVERAGE=1 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Marcos Vinicius Bérgamo 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Joi18nz 2 | 3 | [![Build Status][travis-badge]][travis-url] 4 | [![Coverage Status][coveralls-badge]][coveralls-url] 5 | [![Dependency Status][david-badge]][david-url] 6 | 7 | Internacionlization(i18n) error messages for [Joi](https://github.com/hapijs/joi) 8 | 9 | ### Getting Started 10 | For using Joi18nz is very simple: 11 | ```javascript 12 | 'use strict'; 13 | const Joi = require('joi18nz')(require('joi'), __dirname + '/locale'); 14 | ``` 15 | You must send to joi18nz an intance of Joi and the directory of the translation files. 16 | 17 | After, initializing the module, you have the instance of Joi, and you can use that like you use Joi. 18 | 19 | You can translate your messages, simple adding an extra option in your validate function. 20 | 21 | ```javascript 22 | let schema = { 23 | name: Joi.string().required() 24 | }; 25 | 26 | let value = {}; 27 | 28 | Joi.validate(value, schema, {i18n: 'pt_BR'}, function (err, data) { 29 | console.log(err, data); 30 | }); 31 | // output 32 | /* 33 | { [ValidationError: falha em "name", pois ["name" é obrigatório]] 34 | isJoi: true, 35 | name: 'ValidationError', 36 | details: 37 | [ { message: '"name" é obrigatório', 38 | path: 'name', 39 | type: 'any.required', 40 | context: [Object] } ], 41 | _object: {}, 42 | annotate: [Function] } {} 43 | */ 44 | ``` 45 | 46 | For more information about how translate the tokens, you can see the `pt_BR` translation inside the `i18n` directory in this project. 47 | 48 | **NOTE:** If you specify an invalid path to your folders we just use the default english version for the errors or the translations default in the `i18n` directory in this project. 49 | 50 | After all, you just have a Joi instance, you can use that like you use Joi in your project, no incompatibilities with an existing implementation. 51 | 52 | ### Contribute 53 | 54 | To contribute you can try to find an [issue or enchancment][0] and try to 55 | implement it. Fork the project, implement the code, make tests and send the PR to the master branch. 56 | 57 | ### Testing 58 | 59 | For testing you just need run `npm install && npm test` inside root folder of this project. 60 | 61 | ### License 62 | 63 | Copyright (c) 2016, Marcos Bérgamo 64 | 65 | Permission to use, copy, modify, and/or distribute this software for any purpose 66 | with or without fee is hereby granted, provided that the above copyright notice 67 | and this permission notice appear in all copies. 68 | 69 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 70 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 71 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 72 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 73 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 74 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 75 | THIS SOFTWARE. 76 | 77 | [0]: https://github.com/thebergamo/joi18nz/issues?q=is%3Aopen+is%3Aenchancement+is%3Abug 78 | 79 | [travis-badge]: https://api.travis-ci.org/thebergamo/joi18nz.svg?branch=master 80 | [travis-url]: https://travis-ci.org/thebergamo/joi18nz 81 | [coveralls-badge]:https://coveralls.io/repos/thebergamo/joi18nz/badge.svg?branch=master&service=github 82 | [coveralls-url]: https://coveralls.io/github/thebergamo/joi18nz?branch=master 83 | [david-badge]: https://david-dm.org/thebergamo/joi18nz.svg 84 | [david-url]: https://david-dm.org/thebergamo/joi18nz 85 | 86 | -------------------------------------------------------------------------------- /i18n/pt_BR.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "root": "value", 4 | "key": "\"{{!key}}\" ", 5 | "messages": { 6 | "wrapArrays": true 7 | }, 8 | "any": { 9 | "unknown": "não é permitido", 10 | "invalid": "contém um valor inválido", 11 | "empty": "não é permitido ser vazio", 12 | "required": "é obrigatório", 13 | "allowOnly": "precisa ser um dos seguintes valores: {{valids}}", 14 | "default": "ocorreu um erro ao executar o método padrão" 15 | }, 16 | "alternatives": { 17 | "base": "não corresponde a nenhuma altenativa válida" 18 | }, 19 | "array": { 20 | "base": "deve ser uma matriz", 21 | "includes": "na posição {{pos}} não corresponde a nenhuma tipo válido", 22 | "includesSingle": "valor único de \"{{!key}}\" não corresponde a nenhuma tipo válido", 23 | "includesOne": "na posição {{pos}} falha, pois {{reason}}", 24 | "includesOneSingle": "valor único de \"{{!key}}\" falha, pois {{reason}}", 25 | "includesRequiredUnknowns": "não contém {{unknownMisses}} valor(es) obrigatório(s)", 26 | "includesRequiredKnowns": "não contém {{knownMisses}}", 27 | "includesRequiredBoth": "não contém {{knownMisses}} e {{unknownMisses}} outros valor(es) obrigatório(s)", 28 | "excludes": "na posição {{pos}} contém um valor excluído", 29 | "excludesSingle": "valor único de \"{{!key}}\" contém um valor excluído", 30 | "min": "deve conter no mínimo {{limit}} itens", 31 | "max": "deve conter menos que ou igual a {{limit}} itens", 32 | "length": "deve conter {{limit}} itens", 33 | "ordered": "na posição {{pos}} falha, pois {{reason}}", 34 | "orderedLength": "na posição {{pos}} falha, pois a matriz deve conter até {{limit}} itens", 35 | "sparse": "não pode ser uma matriz esparsa", 36 | "unique": "na posição {{pos}} há um valor duplicado" 37 | }, 38 | "boolean": { 39 | "base": "deve ser boleano" 40 | }, 41 | "binary": { 42 | "base": "deve ser um buffer ou uma string", 43 | "min": "deve conter no mínimo {{limit}} bytes", 44 | "max": "deve conter no máximo {{limit}} bytes", 45 | "length": "deve conter {{limit}} bytes" 46 | }, 47 | "date": { 48 | "base": "deve ser um número em milisegundos ou uma data válida", 49 | "min": "deve ser maior ou igual a \"{{limit}}\"", 50 | "max": "deve ser menor ou igual a \"{{limit}}\"", 51 | "isoDate": "deve ser uma data no padrão ISO 8601", 52 | "ref": "referencia a \"{{ref}}\" que não é uma data" 53 | }, 54 | "function": { 55 | "base": "deve ser uma função" 56 | }, 57 | "object": { 58 | "base": "deve ser um objeto", 59 | "child": "falha em \"{{!key}}\", pois {{reason}}", 60 | "min": "deve conter no mínimo {{limit}} filhos", 61 | "max": "deve conter no máximo {{limit}} filhos", 62 | "length": "deve conter {{limit}} filhos", 63 | "allowUnknown": "não é permitido", 64 | "with": "peer obrigatório não encontrado: \"{{peer}}\"", 65 | "without": "peer conflitante ou proibido: \"{{peer}}\"", 66 | "missing": "deve conter ao menos {{peers}}", 67 | "xor": "contém um conflito entre peers exclusivos {{peers}}", 68 | "or": "deve conter algum dos peers {{peers}}", 69 | "and": "contém {{present}} sem seus peers obrigatórios {{missing}}", 70 | "nand": "!!\"{{main}}\" não deve existir juntamente com {{peers}}", 71 | "assert": "!!\"{{ref}}\" validação falha, pois \"{{ref}}\" falhou em {{message}}", 72 | "rename": { 73 | "multiple": "não pôde renomear \"{{from}}\" pois muitas renomeaçãoes estão desativadas e já havia sido renomeado para \"{{to}}\"", 74 | "override": "não pôde renomear \"{{from}}\" pois sobrescrever está desabilitado e \"{{to}}\" já existe" 75 | }, 76 | "type": "deve ser um instância de \"{{type}}\"" 77 | }, 78 | "number": { 79 | "base": "deve ser um número", 80 | "min": "deve ser maior ou igual a {{limit}}", 81 | "max": "deve ser menor ou igual a {{limit}}", 82 | "less": "deve ser menor que {{limit}}", 83 | "greater": "deve ser maior que {{limit}}", 84 | "float": "deve ser um número decimal", 85 | "integer": "deve ser um número inteiro", 86 | "negative": "deve ser um número negativo", 87 | "positive": "deve ser um número positivo", 88 | "precision": "não pode conter mais que {{limit}} casas decimais", 89 | "ref": "referencia a \"{{ref}}\" na qual não é um número", 90 | "multiple": "deve ser múltiplo de {{multiple}}" 91 | }, 92 | "string": { 93 | "base": "deve ser um texto", 94 | "min": "deve conter no mínimo {{limit}} caracteres", 95 | "max": "deve conter no máximo {{limit}} caracteres", 96 | "length": "deve conter {{limit}} caracteres", 97 | "alphanum": "deve conter apenas caracteres alpha-numéricos", 98 | "token": "deve conter apenas caracteres alpha-numéricos ou underscore '_'", 99 | "regex": { 100 | "base": "com o valor \"{{!value}}\" falha ao comparar ao padrão: {{pattern}}", 101 | "name": "com o valor \"{{!value}}\" falha ao comparar ao padrão: {{name}}" 102 | }, 103 | "email": "deve ser um email válido", 104 | "uri": "dever ser uma URI válida", 105 | "uriCustomScheme": "dever ser uma URI válida que se assemelhe ao padrão {{scheme}}", 106 | "isoDate": "deve ser uma data no padrão ISO 8601", 107 | "guid": "deve ser um GUID válido", 108 | "hex": "deve conter apenas valores hexadecimais", 109 | "hostname": "deve conter apenas hostname válido", 110 | "lowercase": "deve conter apenas caracteres em caixa baixa", 111 | "uppercase": "deve conter apenas caracteres em caixa alta", 112 | "trim": "não deve haver espaços em branco", 113 | "creditCard": "deve ser um cartão de crédito", 114 | "ref": "referencia a \"{{ref}}\" que não é um número", 115 | "ip": "deve ser um IP válido com o CIDR {{cidr}}", 116 | "ipVersion": "deve ser um IP válido nas versões {{version}} com o CIDR {{cidr}}" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const Path = require('path'); 5 | 6 | const internals = {}; 7 | 8 | internals.i18n = function (Joi, i18nDir) { 9 | const directory = i18nDir || ''; 10 | if (!Joi) { 11 | throw new TypeError('Joi is required'); 12 | } 13 | 14 | const originalFn = Joi.validate; 15 | Joi.validate = function (value /*, [schema], [options], callback */) { 16 | const last = arguments[arguments.length - 1]; 17 | const callback = typeof last === 'function' ? last : null; 18 | 19 | const count = arguments.length - (callback ? 1 : 0); 20 | if (count === 1) { 21 | return originalFn(value, callback); 22 | } 23 | 24 | const options = count === 3 ? arguments[2] : {}; 25 | 26 | if (options.i18n) { 27 | options.language = internals.getI18nFile(directory, options.i18n); 28 | delete options.i18n; 29 | } 30 | 31 | const schema = arguments[1]; 32 | 33 | return originalFn(value, schema, options, callback); 34 | }; 35 | 36 | return Joi; 37 | }; 38 | 39 | internals.getI18nFile = function (i18nDir, fileName) { 40 | try { 41 | const root = Path.join(i18nDir, fileName + '.json'); 42 | const isFile = fs.statSync(root).isFile(); 43 | return isFile ? require(root) : {}; 44 | } catch (err) { 45 | console.error(err); 46 | 47 | return {}; 48 | } 49 | }; 50 | 51 | module.exports = internals.i18n; 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joi18nz", 3 | "version": "1.0.0", 4 | "description": "Internacionlization(i18n) error messages for Joi", 5 | "repository": "https://github.com/thebergamo/joi18nz", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "scripts/test" 9 | }, 10 | "keywords": [ 11 | "joi", 12 | "i18n", 13 | "translation" 14 | ], 15 | "author": "Marcos Bérgamo ", 16 | "license": "ISC", 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "code": "~2.1.0", 20 | "coveralls": "~2.11.6", 21 | "joi": "~7.2.3", 22 | "lab": "~8.2.0", 23 | "semistandard": "~7.0.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/test: Run test suite for application. Optionally pass in a path to an 4 | # individual test file to run a single test. 5 | 6 | 7 | set -e 8 | 9 | cd "$(dirname "$0")/.." 10 | 11 | [ -z "$DEBUG" ] || set -x 12 | 13 | echo "===> Running linter..." 14 | 15 | ./node_modules/semistandard/bin/cmd.js 16 | 17 | echo "===> Running tests..." 18 | 19 | if [ ! -z "$EXPORT_COVERAGE" ]; then 20 | mkdir -p coverage 21 | 22 | ./node_modules/.bin/lab -c -l -t 95 -v -r lcov > ./coverage/lab.lcov 23 | 24 | cat ./coverage/lab.lcov | ./node_modules/coveralls/bin/coveralls.js 25 | 26 | rm -rf ./coverage 27 | else 28 | ./node_modules/.bin/lab -c -l -t 95 -v 29 | fi 30 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Lab = require('lab'); 4 | const lab = exports.lab = Lab.script(); 5 | const expect = require('code').expect; 6 | const joi = require('joi'); 7 | 8 | lab.experiment('Initialize Joi18nz', () => { 9 | lab.test('throws an error if no Joi instance sended in constructor', (done) => { 10 | expect(require('../index')).to.throw(TypeError, 'Joi is required'); 11 | return done(); 12 | }); 13 | 14 | lab.test('return a Joi instance if an instance of Joi is sended', (done) => { 15 | expect(require('../index')(joi)).to.include({isJoi: true}); 16 | return done(); 17 | }); 18 | }); 19 | 20 | lab.experiment('Validate Schemas', () => { 21 | let Joi = require('../index')(joi); 22 | const schema = { 23 | name: Joi.string().required(), 24 | isPlayer: Joi.boolean().optional() 25 | }; 26 | 27 | let value = { 28 | name: 'Marcos', 29 | isPlayer: true 30 | }; 31 | 32 | lab.test('when no i18n option is sended (default Joi behavior)', (done) => { 33 | Joi.validate(value, schema, (err, data) => { 34 | expect(err).to.be.null(); 35 | expect(data).to.include(value); 36 | return done(); 37 | }); 38 | }); 39 | 40 | lab.test('when no i18n option is sended (default Joi behavior sync version)', (done) => { 41 | let result = Joi.validate(value, schema); 42 | expect(result.error).to.be.null(); 43 | expect(result.value).to.include(value); 44 | return done(); 45 | }); 46 | 47 | lab.test('when no i18n option is send, and just the value is send (default Joi behavior)', (done) => { 48 | Joi.validate(value, (err, data) => { 49 | expect(err).to.be.null(); 50 | expect(data).to.include(value); 51 | return done(); 52 | }); 53 | }); 54 | 55 | lab.test('when a i18n options is sended, but no file match `file`', (done) => { 56 | Joi.validate({}, schema, {i18n: 'pt_BR'}, (err, data) => { 57 | expect(err).to.include({isJoi: true, name: 'ValidationError'}); 58 | expect(err.details).to.deep.include([{message: '"name" is required'}]); 59 | return done(); 60 | }); 61 | }); 62 | 63 | lab.test('when a i18n option is sended and a correct directory haven`t the translation `file`', (done) => { 64 | Joi = require('../index')(joi, __dirname + '/../i18n'); 65 | Joi.validate({}, schema, {i18n: 'es'}, (err, data) => { 66 | expect(err).to.include({isJoi: true, name: 'ValidationError'}); 67 | expect(err.details).to.deep.include([{message: '"name" is required'}]); 68 | return done(); 69 | }); 70 | }); 71 | 72 | lab.test('when a i18n option is sended and a correct directory have the translation `file`', (done) => { 73 | Joi = require('../index')(joi, __dirname + '/../i18n'); 74 | Joi.validate({}, schema, {i18n: 'pt_BR'}, (err, data) => { 75 | expect(err).to.include({isJoi: true, name: 'ValidationError'}); 76 | expect(err.details).to.deep.include([{message: '"name" é obrigatório'}]); 77 | return done(); 78 | }); 79 | }); 80 | }); 81 | --------------------------------------------------------------------------------