This commit is contained in:
Candy 2016-06-07 10:48:28 -04:00
parent 52f1645713
commit 3767940472
20 changed files with 336 additions and 165 deletions

View File

@ -3,6 +3,8 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
module.exports = function(loopback) { module.exports = function(loopback) {
loopback.getCurrentContext = function() { loopback.getCurrentContext = function() {
return null; return null;
@ -10,6 +12,6 @@ module.exports = function(loopback) {
loopback.runInContext = loopback.runInContext =
loopback.createContext = function() { loopback.createContext = function() {
throw new Error('Current context is not supported in the browser.'); throw new Error(g.f('Current context is not supported in the browser.'));
}; };
}; };

View File

@ -7,6 +7,8 @@
* Module Dependencies. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var assert = require('assert'); var assert = require('assert');
var uid = require('uid2'); var uid = require('uid2');
@ -112,7 +114,7 @@ module.exports = function(AccessToken) {
} else if (isValid) { } else if (isValid) {
cb(null, token); cb(null, token);
} else { } else {
var e = new Error('Invalid Access Token'); var e = new Error(g.f('Invalid Access Token'));
e.status = e.statusCode = 401; e.status = e.statusCode = 401;
e.code = 'INVALID_TOKEN'; e.code = 'INVALID_TOKEN';
cb(e); cb(e);

View File

@ -36,6 +36,8 @@
*/ */
var g = require('strong-globalize')();
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var async = require('async'); var async = require('async');
var assert = require('assert'); var assert = require('assert');
@ -535,7 +537,7 @@ module.exports = function(ACL) {
break; break;
default: default:
process.nextTick(function() { process.nextTick(function() {
var err = new Error('Invalid principal type: ' + type); var err = new Error(g.f('Invalid principal type: %s', type));
err.statusCode = 400; err.statusCode = 400;
cb(err); cb(err);
}); });

View File

@ -7,6 +7,8 @@
* Module Dependencies. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var PersistedModel = require('../../lib/loopback').PersistedModel; var PersistedModel = require('../../lib/loopback').PersistedModel;
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var utils = require('../../lib/utils'); var utils = require('../../lib/utils');
@ -112,7 +114,7 @@ module.exports = function(Change) {
}) })
.join('\n'); .join('\n');
var msg = 'Cannot rectify ' + modelName + ' changes:\n' + desc; var msg = g.f('Cannot rectify %s changes:\n%s', modelName, desc);
err = new Error(msg); err = new Error(msg);
err.details = { errors: errors }; err.details = { errors: errors };
return callback(err); return callback(err);

View File

@ -15,6 +15,8 @@
* @inherits {Model} * @inherits {Model}
*/ */
var g = require('strong-globalize')();
module.exports = function(Email) { module.exports = function(Email) {
/** /**
@ -44,13 +46,13 @@ module.exports = function(Email) {
*/ */
Email.send = function() { Email.send = function() {
throw new Error('You must connect the Email Model to a Mail connector'); throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector'));
}; };
/** /**
* A shortcut for Email.send(this). * A shortcut for Email.send(this).
*/ */
Email.prototype.send = function() { Email.prototype.send = function() {
throw new Error('You must connect the Email Model to a Mail connector'); throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector'));
}; };
}; };

View File

@ -7,6 +7,8 @@
* Module Dependencies. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var loopback = require('../../lib/loopback'); var loopback = require('../../lib/loopback');
var utils = require('../../lib/utils'); var utils = require('../../lib/utils');
var path = require('path'); var path = require('path');
@ -205,14 +207,14 @@ module.exports = function(User) {
realmDelimiter); realmDelimiter);
if (realmRequired && !query.realm) { if (realmRequired && !query.realm) {
var err1 = new Error('realm is required'); var err1 = new Error(g.f('{{realm}} is required'));
err1.statusCode = 400; err1.statusCode = 400;
err1.code = 'REALM_REQUIRED'; err1.code = 'REALM_REQUIRED';
fn(err1); fn(err1);
return fn.promise; return fn.promise;
} }
if (!query.email && !query.username) { if (!query.email && !query.username) {
var err2 = new Error('username or email is required'); var err2 = new Error(g.f('{{username}} or {{email}} is required'));
err2.statusCode = 400; err2.statusCode = 400;
err2.code = 'USERNAME_EMAIL_REQUIRED'; err2.code = 'USERNAME_EMAIL_REQUIRED';
fn(err2); fn(err2);
@ -220,7 +222,7 @@ module.exports = function(User) {
} }
self.findOne({where: query}, function(err, user) { self.findOne({where: query}, function(err, user) {
var defaultError = new Error('login failed'); var defaultError = new Error(g.f('login failed'));
defaultError.statusCode = 401; defaultError.statusCode = 401;
defaultError.code = 'LOGIN_FAILED'; defaultError.code = 'LOGIN_FAILED';
@ -250,7 +252,7 @@ module.exports = function(User) {
if (self.settings.emailVerificationRequired && !user.emailVerified) { if (self.settings.emailVerificationRequired && !user.emailVerified) {
// Fail to log in if email verification is not done yet // Fail to log in if email verification is not done yet
debug('User email has not been verified'); debug('User email has not been verified');
err = new Error('login failed as the email has not been verified'); err = new Error(g.f('login failed as the email has not been verified'));
err.statusCode = 401; err.statusCode = 401;
err.code = 'LOGIN_FAILED_EMAIL_NOT_VERIFIED'; err.code = 'LOGIN_FAILED_EMAIL_NOT_VERIFIED';
fn(err); fn(err);
@ -296,7 +298,7 @@ module.exports = function(User) {
} else if (accessToken) { } else if (accessToken) {
accessToken.destroy(fn); accessToken.destroy(fn);
} else { } else {
fn(new Error('could not find accessToken')); fn(new Error(g.f('could not find {{accessToken}}')));
} }
}); });
return fn.promise; return fn.promise;
@ -438,15 +440,21 @@ module.exports = function(User) {
options.text = options.text.replace(/\{href\}/g, options.verifyHref); options.text = options.text.replace(/\{href\}/g, options.verifyHref);
options.text = g.f(options.text);
options.to = options.to || user.email; options.to = options.to || user.email;
options.subject = options.subject || 'Thanks for Registering'; options.subject = options.subject || 'Thanks for Registering';
options.subject = g.f(options.subject);
options.headers = options.headers || {}; options.headers = options.headers || {};
var template = loopback.template(options.template); var template = loopback.template(options.template);
options.html = template(options); options.html = template(options);
options.html = g.f(options.html);
Email.send(options, function(err, email) { Email.send(options, function(err, email) {
if (err) { if (err) {
fn(err); fn(err);
@ -501,11 +509,11 @@ module.exports = function(User) {
}); });
} else { } else {
if (user) { if (user) {
err = new Error('Invalid token: ' + token); err = new Error(g.f('Invalid token: %s', token));
err.statusCode = 400; err.statusCode = 400;
err.code = 'INVALID_TOKEN'; err.code = 'INVALID_TOKEN';
} else { } else {
err = new Error('User not found: ' + uid); err = new Error(g.f('User not found: %s', uid));
err.statusCode = 404; err.statusCode = 404;
err.code = 'USER_NOT_FOUND'; err.code = 'USER_NOT_FOUND';
} }
@ -533,7 +541,7 @@ module.exports = function(User) {
options = options || {}; options = options || {};
if (typeof options.email !== 'string') { if (typeof options.email !== 'string') {
var err = new Error('Email is required'); var err = new Error(g.f('Email is required'));
err.statusCode = 400; err.statusCode = 400;
err.code = 'EMAIL_REQUIRED'; err.code = 'EMAIL_REQUIRED';
cb(err); cb(err);
@ -545,7 +553,7 @@ module.exports = function(User) {
return cb(err); return cb(err);
} }
if (!user) { if (!user) {
err = new Error('Email not found'); err = new Error(g.f('Email not found'));
err.statusCode = 404; err.statusCode = 404;
err.code = 'EMAIL_NOT_FOUND'; err.code = 'EMAIL_NOT_FOUND';
return cb(err); return cb(err);
@ -581,7 +589,7 @@ module.exports = function(User) {
if (typeof plain === 'string' && plain) { if (typeof plain === 'string' && plain) {
return true; return true;
} }
var err = new Error('Invalid password: ' + plain); var err = new Error(g.f('Invalid password: %s', plain));
err.statusCode = 422; err.statusCode = 422;
throw err; throw err;
}; };
@ -640,20 +648,21 @@ module.exports = function(User) {
UserModel.remoteMethod( UserModel.remoteMethod(
'login', 'login',
{ {
description: 'Login a user with username/email and password.', description: g.f('Login a user with username/email and password.'),
accepts: [ accepts: [
{arg: 'credentials', type: 'object', required: true, http: {source: 'body'}}, {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}},
{arg: 'include', type: ['string'], http: {source: 'query' }, {arg: 'include', type: ['string'], http: {source: 'query'},
description: 'Related objects to include in the response. ' + description: g.f('Related objects to include in the response. ' +
'See the description of return value for more details.'} 'See the description of return value for more details.') },
], ],
returns: { returns: {
arg: 'accessToken', type: 'object', root: true, arg: 'accessToken', type: 'object', root: true,
description: description:
'The response body contains properties of the AccessToken created on login.\n' + g.f('The response body contains properties of the {{AccessToken}} created on login.\n' +
'Depending on the value of `include` parameter, the body may contain ' + 'Depending on the value of `include` parameter, the body may contain ' +
'additional properties:\n\n' + 'additional properties:\n\n' +
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n' ' - `user` - `U+007BUserU+007D` - Data of the currently logged in user. ' +
'{{(`include=user`)}}\n\n'),
}, },
http: {verb: 'post'} http: {verb: 'post'}
} }
@ -662,17 +671,17 @@ module.exports = function(User) {
UserModel.remoteMethod( UserModel.remoteMethod(
'logout', 'logout',
{ {
description: 'Logout a user with access token.', description: g.f('Logout a user with access token.'),
accepts: [ accepts: [
{arg: 'access_token', type: 'string', required: true, http: function(ctx) { {arg: 'access_token', type: 'string', required: true, http: function(ctx) {
var req = ctx && ctx.req; var req = ctx && ctx.req;
var accessToken = req && req.accessToken; var accessToken = req && req.accessToken;
var tokenID = accessToken && accessToken.id; var tokenID = accessToken && accessToken.id;
return tokenID;
return tokenID; }, description: g.f('Do not supply this argument, it is automatically extracted ' +
}, description: 'Do not supply this argument, it is automatically extracted ' + 'from request headers.'),
'from request headers.' },
}], ],
http: {verb: 'all'} http: {verb: 'all'}
} }
); );
@ -680,7 +689,7 @@ module.exports = function(User) {
UserModel.remoteMethod( UserModel.remoteMethod(
'confirm', 'confirm',
{ {
description: 'Confirm a user registration with email verification token.', description: g.f('Confirm a user registration with email verification token.'),
accepts: [ accepts: [
{arg: 'uid', type: 'string', required: true}, {arg: 'uid', type: 'string', required: true},
{arg: 'token', type: 'string', required: true}, {arg: 'token', type: 'string', required: true},
@ -693,7 +702,7 @@ module.exports = function(User) {
UserModel.remoteMethod( UserModel.remoteMethod(
'resetPassword', 'resetPassword',
{ {
description: 'Reset password for a user with email.', description: g.f('Reset password for a user with email.'),
accepts: [ accepts: [
{arg: 'options', type: 'object', required: true, http: {source: 'body'}} {arg: 'options', type: 'object', required: true, http: {source: 'body'}}
], ],
@ -704,7 +713,7 @@ module.exports = function(User) {
UserModel.afterRemote('confirm', function(ctx, inst, next) { UserModel.afterRemote('confirm', function(ctx, inst, next) {
if (ctx.args.redirect !== undefined) { if (ctx.args.redirect !== undefined) {
if (!ctx.res) { if (!ctx.res) {
return next(new Error('The transport does not support HTTP redirects.')); return next(new Error(g.f('The transport does not support HTTP redirects.')));
} }
ctx.res.location(ctx.args.redirect); ctx.res.location(ctx.args.redirect);
ctx.res.status(302); ctx.res.status(302);
@ -722,7 +731,7 @@ module.exports = function(User) {
// email validation regex // email validation regex
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); UserModel.validatesFormatOf('email', {with: re, message: g.f('Must provide a valid email')});
// FIXME: We need to add support for uniqueness of composite keys in juggler // FIXME: We need to add support for uniqueness of composite keys in juggler
if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) { if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) {

View File

@ -3,6 +3,8 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
var loopback = require('../../'); var loopback = require('../../');
var client = loopback(); var client = loopback();
var CartItem = require('./models').CartItem; var CartItem = require('./models').CartItem;
@ -16,10 +18,10 @@ CartItem.attachTo(remote);
// call the remote method // call the remote method
CartItem.sum(1, function(err, total) { CartItem.sum(1, function(err, total) {
console.log('result:', err || total); g.log('result:%s', err || total);
}); });
// call a built in remote method // call a built in remote method
CartItem.find(function(err, items) { CartItem.find(function(err, items) {
console.log(items); g.log(items);
}); });

View File

@ -3,6 +3,8 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
var loopback = require('../../'); var loopback = require('../../');
var app = loopback(); var app = loopback();
@ -22,4 +24,4 @@ Color.create({name: 'blue'});
app.listen(3000); app.listen(3000);
console.log('a list of colors is available at http://localhost:3000/colors'); g.log('a list of colors is available at {{http://localhost:3000/colors}}');

View File

@ -3,6 +3,8 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
var loopback = require('../../'); var loopback = require('../../');
var app = loopback(); var app = loopback();
@ -22,7 +24,7 @@ var Color = loopback.createModel('color', { 'name': String });
Color.beforeRemote('**', function (ctx, unused, next) { Color.beforeRemote('**', function (ctx, unused, next) {
// Inside LoopBack code, you can read the property from the context // Inside LoopBack code, you can read the property from the context
var ns = loopback.getCurrentContext(); var ns = loopback.getCurrentContext();
console.log('Request to host', ns && ns.get('host')); g.log('Request to host %s', ns && ns.get('host'));
next(); next();
}); });
@ -30,5 +32,5 @@ app.dataSource('db', { connector: 'memory' });
app.model(Color, { dataSource: 'db' }); app.model(Color, { dataSource: 'db' });
app.listen(3000, function() { app.listen(3000, function() {
console.log('A list of colors is available at http://localhost:3000/colors'); g.log('A list of colors is available at {{http://localhost:3000/colors}}');
}); });

View File

@ -3,6 +3,8 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
var models = require('../../lib/models'); var models = require('../../lib/models');
var loopback = require('../../'); var loopback = require('../../');
@ -37,14 +39,15 @@ var data = {pushSettings: [
]} ]}
Application.create(data, function(err, data) { Application.create(data, function(err, data) {
console.log('Created: ', data.toObject()); g.log('Created: %s', data.toObject());
}); });
Application.register('rfeng', 'MyApp', {description: 'My first mobile application'}, function (err, result) { Application.register('rfeng', 'MyApp', { description: g.f('My first mobile application') },
function(err, result) {
console.log(result.toObject());
result.resetKeys(function(err, result) {
console.log(result.toObject()); console.log(result.toObject());
});
result.resetKeys(function (err, result) {
console.log(result.toObject());
});
}); });

View File

@ -3,6 +3,8 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
var loopback = require('../../'); var loopback = require('../../');
var app = loopback(); var app = loopback();
@ -25,4 +27,4 @@ Color.all(function () {
app.listen(3000); app.listen(3000);
console.log('a list of colors is available at http://localhost:3000/colors'); g.log('a list of colors is available at {{http://localhost:3000/colors}}');

View File

@ -3,6 +3,9 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var SG = require('strong-globalize');
SG.SetRootDir(__dirname, {autonomousMsgLoading: 'all'});
/** /**
* loopback ~ public api * loopback ~ public api
*/ */

115
intl/en/messages.json Normal file
View File

@ -0,0 +1,115 @@
{
"3b46d3a780fd6ae5f95ade489a0efffe": "Current context is not supported in the browser.",
"7e0fca41d098607e1c9aa353c67e0fa1": "Invalid Access Token",
"320c482401afa1207c04343ab162e803": "Invalid principal type: {0}",
"c2b5d51f007178170ca3952d59640ca4": "Cannot rectify {0} changes:\n{1}",
"5858e63efaa0e4ad86b61c0459ea32fa": "You must connect the {{Email}} Model to a {{Mail}} connector",
"0caffe1d763c8cca6a61814abe33b776": "Email is required",
"1b2a6076dccbe91a56f1672eb3b8598c": "The response body contains properties of the {{AccessToken}} created on login.\nDepending on the value of `include` parameter, the body may contain additional properties:\n\n - `user` - `U+007BUserU+007D` - Data of the currently logged in user. {{(`include=user`)}}\n\n",
"2362ba55796c733e337af169922327a2": "Related objects to include in the response. See the description of return value for more details.",
"306999d39387d87b2638199ff0bed8ad": "Reset password for a user with email.",
"3aae63bb7e8e046641767571c1591441": "login failed as the email has not been verified",
"3caaa84fc103d6d5612173ae6d43b245": "Invalid token: {0}",
"42e3fa18945255412ebc6561e2c6f1dc": "Confirm a user registration with email verification token.",
"430b6326d7ebf6600a39f614ef516bc8": "Do not supply this argument, it is automatically extracted from request headers.",
"44a6c8b1ded4ed653d19ddeaaf89a606": "Email not found",
"5e81ad3847a290dc650b47618b9cbc7e": "login failed",
"83cfabbe3aa84ce52e0f5ed7c3b2e9b3": "Login a user with username/email and password.",
"8608c28f5e6df0008266e3c497836176": "Logout a user with access token.",
"860d1a0b8bd340411fb32baa72867989": "The transport does not support HTTP redirects.",
"895b1f941d026870b3cc8e6af087c197": "{{username}} or {{email}} is required",
"a50d10fc6e0959b220e085454c40381e": "User not found: {0}",
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is required",
"c34fa20eea0091747fcc9eda204b8d37": "could not find {{accessToken}}",
"c68a93f0a9524fed4ff64372fc90c55f": "Must provide a valid email",
"f58cdc481540cd1f69a4aa4da2e37981": "Invalid password: {0}",
"f0bd73df8714cefb925e3b8da2f4c5f6": "result:{0}",
"10e01c895dc0b2fecc385f9f462f1ca6": "a list of colors is available at {{http://localhost:3000/colors}}",
"8a27e0c9ce3ebf0e0c3978efb456e13e": "Request to host {0}",
"a40684f5a9f546115258b76938d1de37": "A list of colors is available at {{http://localhost:3000/colors}}",
"1e85f822b547a75d7d385048030e4ecb": "Created: {0}",
"7d5e7ed0efaedf3f55f380caae0df8b8": "My first mobile application",
"04bd8af876f001ceaf443aad6a9002f9": "Authentication requires model {0} to be defined.",
"095afbf2f1f0e5be678f5dac5c54e717": "Access Denied",
"2d3071e3b18681c80a090dc0efbdb349": "could not find {0} with id {1}",
"316e5b82c203cf3de31a449ee07d0650": "Expected boolean, got {0}",
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Cannot create data source {0}: {1}",
"7e287fc885d9fdcf42da3a12f38572c1": "Authorization Required",
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead",
"1d7833c3ca2f05fdad8fad7537531c40": "\t SUBJECT:{0}",
"275f22ab95671f095640ca99194b7635": "\t FROM:{0}",
"3d63008ccfb2af1db2142e8cc2716ace": "Warning: No email transport specified for sending email. Setup a transport to send mail messages.",
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Sending Mail:",
"63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}",
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
"ecb06666ef95e5db27a5ac1d6a17923b": "\t TO:{0}",
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT:{0}",
"0da38687fed24275c1547e815914a8e3": "Delete a related item by id for {0}.",
"0e4f89f8dde1e88cbfc6c1d88f0f77cb": "Criteria to match model instances",
"22fe62fa8d595b72c62208beddaa2a56": "Update a related item by id for {0}.",
"528325f3cbf1b0ab9a08447515daac9a": "Update {0} of this model.",
"543d19bad5e47ee1e9eb8af688e857b4": "Foreign key for {0}.",
"598ff0255ffd1d1b71e8de55dbe2c034": "Check the existence of {0} relation to an item by id.",
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Remove the {0} relation to an item by id.",
"5fa3afb425819ebde958043e598cb664": "could not find a model with {{id}} {0}",
"61e5deebaf44d68f4e6a508f30cc31a3": "Relation `{0}` does not exist for model `{1}`",
"651f0b3cbba001635152ec3d3d954d0a": "Find a related item by id for {0}.",
"7bc7b301ad9c4fc873029d57fb9740fe": "Queries {0} of {1}.",
"7c837b88fd0e509bd3fc722d7ddf0711": "Foreign key for {0}",
"830cb6c862f8f364e9064cea0026f701": "Fetches hasOne relation {0}.",
"855ecd4a64885ba272d782435f72a4d4": "Unknown \"{0}\" id \"{1}\".",
"86254879d01a60826a851066987703f2": "Add a related item by id for {0}.",
"8ae418c605b6a45f2651be9b1677c180": "Invalid remote method: `{0}`",
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Fetches belongsTo relation {0}.",
"c0057a569ff9d3b509bac61a4b2f605d": "Deletes all {0} of this model.",
"cd0412f2f33a4a2a316acc834f3f21a6": "must specify an {{id}} or {{data}}",
"d6f43b266533b04d442bdb3955622592": "Creates a new instance in {0} of this model.",
"da13d3cdf21330557254670dddd8c5c7": "Counts {0} of {1}.",
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "Unknown \"{0}\" {{id}} \"{1}\".",
"f66ae3cf379b2fce28575a3282defe1a": "Deletes {0} of this model.",
"03f79fa268fe199de2ce4345515431c1": "No change record found for {0} with id {1}",
"0f1c71f74b040bfbe8d384a414e31f03": "Get a set of deltas and conflicts since the given checkpoint.",
"15254dec061d023d6c030083a0cef50f": "Create a new instance of the model and persist it into the data source.",
"16a11368d55b85a209fc6aea69ee5f7a": "Delete all matching records.",
"1bc1d489ddf347af47af3d9b1fc7cc15": "Run multiple updates at once. Note: this is not atomic.",
"1bc7d8283c9abda512692925c8d8e3c0": "Get the current checkpoint.",
"1caa7cc61266e7aef7db7d2f0e27ac3e": "Update the properties of the most recent change record kept for this instance.",
"2a7df74fe6e8462e617b79d5fbb536ea": "Get the most recent change record for this instance.",
"2a9684b3d5b3b67af74bac74eb1b0843": "Find all instances of the model matched by filter from the data source.",
"2e50838caf0c927735eb15d12866bdd7": "Get the changes to a model since a given checkpoint.Provide a filter object to reduce the number of results returned.",
"4203ab415ec66a78d3164345439ba76e": "Cannot call {0}.{1}(). The {2} method has not been setup. The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!",
"51ea9b6245bb5e672b236d640ca3b048": "An object of Change property name/value pairs",
"55ddedd4c501348f82cb89db02ec85c1": "An object of model property name/value pairs",
"5aaa76c72ae1689fd3cf62589784a4ba": "Update attributes for a model instance and persist it into the data source.",
"5f659bbc15e6e2b249fa33b3879b5f69": "Find a model instance by {{id}} from the data source.",
"62e8b0a733417978bab22c8dacf5d7e6": "Cannot apply bulk updates, the connector does not correctly report the number of updated records.",
"6329e0ac1de3c250ebb1df5afd5a2a7b": "The number of instances updated",
"6bc376432cd9972cf991aad3de371e78": "Missing data for change: {0}",
"79295ac04822d2e9702f0dd1d0240336": "Update instances of the model matched by {{where}} from the data source.",
"7f2fde7f0f860ead224b11ba8d75aa1c": "Create an update list from a delta list.",
"89b57e764c2267129294b07589dbfdc2": "Delete a model instance by {{id}} from the data source.",
"8bab6720ecc58ec6412358c858a53484": "Bulk update failed, the connector has modified unexpected number of records: {0}",
"a98b6cc6547706b5c6bffde0ed5fd55c": "Find first instance of the model matched by filter from the data source.",
"c46d4aba1f14809c16730faa46933495": "Filter defining fields and include",
"c65600640f206f585d300b4bcb699d95": "Create a checkpoint.",
"cf64c7afc74d3a8120abcd028f98c770": "Update an existing model instance or insert a new one into the data source.",
"dcb6261868ff0a7b928aa215b07d068c": "Create a change stream.",
"e43e320a435ec1fa07648c1da0d558a7": "Check whether a model instance exists in the data source.",
"e92aa25b6b864e3454b65a7c422bd114": "Bulk update failed, the connector has deleted unexpected number of records: {0}",
"ea63d226b6968e328bdf6876010786b5": "Cannot apply bulk updates, the connector does not correctly report the number of deleted records.",
"f1d4ac54357cc0932f385d56814ba7e4": "Conflict",
"f37d94653793e33f4203e45b4a1cf106": "Count instances of the model matched by where from the data source.",
"0731d0109e46c21a4e34af3346ed4856": "This behaviour may change in the next major version.",
"2e110abee2c95bcfc2dafd48be7e2095": "Cannot configure {0}: {{config.dataSource}} must be an instance of {{DataSource}}",
"308e1d484516a33df788f873e65faaff": "Model `{0}` is extending deprecated `DataModel. Use `PersistedModel` instead.",
"3438fab56cc7ab92dfd88f0497e523e0": "The relations property of `{0}` configuration must be an object",
"4cac5f051ae431321673e04045d37772": "Model `{0}` is extending an unknown model `{1}`. Using `PersistedModel` as the base.",
"734a7bebb65e10899935126ba63dd51f": "The options property of `{0}` configuration must be an object",
"779467f467862836e19f494a37d6ab77": "The acls property of `{0}` configuration must be an array of objects",
"80a32e80cbed65eba2103201a7c94710": "Model not found: {0}",
"83cbdc2560ba9f09155ccfc63e08f1a1": "Property `{0}` cannot be reconfigured for `{1}`",
"97795efe0c3eb7f35ce8cf8cfe70682b": "The configuration of `{0}` is missing {{`dataSource`}} property.\nUse `null` or `false` to mark models not attached to any data source.",
"a80038252430df2754884bf3c845c4cf": "Remoting metadata for \"{0}.{1}\" is missing \"isStatic\" flag, the method is registered as an instance method.",
"dc568bee32deb0f6eaf63e73b20e8ceb": "Ignoring non-object \"methods\" setting of \"{0}\".",
"3aecb24fa8bdd3f79d168761ca8a6729": "Unknown {{middleware}} phase {0}"
}

View File

@ -7,6 +7,8 @@
* Module dependencies. * Module dependencies.
*/ */
var g = require('strong-globalize')();
var DataSource = require('loopback-datasource-juggler').DataSource; var DataSource = require('loopback-datasource-juggler').DataSource;
var Registry = require('./registry'); var Registry = require('./registry');
var assert = require('assert'); var assert = require('assert');
@ -237,8 +239,8 @@ app.dataSource = function(name, config) {
return ds; return ds;
} catch (err) { } catch (err) {
if (err.message) { if (err.message) {
err.message = 'Cannot create data source ' + JSON.stringify(name) + err.message = g.f('Cannot create data source %s: %s',
': ' + err.message; JSON.stringify(name), err.message);
} }
throw err; throw err;
} }
@ -322,7 +324,7 @@ app.enableAuth = function(options) {
var Model = app.registry.findModel(m); var Model = app.registry.findModel(m);
if (!Model) { if (!Model) {
throw new Error( throw new Error(
'Authentication requires model ' + m + ' to be defined.'); g.f('Authentication requires model %s to be defined.', m));
} }
if (m.dataSource || m.app) return; if (m.dataSource || m.app) return;
@ -378,17 +380,17 @@ app.enableAuth = function(options) {
var messages = { var messages = {
403: { 403: {
message: 'Access Denied', message: g.f('Access Denied'),
code: 'ACCESS_DENIED' code: 'ACCESS_DENIED',
}, },
404: { 404: {
message: ('could not find ' + modelName + ' with id ' + modelId), message: (g.f('could not find %s with id %s', modelName, modelId)),
code: 'MODEL_NOT_FOUND' code: 'MODEL_NOT_FOUND',
}, },
401: { 401: {
message: 'Authorization Required', message: g.f('Authorization Required'),
code: 'AUTHORIZATION_REQUIRED' code: 'AUTHORIZATION_REQUIRED',
} },
}; };
var e = new Error(messages[errStatusCode].message || messages[403].message); var e = new Error(messages[errStatusCode].message || messages[403].message);
@ -408,7 +410,7 @@ app.enableAuth = function(options) {
app.boot = function(options) { app.boot = function(options) {
throw new Error( throw new Error(
'`app.boot` was removed, use the new module loopback-boot instead'); g.f('{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead'));
}; };
function dataSourcesFromConfig(name, config, connectorRegistry, registry) { function dataSourcesFromConfig(name, config, connectorRegistry, registry) {
@ -486,7 +488,7 @@ function setSharedMethodSharedProperties(model, app, modelConfigs) {
var settingValue = settings[setting]; var settingValue = settings[setting];
var settingValueType = typeof settingValue; var settingValueType = typeof settingValue;
if (settingValueType !== 'boolean') if (settingValueType !== 'boolean')
throw new TypeError('Expected boolean, got ' + settingValueType); throw new TypeError(g.f('Expected boolean, got %s', settingValueType));
}); });
// set sharedMethod.shared using the merged settings // set sharedMethod.shared using the merged settings

View File

@ -7,6 +7,8 @@
* Dependencies. * Dependencies.
*/ */
var g = require('strong-globalize')();
var mailer = require('nodemailer'); var mailer = require('nodemailer');
var assert = require('assert'); var assert = require('assert');
var debug = require('debug')('loopback:connector:mail'); var debug = require('debug')('loopback:connector:mail');
@ -149,22 +151,22 @@ Mailer.send = function(options, fn) {
} }
if (debug.enabled || settings && settings.debug) { if (debug.enabled || settings && settings.debug) {
console.log('Sending Mail:'); g.log('Sending Mail:');
if (options.transport) { if (options.transport) {
console.log('\t TRANSPORT:', options.transport); console.log(g.f('\t TRANSPORT:%s', options.transport));
} }
console.log('\t TO:', options.to); g.log('\t TO:%s', options.to);
console.log('\t FROM:', options.from); g.log('\t FROM:%s', options.from);
console.log('\t SUBJECT:', options.subject); g.log('\t SUBJECT:%s', options.subject);
console.log('\t TEXT:', options.text); g.log('\t TEXT:%s', options.text);
console.log('\t HTML:', options.html); g.log('\t HTML:%s', options.html);
} }
if (transport) { if (transport) {
assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport'); assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport');
transport.sendMail(options, fn); transport.sendMail(options, fn);
} else { } else {
console.warn('Warning: No email transport specified for sending email.' + g.warn('Warning: No email transport specified for sending email.' +
' Setup a transport to send mail messages.'); ' Setup a transport to send mail messages.');
process.nextTick(function() { process.nextTick(function() {
fn(null, options); fn(null, options);

View File

@ -6,6 +6,9 @@
/*! /*!
* Module Dependencies. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var assert = require('assert'); var assert = require('assert');
var RemoteObjects = require('strong-remoting'); var RemoteObjects = require('strong-remoting');
var SharedClass = require('strong-remoting').SharedClass; var SharedClass = require('strong-remoting').SharedClass;
@ -168,14 +171,14 @@ module.exports = function(registry) {
} else if (model) { } else if (model) {
fn(null, model); fn(null, model);
} else { } else {
err = new Error('could not find a model with id ' + id); err = new Error(g.f('could not find a model with {{id}} %s', id));
err.statusCode = 404; err.statusCode = 404;
err.code = 'MODEL_NOT_FOUND'; err.code = 'MODEL_NOT_FOUND';
fn(err); fn(err);
} }
}); });
} else { } else {
fn(new Error('must specify an id or data')); fn(new Error(g.f('must specify an {{id}} or {{data}}')));
} }
}; };
@ -449,8 +452,8 @@ module.exports = function(registry) {
http: {verb: 'get', path: '/' + pathName}, http: {verb: 'get', path: '/' + pathName},
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
accessType: 'READ', accessType: 'READ',
description: 'Fetches belongsTo relation ' + relationName + '.', description: g.f('Fetches belongsTo relation %s.', relationName),
returns: {arg: relationName, type: modelName, root: true} returns: {arg: relationName, type: modelName, root: true},
}, fn); }, fn);
}; };
@ -458,7 +461,7 @@ module.exports = function(registry) {
if (ctx.result !== null) return cb(); if (ctx.result !== null) return cb();
var fk = ctx.getArgByName('fk'); var fk = ctx.getArgByName('fk');
var msg = 'Unknown "' + toModelName + '" id "' + fk + '".'; var msg = g.f('Unknown "%s" id "%s".', toModelName, fk);
var error = new Error(msg); var error = new Error(msg);
error.statusCode = error.status = 404; error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND'; error.code = 'MODEL_NOT_FOUND';
@ -473,7 +476,7 @@ module.exports = function(registry) {
isStatic: false, isStatic: false,
http: {verb: 'get', path: '/' + pathName}, http: {verb: 'get', path: '/' + pathName},
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}}, accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
description: 'Fetches hasOne relation ' + relationName + '.', description: g.f('Fetches hasOne relation %s.', relationName),
accessType: 'READ', accessType: 'READ',
returns: {arg: relationName, type: relation.modelTo.modelName, root: true}, returns: {arg: relationName, type: relation.modelTo.modelName, root: true},
rest: {after: convertNullToNotFoundError.bind(null, toModelName)} rest: {after: convertNullToNotFoundError.bind(null, toModelName)}
@ -483,7 +486,7 @@ module.exports = function(registry) {
isStatic: false, isStatic: false,
http: {verb: 'post', path: '/' + pathName}, http: {verb: 'post', path: '/' + pathName},
accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}},
description: 'Creates a new instance in ' + relationName + ' of this model.', description: g.f('Creates a new instance in %s of this model.', relationName),
accessType: 'WRITE', accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true} returns: {arg: 'data', type: toModelName, root: true}
}); });
@ -492,7 +495,7 @@ module.exports = function(registry) {
isStatic: false, isStatic: false,
http: {verb: 'put', path: '/' + pathName}, http: {verb: 'put', path: '/' + pathName},
accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}},
description: 'Update ' + relationName + ' of this model.', description: g.f('Update %s of this model.', relationName),
accessType: 'WRITE', accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true} returns: {arg: 'data', type: toModelName, root: true}
}); });
@ -500,8 +503,8 @@ module.exports = function(registry) {
define('__destroy__' + relationName, { define('__destroy__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'delete', path: '/' + pathName}, http: {verb: 'delete', path: '/' + pathName},
description: 'Deletes ' + relationName + ' of this model.', description: g.f('Deletes %s of this model.', relationName),
accessType: 'WRITE' accessType: 'WRITE',
}); });
}; };
@ -514,9 +517,10 @@ module.exports = function(registry) {
isStatic: false, isStatic: false,
http: {verb: 'get', path: '/' + pathName + '/:fk'}, http: {verb: 'get', path: '/' + pathName + '/:fk'},
accepts: {arg: 'fk', type: 'any', accepts: {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true, description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'}}, http: {source: 'path'}},
description: 'Find a related item by id for ' + relationName + '.', description: g.f('Find a related item by id for %s.', relationName),
accessType: 'READ', accessType: 'READ',
returns: {arg: 'result', type: toModelName, root: true}, returns: {arg: 'result', type: toModelName, root: true},
rest: {after: convertNullToNotFoundError.bind(null, toModelName)} rest: {after: convertNullToNotFoundError.bind(null, toModelName)}
@ -526,10 +530,11 @@ module.exports = function(registry) {
define('__destroyById__' + relationName, { define('__destroyById__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'delete', path: '/' + pathName + '/:fk'}, http: {verb: 'delete', path: '/' + pathName + '/:fk'},
accepts: {arg: 'fk', type: 'any', accepts: { arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true, description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'}}, http: {source: 'path'}},
description: 'Delete a related item by id for ' + relationName + '.', description: g.f('Delete a related item by id for %s.', relationName),
accessType: 'WRITE', accessType: 'WRITE',
returns: [] returns: []
}, destroyByIdFunc); }, destroyByIdFunc);
@ -540,11 +545,12 @@ module.exports = function(registry) {
http: {verb: 'put', path: '/' + pathName + '/:fk'}, http: {verb: 'put', path: '/' + pathName + '/:fk'},
accepts: [ accepts: [
{arg: 'fk', type: 'any', {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true, description: g.f('Foreign key for %s', relationName),
http: {source: 'path'}}, required: true,
{arg: 'data', type: toModelName, http: {source: 'body'}} http: { source: 'path' }},
{arg: 'data', type: toModelName, http: {source: 'body'}},
], ],
description: 'Update a related item by id for ' + relationName + '.', description: g.f('Update a related item by id for %s.', relationName),
accessType: 'WRITE', accessType: 'WRITE',
returns: {arg: 'result', type: toModelName, root: true} returns: {arg: 'result', type: toModelName, root: true}
}, updateByIdFunc); }, updateByIdFunc);
@ -562,10 +568,11 @@ module.exports = function(registry) {
define('__link__' + relationName, { define('__link__' + relationName, {
isStatic: false, isStatic: false,
http: {verb: 'put', path: '/' + pathName + '/rel/:fk'}, http: {verb: 'put', path: '/' + pathName + '/rel/:fk'},
accepts: [{arg: 'fk', type: 'any', accepts: [{ arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true, description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'}}].concat(accepts), http: {source: 'path'}}].concat(accepts),
description: 'Add a related item by id for ' + relationName + '.', description: g.f('Add a related item by id for %s.', relationName),
accessType: 'WRITE', accessType: 'WRITE',
returns: {arg: relationName, type: modelThrough.modelName, root: true} returns: {arg: relationName, type: modelThrough.modelName, root: true}
}, addFunc); }, addFunc);
@ -575,9 +582,10 @@ module.exports = function(registry) {
isStatic: false, isStatic: false,
http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'}, http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'},
accepts: {arg: 'fk', type: 'any', accepts: {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true, description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'}}, http: {source: 'path'}},
description: 'Remove the ' + relationName + ' relation to an item by id.', description: g.f('Remove the %s relation to an item by id.', relationName),
accessType: 'WRITE', accessType: 'WRITE',
returns: [] returns: []
}, removeFunc); }, removeFunc);
@ -589,9 +597,10 @@ module.exports = function(registry) {
isStatic: false, isStatic: false,
http: {verb: 'head', path: '/' + pathName + '/rel/:fk'}, http: {verb: 'head', path: '/' + pathName + '/rel/:fk'},
accepts: {arg: 'fk', type: 'any', accepts: {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true, description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'}}, http: {source: 'path'}},
description: 'Check the existence of ' + relationName + ' relation to an item by id.', description: g.f('Check the existence of %s relation to an item by id.', relationName),
accessType: 'READ', accessType: 'READ',
returns: {arg: 'exists', type: 'boolean', root: true}, returns: {arg: 'exists', type: 'boolean', root: true},
rest: { rest: {
@ -600,7 +609,7 @@ module.exports = function(registry) {
if (ctx.result === false) { if (ctx.result === false) {
var modelName = ctx.method.sharedClass.name; var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id'); var id = ctx.getArgByName('id');
var msg = 'Unknown "' + modelName + '" id "' + id + '".'; var msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id);
var error = new Error(msg); var error = new Error(msg);
error.statusCode = error.status = 404; error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND'; error.code = 'MODEL_NOT_FOUND';
@ -634,7 +643,7 @@ module.exports = function(registry) {
isStatic: isStatic, isStatic: isStatic,
http: {verb: 'get', path: '/' + pathName}, http: {verb: 'get', path: '/' + pathName},
accepts: {arg: 'filter', type: 'object'}, accepts: {arg: 'filter', type: 'object'},
description: 'Queries ' + scopeName + ' of ' + this.modelName + '.', description: g.f('Queries %s of %s.', scopeName, this.modelName),
accessType: 'READ', accessType: 'READ',
returns: {arg: scopeName, type: [toModelName], root: true} returns: {arg: scopeName, type: [toModelName], root: true}
}); });
@ -643,7 +652,7 @@ module.exports = function(registry) {
isStatic: isStatic, isStatic: isStatic,
http: {verb: 'post', path: '/' + pathName}, http: {verb: 'post', path: '/' + pathName},
accepts: {arg: 'data', type: toModelName, http: {source: 'body'}}, accepts: {arg: 'data', type: toModelName, http: {source: 'body'}},
description: 'Creates a new instance in ' + scopeName + ' of this model.', description: g.f('Creates a new instance in %s of this model.', scopeName),
accessType: 'WRITE', accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true} returns: {arg: 'data', type: toModelName, root: true}
}); });
@ -651,15 +660,16 @@ module.exports = function(registry) {
define('__delete__' + scopeName, { define('__delete__' + scopeName, {
isStatic: isStatic, isStatic: isStatic,
http: {verb: 'delete', path: '/' + pathName}, http: {verb: 'delete', path: '/' + pathName},
description: 'Deletes all ' + scopeName + ' of this model.', description: g.f('Deletes all %s of this model.', scopeName),
accessType: 'WRITE' accessType: 'WRITE',
}); });
define('__count__' + scopeName, { define('__count__' + scopeName, {
isStatic: isStatic, isStatic: isStatic,
http: {verb: 'get', path: '/' + pathName + '/count'}, http: {verb: 'get', path: '/' + pathName + '/count'},
accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, accepts: {arg: 'where', type: 'object',
description: 'Counts ' + scopeName + ' of ' + this.modelName + '.', description: g.f('Criteria to match model instances')},
description: g.f('Counts %s of %s.', scopeName, this.modelName),
accessType: 'READ', accessType: 'READ',
returns: {arg: 'count', type: 'number'} returns: {arg: 'count', type: 'number'}
}); });
@ -708,9 +718,9 @@ module.exports = function(registry) {
acceptArgs = [ acceptArgs = [
{ {
arg: paramName, type: 'any', http: { source: 'path' }, arg: paramName, type: 'any', http: { source: 'path' },
description: 'Foreign key for ' + relation.name + '.', description: g.f('Foreign key for %s.', relation.name),
required: true required: true,
} },
]; ];
} else { } else {
httpPath = pathName; httpPath = pathName;
@ -738,12 +748,12 @@ module.exports = function(registry) {
var getterFn = relation.modelFrom.prototype[getterName]; var getterFn = relation.modelFrom.prototype[getterName];
if (typeof getterFn !== 'function') { if (typeof getterFn !== 'function') {
throw new Error('Invalid remote method: `' + getterName + '`'); throw new Error(g.f('Invalid remote method: `%s`', getterName));
} }
var nestedFn = relation.modelTo.prototype[method.name]; var nestedFn = relation.modelTo.prototype[method.name];
if (typeof nestedFn !== 'function') { if (typeof nestedFn !== 'function') {
throw new Error('Invalid remote method: `' + method.name + '`'); throw new Error(g.f('Invalid remote method: `%s`', method.name));
} }
var opts = {}; var opts = {};
@ -836,7 +846,8 @@ module.exports = function(registry) {
}); });
} else { } else {
throw new Error('Relation `' + relationName + '` does not exist for model `' + this.modelName + '`'); var msg = g.f('Relation `%s` does not exist for model `%s`', relationName, this.modelName);
throw new Error(msg);
} }
}; };

View File

@ -7,6 +7,7 @@
* Module Dependencies. * Module Dependencies.
*/ */
var g = require('strong-globalize')();
var runtime = require('./runtime'); var runtime = require('./runtime');
var assert = require('assert'); var assert = require('assert');
var async = require('async'); var async = require('async');
@ -65,9 +66,10 @@ module.exports = function(registry) {
function throwNotAttached(modelName, methodName) { function throwNotAttached(modelName, methodName) {
throw new Error( throw new Error(
'Cannot call ' + modelName + '.' + methodName + '().' + g.f('Cannot call %s.%s().' +
' The ' + methodName + ' method has not been setup.' + ' The %s method has not been setup.' +
' The PersistedModel has not been correctly attached to a DataSource!' ' The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!',
modelName, methodName, methodName)
); );
} }
@ -82,7 +84,7 @@ module.exports = function(registry) {
var modelName = ctx.method.sharedClass.name; var modelName = ctx.method.sharedClass.name;
var id = ctx.getArgByName('id'); var id = ctx.getArgByName('id');
var msg = 'Unknown "' + modelName + '" id "' + id + '".'; var msg = g.f('Unknown "%s" {{id}} "%s".', modelName, id);
var error = new Error(msg); var error = new Error(msg);
error.statusCode = error.status = 404; error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND'; error.code = 'MODEL_NOT_FOUND';
@ -558,7 +560,7 @@ module.exports = function(registry) {
} }
setRemoting(PersistedModel, 'create', { setRemoting(PersistedModel, 'create', {
description: 'Create a new instance of the model and persist it into the data source.', description: g.f('Create a new instance of the model and persist it into the data source.'),
accessType: 'WRITE', accessType: 'WRITE',
accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
returns: {arg: 'data', type: typeName, root: true}, returns: {arg: 'data', type: typeName, root: true},
@ -567,7 +569,8 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'upsert', { setRemoting(PersistedModel, 'upsert', {
aliases: ['updateOrCreate'], aliases: ['updateOrCreate'],
description: 'Update an existing model instance or insert a new one into the data source.', description: g.f('Update an existing model instance or insert a new one ' +
'into the data source.'),
accessType: 'WRITE', accessType: 'WRITE',
accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
returns: {arg: 'data', type: typeName, root: true}, returns: {arg: 'data', type: typeName, root: true},
@ -575,7 +578,7 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'exists', { setRemoting(PersistedModel, 'exists', {
description: 'Check whether a model instance exists in the data source.', description: g.f('Check whether a model instance exists in the data source.'),
accessType: 'READ', accessType: 'READ',
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true}, accepts: {arg: 'id', type: 'any', description: 'Model id', required: true},
returns: {arg: 'exists', type: 'boolean'}, returns: {arg: 'exists', type: 'boolean'},
@ -606,13 +609,13 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'findById', { setRemoting(PersistedModel, 'findById', {
description: 'Find a model instance by id from the data source.', description: g.f('Find a model instance by {{id}} from the data source.'),
accessType: 'READ', accessType: 'READ',
accepts: [ accepts: [
{ arg: 'id', type: 'any', description: 'Model id', required: true, { arg: 'id', type: 'any', description: 'Model id', required: true,
http: {source: 'path'}}, http: {source: 'path'}},
{ arg: 'filter', type: 'object', { arg: 'filter', type: 'object',
description: 'Filter defining fields and include'} description: g.f('Filter defining fields and include') },
], ],
returns: {arg: 'data', type: typeName, root: true}, returns: {arg: 'data', type: typeName, root: true},
http: {verb: 'get', path: '/:id'}, http: {verb: 'get', path: '/:id'},
@ -620,7 +623,7 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'find', { setRemoting(PersistedModel, 'find', {
description: 'Find all instances of the model matched by filter from the data source.', description: g.f('Find all instances of the model matched by filter from the data source.'),
accessType: 'READ', accessType: 'READ',
accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'},
returns: {arg: 'data', type: [typeName], root: true}, returns: {arg: 'data', type: [typeName], root: true},
@ -628,7 +631,7 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'findOne', { setRemoting(PersistedModel, 'findOne', {
description: 'Find first instance of the model matched by filter from the data source.', description: g.f('Find first instance of the model matched by filter from the data source.'),
accessType: 'READ', accessType: 'READ',
accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'}, accepts: {arg: 'filter', type: 'object', description: 'Filter defining fields, where, include, order, offset, and limit'},
returns: {arg: 'data', type: typeName, root: true}, returns: {arg: 'data', type: typeName, root: true},
@ -637,7 +640,7 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'destroyAll', { setRemoting(PersistedModel, 'destroyAll', {
description: 'Delete all matching records.', description: g.f('Delete all matching records.'),
accessType: 'WRITE', accessType: 'WRITE',
accepts: {arg: 'where', type: 'object', description: 'filter.where object'}, accepts: {arg: 'where', type: 'object', description: 'filter.where object'},
returns: { returns: {
@ -652,17 +655,17 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'updateAll', { setRemoting(PersistedModel, 'updateAll', {
aliases: ['update'], aliases: ['update'],
description: 'Update instances of the model matched by where from the data source.', description: g.f('Update instances of the model matched by {{where}} from the data source.'),
accessType: 'WRITE', accessType: 'WRITE',
accepts: [ accepts: [
{arg: 'where', type: 'object', http: {source: 'query'}, {arg: 'where', type: 'object', http: { source: 'query'},
description: 'Criteria to match model instances'}, description: g.f('Criteria to match model instances')},
{arg: 'data', type: 'object', http: {source: 'body'}, {arg: 'data', type: 'object', http: {source: 'body'},
description: 'An object of model property name/value pairs'}, description: g.f('An object of model property name/value pairs')},
], ],
returns: { returns: {
arg: 'count', arg: 'count',
description: 'The number of instances updated', description: g.f('The number of instances updated'),
type: 'object', type: 'object',
root: true root: true
}, },
@ -671,7 +674,7 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'deleteById', { setRemoting(PersistedModel, 'deleteById', {
aliases: ['destroyById', 'removeById'], aliases: ['destroyById', 'removeById'],
description: 'Delete a model instance by id from the data source.', description: g.f('Delete a model instance by {{id}} from the data source.'),
accessType: 'WRITE', accessType: 'WRITE',
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true, accepts: {arg: 'id', type: 'any', description: 'Model id', required: true,
http: {source: 'path'}}, http: {source: 'path'}},
@ -680,7 +683,7 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'count', { setRemoting(PersistedModel, 'count', {
description: 'Count instances of the model matched by where from the data source.', description: g.f('Count instances of the model matched by where from the data source.'),
accessType: 'READ', accessType: 'READ',
accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'}, accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'},
returns: {arg: 'count', type: 'number'}, returns: {arg: 'count', type: 'number'},
@ -688,7 +691,8 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel.prototype, 'updateAttributes', { setRemoting(PersistedModel.prototype, 'updateAttributes', {
description: 'Update attributes for a model instance and persist it into the data source.', description: g.f('Update attributes for a model instance and persist it into ' +
'the data source.'),
accessType: 'WRITE', accessType: 'WRITE',
accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'}, accepts: {arg: 'data', type: 'object', http: {source: 'body'}, description: 'An object of model property name/value pairs'},
returns: {arg: 'data', type: typeName, root: true}, returns: {arg: 'data', type: typeName, root: true},
@ -697,7 +701,7 @@ module.exports = function(registry) {
if (options.trackChanges || options.enableRemoteReplication) { if (options.trackChanges || options.enableRemoteReplication) {
setRemoting(PersistedModel, 'diff', { setRemoting(PersistedModel, 'diff', {
description: 'Get a set of deltas and conflicts since the given checkpoint.', description: g.f('Get a set of deltas and conflicts since the given checkpoint.'),
accessType: 'READ', accessType: 'READ',
accepts: [ accepts: [
{arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'}, {arg: 'since', type: 'number', description: 'Find deltas since this checkpoint'},
@ -709,8 +713,8 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'changes', { setRemoting(PersistedModel, 'changes', {
description: 'Get the changes to a model since a given checkpoint.' + description: g.f('Get the changes to a model since a given checkpoint.' +
'Provide a filter object to reduce the number of results returned.', 'Provide a filter object to reduce the number of results returned.'),
accessType: 'READ', accessType: 'READ',
accepts: [ accepts: [
{arg: 'since', type: 'number', description: 'Only return changes since this checkpoint'}, {arg: 'since', type: 'number', description: 'Only return changes since this checkpoint'},
@ -721,7 +725,7 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'checkpoint', { setRemoting(PersistedModel, 'checkpoint', {
description: 'Create a checkpoint.', description: g.f('Create a checkpoint.'),
// The replication algorithm needs to create a source checkpoint, // The replication algorithm needs to create a source checkpoint,
// even though it is otherwise not making any source changes. // even though it is otherwise not making any source changes.
// We need to allow this method for users that don't have full // We need to allow this method for users that don't have full
@ -732,14 +736,14 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'currentCheckpoint', { setRemoting(PersistedModel, 'currentCheckpoint', {
description: 'Get the current checkpoint.', description: g.f('Get the current checkpoint.'),
accessType: 'READ', accessType: 'READ',
returns: {arg: 'checkpoint', type: 'object', root: true}, returns: {arg: 'checkpoint', type: 'object', root: true},
http: {verb: 'get', path: '/checkpoint'} http: {verb: 'get', path: '/checkpoint'}
}); });
setRemoting(PersistedModel, 'createUpdates', { setRemoting(PersistedModel, 'createUpdates', {
description: 'Create an update list from a delta list.', description: g.f('Create an update list from a delta list.'),
// This operation is read-only, it does not change any local data. // This operation is read-only, it does not change any local data.
// It is called by the replication algorithm to compile a list // It is called by the replication algorithm to compile a list
// of changes to apply on the target. // of changes to apply on the target.
@ -750,14 +754,14 @@ module.exports = function(registry) {
}); });
setRemoting(PersistedModel, 'bulkUpdate', { setRemoting(PersistedModel, 'bulkUpdate', {
description: 'Run multiple updates at once. Note: this is not atomic.', description: g.f('Run multiple updates at once. Note: this is not atomic.'),
accessType: 'WRITE', accessType: 'WRITE',
accepts: {arg: 'updates', type: 'array'}, accepts: {arg: 'updates', type: 'array'},
http: {verb: 'post', path: '/bulk-update'} http: {verb: 'post', path: '/bulk-update'}
}); });
setRemoting(PersistedModel, 'findLastChange', { setRemoting(PersistedModel, 'findLastChange', {
description: 'Get the most recent change record for this instance.', description: g.f('Get the most recent change record for this instance.'),
accessType: 'READ', accessType: 'READ',
accepts: { accepts: {
arg: 'id', type: 'any', required: true, http: { source: 'path' }, arg: 'id', type: 'any', required: true, http: { source: 'path' },
@ -769,8 +773,8 @@ module.exports = function(registry) {
setRemoting(PersistedModel, 'updateLastChange', { setRemoting(PersistedModel, 'updateLastChange', {
description: [ description: [
'Update the properties of the most recent change record', g.f('Update the properties of the most recent change record ' +
'kept for this instance.' 'kept for this instance.'),
], ],
accessType: 'WRITE', accessType: 'WRITE',
accepts: [ accepts: [
@ -780,7 +784,7 @@ module.exports = function(registry) {
}, },
{ {
arg: 'data', type: 'object', http: {source: 'body'}, arg: 'data', type: 'object', http: {source: 'body'},
description: 'An object of Change property name/value pairs' description: g.f('An object of Change property name/value pairs'),
}, },
], ],
returns: { arg: 'result', type: this.Change.modelName, root: true }, returns: { arg: 'result', type: this.Change.modelName, root: true },
@ -806,7 +810,7 @@ module.exports = function(registry) {
} }
setRemoting(PersistedModel, 'createChangeStream', { setRemoting(PersistedModel, 'createChangeStream', {
description: 'Create a change stream.', description: g.f('Create a change stream.'),
accessType: 'READ', accessType: 'READ',
http: [ http: [
{verb: 'post', path: '/change-stream'}, {verb: 'post', path: '/change-stream'},
@ -1148,7 +1152,7 @@ module.exports = function(registry) {
if (err) return cb(err); if (err) return cb(err);
if (!inst) { if (!inst) {
return cb && return cb &&
cb(new Error('Missing data for change: ' + change.modelId)); cb(new Error(g.f('Missing data for change: %s', change.modelId)));
} }
if (inst.toObject) { if (inst.toObject) {
update.data = inst.toObject(); update.data = inst.toObject();
@ -1216,7 +1220,7 @@ module.exports = function(registry) {
async.parallel(tasks, function(err) { async.parallel(tasks, function(err) {
if (err) return callback(err); if (err) return callback(err);
if (conflicts.length) { if (conflicts.length) {
err = new Error('Conflict'); err = new Error(g.f('Conflict'));
err.statusCode = 409; err.statusCode = 409;
err.details = { conflicts: conflicts }; err.details = { conflicts: conflicts };
return callback(err); return callback(err);
@ -1280,16 +1284,16 @@ module.exports = function(registry) {
case undefined: case undefined:
case null: case null:
return cb(new Error( return cb(new Error(
'Cannot apply bulk updates, ' + g.f('Cannot apply bulk updates, ' +
'the connector does not correctly report ' + 'the connector does not correctly report ' +
'the number of updated records.')); 'the number of updated records.')));
default: default:
debug('%s.updateAll modified unexpected number of instances: %j', debug('%s.updateAll modified unexpected number of instances: %j',
Model.modelName, count); Model.modelName, count);
return cb(new Error( return cb(new Error(
'Bulk update failed, the connector has modified unexpected ' + g.f('Bulk update failed, the connector has modified unexpected ' +
'number of records: ' + JSON.stringify(count))); 'number of records: %s', JSON.stringify(count))));
} }
}); });
} }
@ -1362,16 +1366,16 @@ module.exports = function(registry) {
case undefined: case undefined:
case null: case null:
return cb(new Error( return cb(new Error(
'Cannot apply bulk updates, ' + g.f('Cannot apply bulk updates, ' +
'the connector does not correctly report ' + 'the connector does not correctly report ' +
'the number of deleted records.')); 'the number of deleted records.')));
default: default:
debug('%s.deleteAll modified unexpected number of instances: %j', debug('%s.deleteAll modified unexpected number of instances: %j',
Model.modelName, count); Model.modelName, count);
return cb(new Error( return cb(new Error(
'Bulk update failed, the connector has deleted unexpected ' + g.f('Bulk update failed, the connector has deleted unexpected ' +
'number of records: ' + JSON.stringify(count))); 'number of records: %s', JSON.stringify(count))));
} }
}); });
} }
@ -1586,8 +1590,8 @@ module.exports = function(registry) {
this.findLastChange(id, function(err, inst) { this.findLastChange(id, function(err, inst) {
if (err) return cb(err); if (err) return cb(err);
if (!inst) { if (!inst) {
err = new Error('No change record found for ' + err = new Error(g.f('No change record found for %s with id %s',
self.modelName + ' with id ' + id); self.modelName, id));
err.statusCode = 404; err.statusCode = 404;
return cb(err); return cb(err);
} }

View File

@ -3,6 +3,7 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
var assert = require('assert'); var assert = require('assert');
var extend = require('util')._extend; var extend = require('util')._extend;
var juggler = require('loopback-datasource-juggler'); var juggler = require('loopback-datasource-juggler');
@ -113,11 +114,11 @@ Registry.prototype.createModel = function(name, properties, options) {
if (BaseModel === undefined) { if (BaseModel === undefined) {
if (baseName === 'DataModel') { if (baseName === 'DataModel') {
console.warn('Model `%s` is extending deprecated `DataModel. ' + g.warn('Model `%s` is extending deprecated `DataModel. ' +
'Use `PersistedModel` instead.', name); 'Use `PersistedModel` instead.', name);
BaseModel = this.getModel('PersistedModel'); BaseModel = this.getModel('PersistedModel');
} else { } else {
console.warn('Model `%s` is extending an unknown model `%s`. ' + g.warn('Model `%s` is extending an unknown model `%s`. ' +
'Using `PersistedModel` as the base.', name, baseName); 'Using `PersistedModel` as the base.', name, baseName);
} }
} }
@ -197,7 +198,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
relations[key] = extend(relations[key] || {}, config.relations[key]); relations[key] = extend(relations[key] || {}, config.relations[key]);
}); });
} else if (config.relations != null) { } else if (config.relations != null) {
console.warn('The relations property of `%s` configuration ' + g.warn('The relations property of `%s` configuration ' +
'must be an object', modelName); 'must be an object', modelName);
} }
@ -208,7 +209,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
addACL(acls, acl); addACL(acls, acl);
}); });
} else if (config.acls != null) { } else if (config.acls != null) {
console.warn('The acls property of `%s` configuration ' + g.warn('The acls property of `%s` configuration ' +
'must be an array of objects', modelName); 'must be an array of objects', modelName);
} }
@ -225,12 +226,12 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
if (!(p in excludedProperties)) { if (!(p in excludedProperties)) {
settings[p] = config.options[p]; settings[p] = config.options[p];
} else { } else {
console.warn('Property `%s` cannot be reconfigured for `%s`', g.warn('Property `%s` cannot be reconfigured for `%s`',
p, modelName); p, modelName);
} }
} }
} else if (config.options != null) { } else if (config.options != null) {
console.warn('The options property of `%s` configuration ' + g.warn('The options property of `%s` configuration ' +
'must be an object', modelName); 'must be an object', modelName);
} }
@ -238,8 +239,8 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
// configuration, so that the datasource picks up updated relations // configuration, so that the datasource picks up updated relations
if (config.dataSource) { if (config.dataSource) {
assert(config.dataSource instanceof DataSource, assert(config.dataSource instanceof DataSource,
'Cannot configure ' + ModelCtor.modelName + g.f('Cannot configure %s: {{config.dataSource}} must be an instance ' +
': config.dataSource must be an instance of DataSource'); 'of {{DataSource}}', ModelCtor.modelName));
ModelCtor.attachTo(config.dataSource); ModelCtor.attachTo(config.dataSource);
debug('Attached model `%s` to dataSource `%s`', debug('Attached model `%s` to dataSource `%s`',
modelName, config.dataSource.name); modelName, config.dataSource.name);
@ -249,8 +250,8 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
} else { } else {
debug('Model `%s` is not attached to any DataSource, possibly by a mistake.', debug('Model `%s` is not attached to any DataSource, possibly by a mistake.',
modelName); modelName);
console.warn( g.warn(
'The configuration of `%s` is missing `dataSource` property.\n' + 'The configuration of `%s` is missing {{`dataSource`}} property.\n' +
'Use `null` or `false` to mark models not attached to any data source.', 'Use `null` or `false` to mark models not attached to any data source.',
modelName); modelName);
} }
@ -262,7 +263,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) { Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) {
if (!methods) return; if (!methods) return;
if (typeof methods !== 'object') { if (typeof methods !== 'object') {
console.warn('Ignoring non-object "methods" setting of "%s".', g.warn('Ignoring non-object "methods" setting of "%s".',
ModelCtor.modelName); ModelCtor.modelName);
return; return;
} }
@ -270,11 +271,11 @@ Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) {
Object.keys(methods).forEach(function(key) { Object.keys(methods).forEach(function(key) {
var meta = methods[key]; var meta = methods[key];
if (typeof meta.isStatic !== 'boolean') { if (typeof meta.isStatic !== 'boolean') {
console.warn('Remoting metadata for "%s.%s" is missing "isStatic" ' + g.warn('Remoting metadata for "%s.%s" is missing "isStatic" ' +
'flag, the method is registered as an instance method.', 'flag, the method is registered as an instance method.',
ModelCtor.modelName, ModelCtor.modelName,
key); key);
console.warn('This behaviour may change in the next major version.'); g.warn('This behaviour may change in the next major version.');
} }
ModelCtor.remoteMethod(key, meta); ModelCtor.remoteMethod(key, meta);
}); });
@ -306,7 +307,7 @@ Registry.prototype.getModel = function(modelName) {
var model = this.findModel(modelName); var model = this.findModel(modelName);
if (model) return model; if (model) return model;
throw new Error('Model not found: ' + modelName); throw new Error(g.f('Model not found: %s', modelName));
}; };
/** /**

View File

@ -3,6 +3,8 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
var assert = require('assert'); var assert = require('assert');
var express = require('express'); var express = require('express');
var merge = require('util')._extend; var merge = require('util')._extend;
@ -188,7 +190,7 @@ proto.middleware = function(name, paths, handler) {
} }
if (this._requestHandlingPhases.indexOf(name) === -1) if (this._requestHandlingPhases.indexOf(name) === -1)
throw new Error('Unknown middleware phase ' + name); throw new Error(g.f('Unknown {{middleware}} phase %s', name));
debug('use %s %s %s', fullPhaseName, paths, handlerName); debug('use %s %s %s', fullPhaseName, paths, handlerName);

View File

@ -50,6 +50,7 @@
"nodemailer-stub-transport": "^1.0.0", "nodemailer-stub-transport": "^1.0.0",
"serve-favicon": "^2.2.0", "serve-favicon": "^2.2.0",
"stable": "^0.1.5", "stable": "^0.1.5",
"strong-globalize": "^2.6.2",
"strong-remoting": "^2.21.0", "strong-remoting": "^2.21.0",
"uid2": "0.0.3", "uid2": "0.0.3",
"underscore.string": "^3.0.3" "underscore.string": "^3.0.3"